From c31603b8fb8d88839f59d0fa690b46e9cbfe8cca Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Wed, 15 Jul 2026 15:12:51 +0200 Subject: [PATCH 01/11] =?UTF-8?q?Fix=20GO-2026-5410:=20upgrade=20slack-go/?= =?UTF-8?q?slack=20v0.15.0=20=E2=86=92=20v0.23.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves vulnerability GO-2026-5410 by upgrading github.com/slack-go/slack to v0.23.1. Adapts to breaking API changes: - UploadFileV2Parameters renamed to UploadFileParameters - RichTextPreformatted struct flattened (no longer embeds RichTextSection) - TextBlockObject.Emoji changed from bool to *bool - WorkflowStep types removed (deprecated Steps from Apps feature): defines local types and HTTP client for workflows.updateStep, workflows.stepCompleted, and workflows.stepFailed API calls - InteractionCallback.WorkflowStep field removed: extracts workflow_step_edit_id from raw JSON in handleInteraction Co-Authored-By: Claude Opus 4.6 --- cmd/ci-chat-bot/slack.go | 15 +- go.mod | 2 +- go.sum | 7 +- pkg/slack/actions_request_test.go | 14 +- pkg/slack/events/router/router.go | 4 +- .../events/workflowSubmissionEvents/types.go | 131 ++- .../workflow_handler.go | 29 +- pkg/slack/interactions/router/router.go | 17 +- pkg/slack/modals/common/simple_modals.go | 8 +- pkg/slack/modals/common/version_views.go | 20 +- pkg/slack/modals/launch/views.go | 4 +- pkg/slack/modals/list/views.go | 2 +- pkg/slack/modals/mce/create/views.go | 4 +- pkg/slack/modals/stepsFromApp/jira_step.go | 2 +- .../modals/stepsFromApp/workflow_submit.go | 9 +- pkg/slack/parser/types.go | 4 +- pkg/slack/slack.go | 12 +- vendor/github.com/slack-go/slack/.gitignore | 3 +- .../github.com/slack-go/slack/.golangci.yml | 29 +- vendor/github.com/slack-go/slack/CHANGELOG.md | 683 ++++++++++++--- .../github.com/slack-go/slack/CONTRIBUTING.md | 40 + vendor/github.com/slack-go/slack/Makefile | 4 +- vendor/github.com/slack-go/slack/README.md | 53 +- vendor/github.com/slack-go/slack/TODO.txt | 3 - vendor/github.com/slack-go/slack/admin.go | 12 +- .../slack-go/slack/admin_conversations.go | 807 ++++++++++++++++++ .../slack-go/slack/admin_conversations_ekm.go | 101 +++ .../admin_conversations_restrictAccess.go | 150 ++++ .../github.com/slack-go/slack/admin_roles.go | 204 +++++ .../github.com/slack-go/slack/admin_teams.go | 153 ++++ vendor/github.com/slack-go/slack/apps.go | 2 +- vendor/github.com/slack-go/slack/assistant.go | 377 ++++++++ .../github.com/slack-go/slack/attachments.go | 7 +- vendor/github.com/slack-go/slack/audit.go | 3 +- vendor/github.com/slack-go/slack/auth.go | 9 +- vendor/github.com/slack-go/slack/block.go | 37 +- .../github.com/slack-go/slack/block_action.go | 5 + .../github.com/slack-go/slack/block_alert.go | 70 ++ .../github.com/slack-go/slack/block_call.go | 76 +- .../github.com/slack-go/slack/block_card.go | 90 ++ .../slack-go/slack/block_carousel.go | 42 + .../slack-go/slack/block_context.go | 5 + .../slack-go/slack/block_context_actions.go | 31 + .../github.com/slack-go/slack/block_conv.go | 51 +- .../slack-go/slack/block_divider.go | 6 +- .../slack-go/slack/block_element.go | 255 +++++- .../github.com/slack-go/slack/block_file.go | 5 + .../github.com/slack-go/slack/block_header.go | 9 +- .../github.com/slack-go/slack/block_image.go | 17 + .../github.com/slack-go/slack/block_input.go | 5 + .../github.com/slack-go/slack/block_json.go | 110 +++ .../slack-go/slack/block_markdown.go | 34 + .../github.com/slack-go/slack/block_object.go | 29 +- .../github.com/slack-go/slack/block_plan.go | 55 ++ .../slack-go/slack/block_rich_text.go | 87 +- .../slack-go/slack/block_section.go | 15 + .../github.com/slack-go/slack/block_table.go | 55 ++ .../slack-go/slack/block_task_card.go | 101 +++ .../slack-go/slack/block_unknown.go | 27 +- .../github.com/slack-go/slack/block_video.go | 5 + vendor/github.com/slack-go/slack/channels.go | 2 +- vendor/github.com/slack-go/slack/chat.go | 345 +++++++- .../slack-go/slack/chat_stream_chunks.go | 95 +++ .../github.com/slack-go/slack/conversation.go | 269 +++++- vendor/github.com/slack-go/slack/dialog.go | 3 +- vendor/github.com/slack-go/slack/dnd.go | 36 +- vendor/github.com/slack-go/slack/entity.go | 132 +++ vendor/github.com/slack-go/slack/files.go | 243 +++--- .../slack-go/slack/function_execute.go | 6 +- vendor/github.com/slack-go/slack/huddle.go | 64 ++ vendor/github.com/slack-go/slack/im.go | 21 - vendor/github.com/slack-go/slack/info.go | 36 +- .../github.com/slack-go/slack/interactions.go | 75 +- vendor/github.com/slack-go/slack/manifests.go | 16 +- vendor/github.com/slack-go/slack/messages.go | 15 + vendor/github.com/slack-go/slack/metadata.go | 35 +- vendor/github.com/slack-go/slack/migration.go | 40 + vendor/github.com/slack-go/slack/misc.go | 314 +++++-- vendor/github.com/slack-go/slack/mise.toml | 4 + vendor/github.com/slack-go/slack/oauth.go | 188 +++- vendor/github.com/slack-go/slack/reactions.go | 83 +- .../github.com/slack-go/slack/remotefiles.go | 23 +- vendor/github.com/slack-go/slack/retry.go | 307 +++++++ vendor/github.com/slack-go/slack/rtm.go | 6 + vendor/github.com/slack-go/slack/search.go | 10 +- vendor/github.com/slack-go/slack/security.go | 8 +- vendor/github.com/slack-go/slack/slack.go | 132 ++- .../slack/slackevents/action_events.go | 7 + .../slack/slackevents/inner_events.go | 593 ++++++++----- .../slack-go/slack/slackevents/parsers.go | 31 +- .../github.com/slack-go/slack/socket_mode.go | 13 +- vendor/github.com/slack-go/slack/stars.go | 60 +- vendor/github.com/slack-go/slack/team.go | 40 +- .../github.com/slack-go/slack/usergroups.go | 291 ++++++- vendor/github.com/slack-go/slack/users.go | 218 +++-- vendor/github.com/slack-go/slack/views.go | 92 +- vendor/github.com/slack-go/slack/webhooks.go | 4 +- .../slack-go/slack/websocket_groups.go | 3 - .../slack-go/slack/websocket_managed_conn.go | 15 +- .../slack-go/slack/websocket_misc.go | 142 ++- .../slack-go/slack/workflow_step.go | 101 --- .../slack-go/slack/workflow_step_execute.go | 85 -- .../slack-go/slack/workflows_featured.go | 143 ++++ .../slack-go/slack/workflows_triggers.go | 177 ++++ vendor/modules.txt | 4 +- 105 files changed, 7333 insertions(+), 1340 deletions(-) create mode 100644 vendor/github.com/slack-go/slack/CONTRIBUTING.md delete mode 100644 vendor/github.com/slack-go/slack/TODO.txt create mode 100644 vendor/github.com/slack-go/slack/admin_conversations.go create mode 100644 vendor/github.com/slack-go/slack/admin_conversations_ekm.go create mode 100644 vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go create mode 100644 vendor/github.com/slack-go/slack/admin_roles.go create mode 100644 vendor/github.com/slack-go/slack/admin_teams.go create mode 100644 vendor/github.com/slack-go/slack/assistant.go create mode 100644 vendor/github.com/slack-go/slack/block_alert.go create mode 100644 vendor/github.com/slack-go/slack/block_card.go create mode 100644 vendor/github.com/slack-go/slack/block_carousel.go create mode 100644 vendor/github.com/slack-go/slack/block_context_actions.go create mode 100644 vendor/github.com/slack-go/slack/block_json.go create mode 100644 vendor/github.com/slack-go/slack/block_markdown.go create mode 100644 vendor/github.com/slack-go/slack/block_plan.go create mode 100644 vendor/github.com/slack-go/slack/block_table.go create mode 100644 vendor/github.com/slack-go/slack/block_task_card.go create mode 100644 vendor/github.com/slack-go/slack/chat_stream_chunks.go create mode 100644 vendor/github.com/slack-go/slack/entity.go create mode 100644 vendor/github.com/slack-go/slack/huddle.go delete mode 100644 vendor/github.com/slack-go/slack/im.go create mode 100644 vendor/github.com/slack-go/slack/migration.go create mode 100644 vendor/github.com/slack-go/slack/mise.toml create mode 100644 vendor/github.com/slack-go/slack/retry.go delete mode 100644 vendor/github.com/slack-go/slack/workflow_step.go delete mode 100644 vendor/github.com/slack-go/slack/workflow_step_execute.go create mode 100644 vendor/github.com/slack-go/slack/workflows_featured.go create mode 100644 vendor/github.com/slack-go/slack/workflows_triggers.go diff --git a/cmd/ci-chat-bot/slack.go b/cmd/ci-chat-bot/slack.go index 214ecdd27..2766e0f7d 100644 --- a/cmd/ci-chat-bot/slack.go +++ b/cmd/ci-chat-bot/slack.go @@ -61,8 +61,8 @@ func Start(bot *slack.Bot, jiraclient *jiraClient.Client, jobManager manager.Job // handle the root to allow for a simple uptime probe mux.Handle("/", handler(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusOK) }))) mux.Handle("/readyz", handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))) // report ready once the server is up and responding - mux.Handle("/slack/events-endpoint", handler(handleEvent(bot.BotSigningSecret, eventrouter.ForEvents(slackclient, jobManager, bot.SupportedCommands(), issueFiler)))) - mux.Handle("/slack/interactive-endpoint", handler(handleInteraction(bot.BotSigningSecret, interactionrouter.ForModals(slackclient, jobManager, httpclient)))) + mux.Handle("/slack/events-endpoint", handler(handleEvent(bot.BotSigningSecret, eventrouter.ForEvents(slackclient, jobManager, bot.SupportedCommands(), issueFiler, bot.BotToken)))) + mux.Handle("/slack/interactive-endpoint", handler(handleInteraction(bot.BotSigningSecret, interactionrouter.ForModals(slackclient, jobManager, httpclient, bot.BotToken)))) server := &http.Server{Addr: ":" + strconv.Itoa(bot.Port), Handler: mux, ReadHeaderTimeout: 10 * time.Second} health.ServeReady(func() bool { resp, err := http.DefaultClient.Get("http://127.0.0.1:" + strconv.Itoa(bot.Port) + "/readyz") @@ -132,6 +132,17 @@ func handleInteraction(signingSecret string, handler interactionhandler.Handler) writer.WriteHeader(http.StatusInternalServerError) return } + // Extract workflow_step_edit_id from raw JSON for deprecated Steps from Apps. + // The WorkflowStep field was removed from InteractionCallback in slack-go v0.23. + var wsData struct { + WorkflowStep struct { + WorkflowStepEditID string `json:"workflow_step_edit_id"` + } `json:"workflow_step"` + } + if err := json.Unmarshal([]byte(payload), &wsData); err == nil && wsData.WorkflowStep.WorkflowStepEditID != "" { + callback.Value = wsData.WorkflowStep.WorkflowStepEditID + } + logger.WithField("interaction", callback).Trace("Read an interaction payload.") logger = logger.WithFields(fieldsFor(&callback)) response, err := handler.Handle(&callback, logger) diff --git a/go.mod b/go.mod index 73e1e3a10..879076c69 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/openshift/oc v0.0.0-alpha.0.0.20230410203846-b68ac7c39057 github.com/operator-framework/operator-registry v1.51.0 github.com/sirupsen/logrus v1.9.4 - github.com/slack-go/slack v0.15.0 + github.com/slack-go/slack v0.23.1 github.com/spf13/afero v1.15.0 github.com/spf13/pflag v1.0.10 go.podman.io/image/v5 v5.40.0 diff --git a/go.sum b/go.sum index c1fe9a8b8..cfc9b8ddc 100644 --- a/go.sum +++ b/go.sum @@ -481,7 +481,6 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= @@ -566,7 +565,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -630,7 +628,6 @@ github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkM github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= @@ -1033,8 +1030,8 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= -github.com/slack-go/slack v0.15.0 h1:LE2lj2y9vqqiOf+qIIy0GvEoxgF1N5yLGZffmEZykt0= -github.com/slack-go/slack v0.15.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= +github.com/slack-go/slack v0.23.1 h1:ZS5B96wxxYQRwvJ3/vJFtqtUZi3tXhsZCyT44Nv7M80= +github.com/slack-go/slack v0.23.1/go.mod h1:H0yR/YBuRJ39RkE+JpV/d/oEsbanzTRowR82bCN0cEs= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= diff --git a/pkg/slack/actions_request_test.go b/pkg/slack/actions_request_test.go index 8ff132b57..a10957cc6 100644 --- a/pkg/slack/actions_request_test.go +++ b/pkg/slack/actions_request_test.go @@ -19,8 +19,8 @@ import ( // mockSlackClient is a mock implementation of the Slack client for testing type mockSlackClient struct { - getUserInfoFunc func(userID string) (*slack.User, error) - uploadFileV2Func func(params slack.UploadFileV2Parameters) (*slack.FileSummary, error) + getUserInfoFunc func(userID string) (*slack.User, error) + uploadFileFunc func(params slack.UploadFileParameters) (*slack.FileSummary, error) } func (m *mockSlackClient) GetUserInfo(userID string) (*slack.User, error) { @@ -30,11 +30,11 @@ func (m *mockSlackClient) GetUserInfo(userID string) (*slack.User, error) { return nil, fmt.Errorf("mock GetUserInfo not implemented") } -func (m *mockSlackClient) UploadFileV2(params slack.UploadFileV2Parameters) (*slack.FileSummary, error) { - if m.uploadFileV2Func != nil { - return m.uploadFileV2Func(params) +func (m *mockSlackClient) UploadFile(params slack.UploadFileParameters) (*slack.FileSummary, error) { + if m.uploadFileFunc != nil { + return m.uploadFileFunc(params) } - return nil, fmt.Errorf("mock UploadFileV2 not implemented") + return nil, fmt.Errorf("mock UploadFile not implemented") } func (m *mockSlackClient) PostMessage(channelID string, options ...slack.MsgOption) (string, string, error) { @@ -303,7 +303,7 @@ func TestRequest(t *testing.T) { }, }, nil }, - uploadFileV2Func: func(params slack.UploadFileV2Parameters) (*slack.FileSummary, error) { + uploadFileFunc: func(params slack.UploadFileParameters) (*slack.FileSummary, error) { if tc.uploadFileError != nil { return nil, tc.uploadFileError } diff --git a/pkg/slack/events/router/router.go b/pkg/slack/events/router/router.go index b4d8041c8..5e4d956af 100644 --- a/pkg/slack/events/router/router.go +++ b/pkg/slack/events/router/router.go @@ -15,11 +15,11 @@ import ( // ForEvents returns a Handler that appropriately routes // event callbacks for the handlers we know about -func ForEvents(client *slack.Client, manager manager.JobManager, botCommands []slackCommandParser.BotCommand, filer jira.IssueFiler) events.Handler { +func ForEvents(client *slack.Client, manager manager.JobManager, botCommands []slackCommandParser.BotCommand, filer jira.IssueFiler, botToken string) events.Handler { return events.MultiHandler( messages.Handle(client, manager, botCommands), mention.Handler(client), - workflowSubmissionEvents.Handler(client, filer), + workflowSubmissionEvents.Handler(botToken, filer), apphome.Handler(client, manager), ) } diff --git a/pkg/slack/events/workflowSubmissionEvents/types.go b/pkg/slack/events/workflowSubmissionEvents/types.go index 6a6394b37..3bb0d1f0a 100644 --- a/pkg/slack/events/workflowSubmissionEvents/types.go +++ b/pkg/slack/events/workflowSubmissionEvents/types.go @@ -1,13 +1,16 @@ package workflowSubmissionEvents import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" "text/template" - - "github.com/slack-go/slack" ) type workflowSubmit interface { - WorkflowStepCompleted(workflowStepExecuteID string, options ...slack.WorkflowStepCompletedRequestOption) error + WorkflowStepCompleted(workflowStepExecuteID string, outputs map[string]string) error WorkflowStepFailed(workflowStepExecuteID string, errorMessage string) error } @@ -20,3 +23,125 @@ type JiraIssueParameters struct { Template *template.Template Fields []string } + +// workflowStepExecuteEvent mirrors the deprecated slackevents.WorkflowStepExecuteEvent +// which was removed from slack-go/slack v0.23.0+. +type workflowStepExecuteEvent struct { + Type string `json:"type"` + CallbackID string `json:"callback_id"` + WorkflowStep eventWorkflowStep `json:"workflow_step"` + EventTimestamp string `json:"event_ts"` +} + +type eventWorkflowStep struct { + WorkflowStepExecuteID string `json:"workflow_step_execute_id"` + WorkflowID string `json:"workflow_id"` + WorkflowInstanceID string `json:"workflow_instance_id"` + StepID string `json:"step_id"` + Inputs *WorkflowStepInputs `json:"inputs,omitempty"` + Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` +} + +type WorkflowStepInputElement struct { + Value string `json:"value"` + SkipVariableReplacement bool `json:"skip_variable_replacement"` +} + +type WorkflowStepInputs map[string]WorkflowStepInputElement + +type WorkflowStepOutput struct { + Name string `json:"name"` + Type string `json:"type"` + Label string `json:"label"` +} + +// SlackWorkflowClient wraps a tokenized HTTP client to call the deprecated +// Slack workflow step API methods (workflows.stepCompleted, workflows.stepFailed, +// workflows.updateStep). +type SlackWorkflowClient struct { + token string + endpoint string +} + +func NewSlackWorkflowClient(token string) *SlackWorkflowClient { + return &SlackWorkflowClient{ + token: token, + endpoint: "https://slack.com/api/", + } +} + +type workflowUpdateStepRequest struct { + WorkflowStepEditID string `json:"workflow_step_edit_id"` + Inputs *WorkflowStepInputs `json:"inputs,omitempty"` + Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` +} + +type workflowStepCompletedRequest struct { + WorkflowStepExecuteID string `json:"workflow_step_execute_id"` + Outputs map[string]string `json:"outputs,omitempty"` +} + +type workflowStepFailedRequest struct { + WorkflowStepExecuteID string `json:"workflow_step_execute_id"` + Error struct { + Message string `json:"message"` + } `json:"error"` +} + +type slackResponse struct { + OK bool `json:"ok"` + Error string `json:"error,omitempty"` +} + +func (c *SlackWorkflowClient) WorkflowStepCompleted(workflowStepExecuteID string, outputs map[string]string) error { + r := &workflowStepCompletedRequest{ + WorkflowStepExecuteID: workflowStepExecuteID, + Outputs: outputs, + } + return c.postJSON("workflows.stepCompleted", r) +} + +func (c *SlackWorkflowClient) WorkflowStepFailed(workflowStepExecuteID string, errorMessage string) error { + r := &workflowStepFailedRequest{ + WorkflowStepExecuteID: workflowStepExecuteID, + } + r.Error.Message = errorMessage + return c.postJSON("workflows.stepFailed", r) +} + +func (c *SlackWorkflowClient) SaveWorkflowStepConfiguration(workflowStepEditID string, inputs *WorkflowStepInputs, outputs *[]WorkflowStepOutput) error { + r := &workflowUpdateStepRequest{ + WorkflowStepEditID: workflowStepEditID, + Inputs: inputs, + Outputs: outputs, + } + return c.postJSON("workflows.updateStep", r) +} + +func (c *SlackWorkflowClient) postJSON(method string, payload interface{}) error { + body, err := json.Marshal(payload) + if err != nil { + return err + } + req, err := http.NewRequestWithContext(context.Background(), "POST", c.endpoint+method, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", "Bearer "+c.token) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + var sr slackResponse + if err := json.NewDecoder(resp.Body).Decode(&sr); err != nil { + return err + } + if !sr.OK { + return fmt.Errorf("slack API %s: %s", method, sr.Error) + } + return nil +} diff --git a/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go b/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go index 7169b0e3d..1dfd3c4ab 100644 --- a/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go +++ b/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go @@ -2,13 +2,13 @@ package workflowSubmissionEvents import ( "bytes" + "encoding/json" "fmt" jiraClient "github.com/andygrunwald/go-jira" "github.com/openshift/ci-chat-bot/pkg/jira" "github.com/openshift/ci-chat-bot/pkg/slack/events" "github.com/sirupsen/logrus" - "github.com/slack-go/slack" "github.com/slack-go/slack/slackevents" ) @@ -23,20 +23,28 @@ const ( UserDetails = "user_details" ) -func Handler(client workflowSubmit, filer jira.IssueFiler) events.PartialHandler { +func Handler(token string, filer jira.IssueFiler) events.PartialHandler { + wc := NewSlackWorkflowClient(token) return events.PartialHandlerFunc("workflow-execution-event", func(callback *slackevents.EventsAPIEvent, logger *logrus.Entry) (handled bool, err error) { if callback.Type != slackevents.CallbackEvent { return false, nil } - event, ok := callback.InnerEvent.Data.(*slackevents.WorkflowStepExecuteEvent) - if !ok { + raw, err := json.Marshal(callback.InnerEvent.Data) + if err != nil { + return false, nil + } + var event workflowStepExecuteEvent + if err := json.Unmarshal(raw, &event); err != nil { + return false, nil + } + if event.Type != "workflow_step_execute" { return false, nil } switch event.CallbackID { case "jira_ticket": - err := handleJiraStep(client, event, filer) + err := handleJiraStep(wc, &event, filer) if err != nil { - error := client.WorkflowStepFailed(event.WorkflowStep.WorkflowStepExecuteID, err.Error()) + error := wc.WorkflowStepFailed(event.WorkflowStep.WorkflowStepExecuteID, err.Error()) if error != nil { return false, error } @@ -47,7 +55,7 @@ func Handler(client workflowSubmit, filer jira.IssueFiler) events.PartialHandler }) } -func checkTicketType(event *slackevents.WorkflowStepExecuteEvent) (ticketType string, supported bool) { +func checkTicketType(event *workflowStepExecuteEvent) (ticketType string, supported bool) { for key, output := range *event.WorkflowStep.Inputs { if key == "ticket_type" { switch output.Value { @@ -65,7 +73,7 @@ func checkTicketType(event *slackevents.WorkflowStepExecuteEvent) (ticketType st return "unsupported_ticket_type", false } -func handleJiraStep(client workflowSubmit, event *slackevents.WorkflowStepExecuteEvent, filer jira.IssueFiler) error { +func handleJiraStep(client workflowSubmit, event *workflowStepExecuteEvent, filer jira.IssueFiler) error { ticketType, isSupported := checkTicketType(event) if !isSupported { @@ -100,15 +108,14 @@ func handleJiraStep(client workflowSubmit, event *slackevents.WorkflowStepExecut outgoingOutputs[incomingOutputs.Name] = fmt.Sprintf("https://issues.redhat.com/browse/%s", issue.Key) } } - options := slack.WorkflowStepCompletedRequestOptionOutput(outgoingOutputs) - err = client.WorkflowStepCompleted(event.WorkflowStep.WorkflowStepExecuteID, options) + err = client.WorkflowStepCompleted(event.WorkflowStep.WorkflowStepExecuteID, outgoingOutputs) if err != nil { return err } return nil } -func fileTicket(event *slackevents.WorkflowStepExecuteEvent, parameters JiraIssueParameters, filer jira.IssueFiler) (*jiraClient.Issue, error) { +func fileTicket(event *workflowStepExecuteEvent, parameters JiraIssueParameters, filer jira.IssueFiler) (*jiraClient.Issue, error) { title := "not_defined" reporter := "not_defined" data := make(map[string]string) diff --git a/pkg/slack/interactions/router/router.go b/pkg/slack/interactions/router/router.go index 4d745271c..5405e1aed 100644 --- a/pkg/slack/interactions/router/router.go +++ b/pkg/slack/interactions/router/router.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/openshift/ci-chat-bot/pkg/manager" + "github.com/openshift/ci-chat-bot/pkg/slack/events/workflowSubmissionEvents" "github.com/openshift/ci-chat-bot/pkg/slack/interactions" "github.com/openshift/ci-chat-bot/pkg/slack/modals" "github.com/openshift/ci-chat-bot/pkg/slack/modals/auth" @@ -24,9 +25,12 @@ import ( // ForModals returns a Handler that appropriately routes // interaction callbacks for the modals we know about -func ForModals(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { +func ForModals(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client, botToken string) interactions.Handler { router := &modalRouter{ - slackClient: client, + slackClient: &combinedSlackClient{ + Client: client, + SlackWorkflowClient: workflowSubmissionEvents.NewSlackWorkflowClient(botToken), + }, viewsByID: map[modals.Identifier]slack.ModalViewRequest{}, handlersByIDAndType: map[modals.Identifier]map[slack.InteractionType]interactions.Handler{}, } @@ -92,7 +96,7 @@ func (r *modalRouter) Handle(callback *slack.InteractionCallback, logger *logrus case slack.InteractionTypeViewSubmission: if metadataToIdentifier(callback.View.PrivateMetadata, logger) == string(slack.InteractionTypeWorkflowStepEdit) { input, output := stepsFromApp.StepFromAppSubmit(callback) - return nil, r.slackClient.SaveWorkflowStepConfiguration(callback.WorkflowStep.WorkflowStepEditID, &input, &output) + return nil, r.slackClient.SaveWorkflowStepConfiguration(callback.Value, &input, &output) } return r.delegate(callback, logger) default: @@ -119,7 +123,12 @@ func isMessageButtonPress(callback *slack.InteractionCallback) bool { type slackClient interface { OpenView(triggerID string, view slack.ModalViewRequest) (*slack.ViewResponse, error) - SaveWorkflowStepConfiguration(workflowStepEditID string, inputs *slack.WorkflowStepInputs, outputs *[]slack.WorkflowStepOutput) error + SaveWorkflowStepConfiguration(workflowStepEditID string, inputs *workflowSubmissionEvents.WorkflowStepInputs, outputs *[]workflowSubmissionEvents.WorkflowStepOutput) error +} + +type combinedSlackClient struct { + *slack.Client + *workflowSubmissionEvents.SlackWorkflowClient } // viewForShortcut reacts to the original shortcut action from the user diff --git a/pkg/slack/modals/common/simple_modals.go b/pkg/slack/modals/common/simple_modals.go index 5d374dfeb..fb6decd52 100644 --- a/pkg/slack/modals/common/simple_modals.go +++ b/pkg/slack/modals/common/simple_modals.go @@ -87,11 +87,9 @@ func AppendKubeconfigBlock(view *slack.ModalViewRequest, kubeconfig, headerText slack.NewDividerBlock(), slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, headerText, true, false)), slack.NewRichTextBlock("kubeconfig", &slack.RichTextPreformatted{ - RichTextSection: slack.RichTextSection{ - Type: slack.RTEPreformatted, - Elements: []slack.RichTextSectionElement{ - slack.NewRichTextSectionTextElement(kubeconfig, &slack.RichTextSectionTextStyle{Code: false}), - }, + Type: slack.RTEPreformatted, + Elements: []slack.RichTextSectionElement{ + slack.NewRichTextSectionTextElement(kubeconfig, &slack.RichTextSectionTextStyle{Code: false}), }, })) } diff --git a/pkg/slack/modals/common/version_views.go b/pkg/slack/modals/common/version_views.go index 0436a418e..d04bd0fce 100644 --- a/pkg/slack/modals/common/version_views.go +++ b/pkg/slack/modals/common/version_views.go @@ -107,7 +107,7 @@ func BuildFilterVersionView(config FilterVersionViewConfig) slackClient.ModalVie Text: &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: "Version Specifications", - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }, @@ -163,7 +163,7 @@ func BuildFilterVersionView(config FilterVersionViewConfig) slackClient.ModalVie &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: config.ContextMetadata, - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }}, @@ -241,7 +241,7 @@ func BuildSelectVersionView(config SelectVersionViewConfig) slackClient.ModalVie Text: &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: "Select a Version", - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }, @@ -270,7 +270,7 @@ func BuildSelectVersionView(config SelectVersionViewConfig) slackClient.ModalVie &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: config.ContextMetadata, - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }}, @@ -344,7 +344,7 @@ func BuildSelectMinorMajorView(config ReleaseViewConfig) slackClient.ModalViewRe Text: &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: "There are too many results from the selected Stream. Select a Major.Minor as well", - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }, @@ -373,7 +373,7 @@ func BuildSelectMinorMajorView(config ReleaseViewConfig) slackClient.ModalViewRe &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: config.ContextMetadata, - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }}, @@ -404,7 +404,7 @@ func BuildPRInputView(config BaseViewConfig) slackClient.ModalViewRequest { Text: &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: "Enter A PR", - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }, @@ -428,7 +428,7 @@ func BuildPRInputView(config BaseViewConfig) slackClient.ModalViewRequest { &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: config.ContextMetadata, - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }}, @@ -496,7 +496,7 @@ func BuildSelectModeView(config SelectModeViewConfig) slackClient.ModalViewReque Text: &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: "Do you want to launch from a PR?", - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }, @@ -522,7 +522,7 @@ func BuildSelectModeView(config SelectModeViewConfig) slackClient.ModalViewReque &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: config.ContextMetadata, - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }}, diff --git a/pkg/slack/modals/launch/views.go b/pkg/slack/modals/launch/views.go index 71dabbcab..9440b99b3 100644 --- a/pkg/slack/modals/launch/views.go +++ b/pkg/slack/modals/launch/views.go @@ -46,7 +46,7 @@ func FirstStepViewWithData(data modals.CallbackData) slackClient.ModalViewReques Text: &slackClient.TextBlockObject{ Type: "plain_text", Text: "Select the Launch Platform and Architecture", - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }, @@ -139,7 +139,7 @@ func ThirdStepView(callback *slackClient.InteractionCallback, jobmanager manager &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: context, - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }}, diff --git a/pkg/slack/modals/list/views.go b/pkg/slack/modals/list/views.go index 4522fc2d6..0f87ab3e9 100644 --- a/pkg/slack/modals/list/views.go +++ b/pkg/slack/modals/list/views.go @@ -20,7 +20,7 @@ func View() slackClient.ModalViewRequest { Text: &slackClient.TextBlockObject{ Type: "plain_text", Text: "See who is hogging all the clusters", - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }, diff --git a/pkg/slack/modals/mce/create/views.go b/pkg/slack/modals/mce/create/views.go index 525b4f52a..3dc06be3e 100644 --- a/pkg/slack/modals/mce/create/views.go +++ b/pkg/slack/modals/mce/create/views.go @@ -51,7 +51,7 @@ func FirstStepViewWithData(data modals.CallbackData) slackClient.ModalViewReques Text: &slackClient.TextBlockObject{ Type: "plain_text", Text: "Select the Launch Platform and Duration", - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }, @@ -117,7 +117,7 @@ func ThirdStepView(callback *slackClient.InteractionCallback, jobmanager manager &slackClient.TextBlockObject{ Type: slackClient.PlainTextType, Text: context, - Emoji: false, + Emoji: new(bool), Verbatim: false, }, }}, diff --git a/pkg/slack/modals/stepsFromApp/jira_step.go b/pkg/slack/modals/stepsFromApp/jira_step.go index 96f9c0dc9..71cec6430 100644 --- a/pkg/slack/modals/stepsFromApp/jira_step.go +++ b/pkg/slack/modals/stepsFromApp/jira_step.go @@ -16,7 +16,7 @@ const ( func WorkflowStepEditView(callback *slack.InteractionCallback) slack.ModalViewRequest { return slack.ModalViewRequest{ - Type: slack.VTWorkflowStep, + Type: "workflow_step", PrivateMetadata: string(Identifier), CallbackID: callback.CallbackID, Blocks: slack.Blocks{BlockSet: []slack.Block{ diff --git a/pkg/slack/modals/stepsFromApp/workflow_submit.go b/pkg/slack/modals/stepsFromApp/workflow_submit.go index ccb3e42f3..01bcd38c9 100644 --- a/pkg/slack/modals/stepsFromApp/workflow_submit.go +++ b/pkg/slack/modals/stepsFromApp/workflow_submit.go @@ -1,12 +1,13 @@ package stepsFromApp import ( + "github.com/openshift/ci-chat-bot/pkg/slack/events/workflowSubmissionEvents" "github.com/slack-go/slack" ) -func StepFromAppSubmit(callback *slack.InteractionCallback) (slack.WorkflowStepInputs, []slack.WorkflowStepOutput) { - inputMap := make(slack.WorkflowStepInputs) - outputMap := []slack.WorkflowStepOutput{ +func StepFromAppSubmit(callback *slack.InteractionCallback) (workflowSubmissionEvents.WorkflowStepInputs, []workflowSubmissionEvents.WorkflowStepOutput) { + inputMap := make(workflowSubmissionEvents.WorkflowStepInputs) + outputMap := []workflowSubmissionEvents.WorkflowStepOutput{ { Name: "issue.key", Type: "text", @@ -24,7 +25,7 @@ func StepFromAppSubmit(callback *slack.InteractionCallback) (slack.WorkflowStepI }, } for key, text := range callback.View.State.Values { - var input slack.WorkflowStepInputElement + var input workflowSubmissionEvents.WorkflowStepInputElement for _, subInput := range text { // we expect a single value here. If the array has multiple arrays, the last element proceeds. input.Value = subInput.Value diff --git a/pkg/slack/parser/types.go b/pkg/slack/parser/types.go index bc0bfa517..ac028838c 100644 --- a/pkg/slack/parser/types.go +++ b/pkg/slack/parser/types.go @@ -18,8 +18,8 @@ type SlackClient interface { // PostMessage posts a message to a Slack channel PostMessage(channelID string, options ...slack.MsgOption) (string, string, error) - // UploadFileV2 uploads a file to Slack - UploadFileV2(params slack.UploadFileV2Parameters) (*slack.FileSummary, error) + // UploadFile uploads a file to Slack + UploadFile(params slack.UploadFileParameters) (*slack.FileSummary, error) } type Command struct { diff --git a/pkg/slack/slack.go b/pkg/slack/slack.go index df4e5b306..124cc5f8c 100644 --- a/pkg/slack/slack.go +++ b/pkg/slack/slack.go @@ -514,14 +514,14 @@ func NotifyJob(client parser.SlackClient, job *manager.Job, postMessage bool) (s } func SendKubeConfig(client parser.SlackClient, channel, contents, comment, identifier string) string { - params := slack.UploadFileV2Parameters{ + params := slack.UploadFileParameters{ Content: contents, FileSize: len(contents), Channel: channel, Filename: fmt.Sprintf("cluster-bot-%s.kubeconfig", identifier), InitialComment: comment, } - summary, err := client.UploadFileV2(params) + summary, err := client.UploadFile(params) if err != nil { klog.Errorf("error: unable to send attachment with message: %v", err) return "" @@ -532,14 +532,14 @@ func SendKubeConfig(client parser.SlackClient, channel, contents, comment, ident func SendGCPServiceAccountKey(client parser.SlackClient, channel, keyJSON, email string) error { sanitized := strings.ReplaceAll(strings.ReplaceAll(email, "@", "-"), ".", "-") - params := slack.UploadFileV2Parameters{ + params := slack.UploadFileParameters{ Content: keyJSON, FileSize: len(keyJSON), Channel: channel, Filename: fmt.Sprintf("gcp-access-%s.json", sanitized), InitialComment: "⚠️ Service Account Key - Keep secure and do not share!", } - _, err := client.UploadFileV2(params) + _, err := client.UploadFile(params) if err != nil { klog.Errorf("error: unable to upload GCP service account key: %v", err) return err @@ -749,14 +749,14 @@ func NotifyMce(client parser.SlackClient, cluster *clusterv1.ManagedCluster, clu } return message, "" } - params := slack.UploadFileV2Parameters{ + params := slack.UploadFileParameters{ Content: *clusterProvision.Spec.InstallLog, FileSize: len(*clusterProvision.Spec.InstallLog), Channel: channel, Filename: fmt.Sprintf("%s-error.txt", cluster.Name), InitialComment: message, } - _, err := client.UploadFileV2(params) + _, err := client.UploadFile(params) if err != nil { klog.Errorf("error: unable to send attachment with message: %v", err) return message, "" diff --git a/vendor/github.com/slack-go/slack/.gitignore b/vendor/github.com/slack-go/slack/.gitignore index 027f4de5f..9b3903cd2 100644 --- a/vendor/github.com/slack-go/slack/.gitignore +++ b/vendor/github.com/slack-go/slack/.gitignore @@ -1,4 +1,5 @@ *.test *~ .idea/ -/vendor/ \ No newline at end of file +/vendor/ +.env* diff --git a/vendor/github.com/slack-go/slack/.golangci.yml b/vendor/github.com/slack-go/slack/.golangci.yml index c16f5389a..0ef0f87ec 100644 --- a/vendor/github.com/slack-go/slack/.golangci.yml +++ b/vendor/github.com/slack-go/slack/.golangci.yml @@ -1,14 +1,35 @@ +version: "2" run: timeout: 6m issues-exit-code: 1 linters: - disable-all: true + default: none enable: - - goimports + - gocritic - govet - - interfacer - misspell - - structcheck - unconvert + - unused + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ issues: new: true +formatters: + enable: + - goimports + exclusions: + generated: lax + warn-unused: true + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/vendor/github.com/slack-go/slack/CHANGELOG.md b/vendor/github.com/slack-go/slack/CHANGELOG.md index 32da687bb..2f9f821e2 100644 --- a/vendor/github.com/slack-go/slack/CHANGELOG.md +++ b/vendor/github.com/slack-go/slack/CHANGELOG.md @@ -1,103 +1,580 @@ -### v0.7.0 - October 2, 2020 -full differences can be viewed using `git log --oneline --decorate --color v0.6.6..v0.7.0` -Thank you for many contributions! - -#### Breaking Changes -- Add ScheduledMessage type ([#753]) -- Add description field to option block object ([#783]) -- Fix wrong conditional branch ([#782]) - - The behavior of the user's application may change.(The current behavior is incorrect) - -#### Highlights -- example: fix to start up a server ([#773]) -- example: Add explanation how the message could be sent in a proper way ([#787]) -- example: fix typo in error log ([#779]) -- refactor: Make GetConversationsParameters.ExcludeArchived optional ([#791]) -- refactor: Unify variables to "config" ([#800]) -- refactor: Rename wrong file name ([#810]) -- feature: Add SetUserRealName for change user's realName([#755]) -- feature: Add response metadata to slack response ([#772]) -- feature: Add response metadata to slack response ([#778]) -- feature: Add select block element conversations filter field ([#790]) -- feature: Add Root field to MessageEvent to support thread_broadcast subtype ([#793]) -- feature: Add bot_profile to messages ([#794]) -- doc: Add logo to README ([#813]) -- doc: Update current project status and Add changelog for v0.7.0 ([#814]) - -[#753]: https://github.com/slack-go/slack/pull/753 -[#755]: https://github.com/slack-go/slack/pull/755 -[#772]: https://github.com/slack-go/slack/pull/772 -[#773]: https://github.com/slack-go/slack/pull/773 -[#778]: https://github.com/slack-go/slack/pull/778 -[#779]: https://github.com/slack-go/slack/pull/779 -[#782]: https://github.com/slack-go/slack/pull/782 -[#783]: https://github.com/slack-go/slack/pull/783 -[#787]: https://github.com/slack-go/slack/pull/787 -[#790]: https://github.com/slack-go/slack/pull/790 -[#791]: https://github.com/slack-go/slack/pull/791 -[#793]: https://github.com/slack-go/slack/pull/793 -[#794]: https://github.com/slack-go/slack/pull/794 -[#800]: https://github.com/slack-go/slack/pull/800 -[#810]: https://github.com/slack-go/slack/pull/810 -[#813]: https://github.com/slack-go/slack/pull/813 -[#814]: https://github.com/slack-go/slack/pull/814 - -### v0.6.0 - August 31, 2019 -full differences can be viewed using `git log --oneline --decorate --color v0.5.0..v0.6.0` -thanks to everyone who has contributed since January! - - -#### Breaking Changes: -- Info struct has had fields removed related to deprecated functionality by slack. -- minor adjustments to some structs. -- some internal default values have changed, usually to be more inline with slack defaults or to correct inability to set a particular value. (Message Parse for example.) - -##### Highlights: -- new slacktest package easy mocking for slack client. use, enjoy, please submit PRs for improvements and default behaviours! shamelessly taken from the [slack-test repo](https://github.com/lusis/slack-test) thank you lusis for letting us use it and bring it into the slack repo. -- blocks, blocks, blocks. -- RTM ManagedConnection has undergone a significant cleanup. -in particular handles backoffs gracefully, removed many deadlocks, -and Disconnect is now much more responsive. - -### v0.5.0 - January 20, 2019 -full differences can be viewed using `git log --oneline --decorate --color v0.4.0..v0.5.0` -- Breaking changes: various old struct fields have been removed or updated to match slack's api. -- deadlock fix in RTM disconnect. - -### v0.4.0 - October 06, 2018 -full differences can be viewed using `git log --oneline --decorate --color v0.3.0..v0.4.0` -- Breaking Change: renamed ApplyMessageOption, to mark it as unsafe, -this means it may break without warning in the future. -- Breaking: Msg structure files field changed to an array. -- General: implementation for new security headers. -- RTM: deadlock fix between connect/disconnect. -- Events: various new fields added. -- Web: various fixes, new fields exposed, new methods added. -- Interactions: minor additions expect breaking changes in next release for dialogs/button clicks. -- Utils: new methods added. - -### v0.3.0 - July 30, 2018 -full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0` -- slack events initial support added. (still considered experimental and undergoing changes, stability not promised) -- vendored depedencies using dep, ensure using up to date tooling before filing issues. -- RTM has improved its ability to identify dead connections and reconnect automatically (worth calling out in case it has unintended side effects). -- bug fixes (various timestamp handling, error handling, RTM locking, etc). - -### v0.2.0 - Feb 10, 2018 - -Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against. - -Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0) - -### v0.1.0 - May 28, 2017 - -This is released before adding context support. -As the used context package is the one from Go 1.7 this will be the last -compatible with Go < 1.7. - -Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0) - -### v0.0.1 - Jul 26, 2015 - -If you just updated from master and it broke your implementation, please -check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1) +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.23.1] - 2026-05-10 + +### Fixed + +- `NewSecretsVerifier` now rejects empty signing secrets to avoid accepting forged request + signatures when applications are misconfigured. + +## [0.23.0] - 2026-04-22 + +### Added + +- **Block Kit: `CardBlock` and `CarouselBlock`** — Support for two of the new + agent-UI blocks announced in the + [April 16 Slack changelog](https://docs.slack.dev/changelog/2026/04/16/block-kit-new-blocks). + `CardBlock` is constructed via `NewCardBlock` with a functional-options + pattern and fluent `With*` builders (`WithTitle`, `WithSubtitle`, `WithBody`, + `WithIcon`, `WithHeroImage`, `WithActions`). `CarouselBlock` is constructed + via `NewCarouselBlock` with a variadic `*CardBlock` list plus `WithBlockID` + and `AddCard` helpers. Both blocks wire into `Blocks.UnmarshalJSON` for + round-trip fidelity, and reuse existing `ImageBlockElement` / + `ButtonBlockElement` / `BlockElements` types rather than introducing new + composition objects. +- **Block Kit: `AlertBlock`** — Support for the third of the new agent-UI + blocks from the + [April 16 Slack changelog](https://docs.slack.dev/changelog/2026/04/16/block-kit-new-blocks). + `AlertBlock` is constructed via `NewAlertBlock` with a `*TextBlockObject` + body and a functional-options pattern. Severity is set via + `AlertBlockOptionLevel` (`AlertLevelDefault`, `AlertLevelInfo`, + `AlertLevelWarning`, `AlertLevelError`, `AlertLevelSuccess`) and the block + ID via `AlertBlockOptionBlockID`. Wires into `Blocks.UnmarshalJSON` for + round-trip fidelity. Must be delivered via the streaming chunks API — + `chat.postMessage` rejects it as an unsupported block type. +- **Streaming-message chunks API** — `chat.startStream` / `chat.appendStream` / + `chat.stopStream` now accept a `chunks` parameter. Added `MsgOptionChunks` + along with a `StreamChunk` interface and four chunk types: + `MarkdownTextChunk`, `TaskUpdateChunk`, `PlanUpdateChunk`, and `BlocksChunk` + (each with a `New*Chunk` constructor). This is the supported transport for + streaming Block Kit content and the new agent-UI blocks in particular + (which `chat.postMessage` rejects as `Unsupported block type`). +- **`MsgOptionTaskDisplayMode`** — New option for `chat.startStream` controlling + whether task chunks render as a sequential timeline or a grouped plan. + Accepts `TaskDisplayModeTimeline` or `TaskDisplayModePlan`. +- Added `Username`, `IconURL`, and `IconEmoji` fields to + `AssistantThreadsSetStatusParameters`, forwarded by + `SetAssistantThreadsStatusContext`, matching the new optional parameters on + [`assistant.threads.setStatus`](https://docs.slack.dev/reference/methods/assistant.threads.setStatus) + for customising the status-update presentation. +- Exposed `SocketmodeHandler.DispatchEvent` (previously the unexported + `dispatcher`), enabling integration tests to exercise registered handlers + without a live WebSocket connection. The unexported `dispatcher` is kept as + a thin wrapper for backwards compatibility. Closes #1549. + +## [0.22.0] - 2026-04-12 + +### Added + +- Added missing parameters to `assistant.search.context` (`Sort`, `SortDir`, `Before`, + `After`, `Highlight`, `IncludeContextMessages`, `IncludeDeletedUsers`, + `IncludeMessageBlocks`, `IncludeArchivedChannels`, `DisableSemanticSearch`, `Modifiers`, + `TermClauses`) and new response types (`AssistantSearchContextFile`, + `AssistantSearchContextChannel`, `AssistantSearchContextMessageContext`) to match the + full Real-Time Search API surface. +- Added `Underline`, `Highlight`, `ClientHighlight`, and `Unlink` fields to + `RichTextSectionTextStyle`. Added `Style` field to `RichTextSectionUserGroupElement`. +- Added `BotOptional` and `UserOptional` fields to `OAuthScopes` for app manifests. +- Added PKCE support for OAuth: `OAuthOptionCodeVerifier` option for + `GetOAuthV2Response`, `GenerateCodeVerifier()` and `GenerateCodeChallenge()` + helper functions (RFC 7636). `client_secret` is now conditionally omitted when + empty in both `GetOAuthV2ResponseContext` and `RefreshOAuthV2TokenContext`. + +### Fixed + +- `ChannelTypes` and `ContentTypes` now send comma-separated values instead of repeated + form keys, matching the convention used by every other method in the library. +- In `socketmode` malformed JSON messages no longer force an unnecessary reconnect. + Instead the error is emitted and the connection continues as normal. + +## [0.21.1] - 2026-04-08 + +### Added + +- **`slackevents.ChannelType*` constants and `MessageEvent` helpers** — Added + `ChannelTypeChannel`, `ChannelTypeGroup`, `ChannelTypeIM`, `ChannelTypeMPIM` constants + and `IsChannel()`, `IsGroup()`, `IsIM()`, `IsMpIM()` methods on `MessageEvent` so + callers no longer need to compare raw strings. + +### Fixed + +- **Duplicate attachment/block serialization in `MsgOptionAttachments` / `MsgOptionBlocks`** — + Attachments and blocks were serialized twice: once into typed struct fields (for the JSON + response-URL path) and again into `url.Values` (for the form POST path). Serialization for + the form path now happens inside `formSender.BuildRequestContext`, so each sender owns its + own marshalling. This fixes the long-standing FIXME and eliminates redundant `json.Marshal` + calls in the option functions. ([#1547]) + + > [!NOTE] + > `UnsafeApplyMsgOptions` returns `config.values` directly. After this change, + > `attachments` and `blocks` keys are no longer present in those values since + > marshalling is deferred to send time. This function is documented as unsupported. + +## [0.21.0] - 2026-04-05 + +### Deprecated + +- **`slackevents.ParseActionEvent`** — Cannot parse `block_actions` payloads (returns + unmarshalling error). Use `slack.InteractionCallback` with `json.Unmarshal` instead, + or `slack.InteractionCallbackParse` for HTTP requests. `InteractionCallback` handles + all interaction types. ([#596]) +- **`slackevents.MessageAction`**, **`MessageActionEntity`**, **`MessageActionResponse`** — + Associated types that only support legacy `interactive_message` payloads. + +### Removed + +- **`IM` struct** — Removed the `IM` struct (and unused internal types `imChannel`, + `imResponseFull`). The `IsUserDeleted` field has been moved to `Conversation`, where it + is populated for IM-type conversations. Code using `IM` should switch to `Conversation`. + + > [!NOTE] + > In practice no user should be affected — `IM` was never returned by any public API + > method in this library, so there was no way to obtain one outside of manual construction. + +- **`Info.GetBotByID`, `GetUserByID`, `GetChannelByID`, `GetGroupByID`, `GetIMByID`** — + These methods were deprecated and returned `nil` unconditionally. They have been removed. + + > [!WARNING] + > **Breaking change.** If you are calling any of these methods, remove those calls — they + > were already no-ops. + +### Added + +- **`admin.teams.settings.*` API support** — `AdminTeamsSettingsInfo`, + `AdminTeamsSettingsSetDefaultChannels`, `AdminTeamsSettingsSetDescription`, + `AdminTeamsSettingsSetDiscoverability`, `AdminTeamsSettingsSetIcon`, and + `AdminTeamsSettingsSetName`. Includes `TeamDiscoverability` enum with `Open`, + `InviteOnly`, `Closed`, and `Unlisted` variants. ([#960]) +- **`OAuthOptionAPIURL` for package-level OAuth functions** — All package-level OAuth + functions (`GetOAuthV2Response`, `GetOpenIDConnectToken`, `RefreshOAuthV2Token`, etc.) + now accept variadic `OAuthOption` arguments. Use `OAuthOptionAPIURL(url)` to override + the default Slack API URL, enabling integration tests against a local HTTP server. + Existing callers are unaffected. ([#744]) +- **`GetOpenIDConnectUserInfo` / `GetOpenIDConnectUserInfoContext`** — Returns identity + information about the user associated with the token via `openid.connect.userInfo`. + Complements the existing `GetOpenIDConnectToken` method. ([#967]) +- **HTTP response headers** — Slack API response headers (e.g. `X-OAuth-Scopes`, + `X-Accepted-OAuth-Scopes`, `X-Ratelimit-*`) are now accessible. `AuthTestResponse` + exposes a `Header` field directly. For all other methods, use + `OptionOnResponseHeaders(func(method string, headers http.Header))` to register a + callback that fires after every API call. ([#1076]) +- **`DNDOptionTeamID`** — `GetDNDInfo` and `GetDNDTeamInfo` now accept optional + `ParamOption` arguments. Use `DNDOptionTeamID("T...")` to pass `team_id`, which is + required after workspace migration (Slack returns `missing_argument` without it). + ([#1157]) +- **`UpdateUserGroupMembersList` / `UpdateUserGroupMembersListContext`** — Convenience + wrappers around `UpdateUserGroupMembers` that accept `[]string` instead of a + comma-separated string, enabling clean chaining with `GetUserGroupMembers`. ([#1172]) +- **`SetUserProfile` / `SetUserProfileContext`** — Set multiple user profile fields in a + single API call by passing a `*UserProfile` struct to `users.profile.set`. Complements + the existing single-field methods (`SetUserRealName`, `SetUserCustomStatus`, etc.). + ([#1158]) +- **API warning callbacks** — Slack API responses may include a `warnings` field with + deprecation notices or usage hints. Use `OptionWarnings(func(warnings []string))` to + register a callback that receives these warnings. ([#1540]) +- **RTM support for `user_status_changed`, `user_huddle_changed`, `user_profile_changed` + events** — these events are now mapped in `EventMapping` with dedicated structs + (`UserStatusChangedEvent`, `UserHuddleChangedEvent`, `UserProfileChangedEvent`). + Previously they triggered `UnmarshallingErrorEvent`. ([#1541]) +- **RTM support for `sh_room_join`, `sh_room_leave`, `sh_room_update`, `channel_updated` + events** — Slack Call/Huddle room events and channel property updates are now mapped with + dedicated structs (`SHRoomJoinEvent`, `SHRoomLeaveEvent`, `SHRoomUpdateEvent`, + `ChannelUpdatedEvent`). ([#858]) +- **`CacheTS` and `EventTS` fields on `UserChangeEvent`** — these fields were sent by Slack + but silently dropped during unmarshalling. +- **`workflows.featured` API support** — add, list, remove, and set featured workflows on + channels via `WorkflowsFeaturedAdd`, `WorkflowsFeaturedList`, `WorkflowsFeaturedRemove`, + and `WorkflowsFeaturedSet` +- **`IsConnectorBot` and `IsWorkflowBot` in `User`** — boolean flags for connector and + workflow bot users +- **`GuestInvitedBy` in `UserProfile`** — user ID of whoever invited a guest user +- **`Blocks` field on `MessageEvent`** — block data from webhook payloads is now directly + accessible via `event.Blocks` instead of only through `event.Message.Blocks`. ([#1257]) +- **`Username` field on `User`** — Slack's interaction payloads (block_actions, shortcuts) + include a `username` field in the user object that was previously dropped during + unmarshalling. ([#1218]) +- **`Blocks`, `Attachments`, `Files`, `Upload` fields on `AppMentionEvent`** — these fields + are sent by Slack in `app_mention` event payloads but were silently dropped. ([#961]) +- **`HandleShortcut`, `HandleViewSubmission`, `HandleViewClosed` in socketmode handler** — + Level 3 handlers that dispatch `shortcut`/`message_action`, `view_submission`, and + `view_closed` interactions by `CallbackID`, matching the pattern of + `HandleInteractionBlockAction` and `HandleSlashCommand`. ([#1161]) +- **`BlockFromJSON` / `MustBlockFromJSON`** — Create blocks from raw JSON strings, enabling + direct use of output from Slack's [Block Kit Builder](https://app.slack.com/block-kit-builder) + or quick adoption of new block types before the library adds typed support. The original + JSON is preserved through marshalling. ([#1497]) + +### Documentation + +- **`ViewSubmissionResponse` constructors** — `NewClearViewSubmissionResponse`, + `NewUpdateViewSubmissionResponse`, `NewPushViewSubmissionResponse`, and + `NewErrorsViewSubmissionResponse` now have doc comments explaining the HTTP response + pattern (write JSON and return promptly) and the Socket Mode pattern (pass as Ack + payload). `NewErrorsViewSubmissionResponse` additionally documents that map keys must + be `BlockID`s of `InputBlock` elements. ([#726], [#1013]) +- **Socket Mode examples** — `examples/socketmode/` and `examples/socketmode_handler/` now + have doc comments explaining the two-token requirement: app-level token (`xapp-`) for the + WebSocket connection and bot token (`xoxb-`) for API calls. ([#941]) + +### Fixed + +- **`UnknownBlock` round-trip data loss** — Unrecognized block types (e.g. new Slack block + types not yet supported by this library) now preserve their full JSON through + unmarshal/marshal cycles. Previously only `type` and `block_id` were retained, silently + discarding all other fields. + +### Changed + +- Adjusted some `admin` errors that started with uppercase to be lowercase per go + conventions. + + > [!WARNING] + > **Breaking change.** If you are matching the error content in your code, this is a + > BREAKING CHANGE. +- **`WebhookMessage.UnfurlLinks` and `UnfurlMedia` are now `*bool`** — Previously these + were `bool` with `omitempty`, which meant `false` was silently stripped from the JSON + payload. Users could not explicitly disable link or media unfurling via webhooks. The + fields are now `*bool` so that `nil` (omit), `false`, and `true` all serialize correctly. + ([#1231]) + + > [!WARNING] + > **Breaking change.** Code that sets these fields directly must be updated: + > + > ```go + > // Before + > msg := slack.WebhookMessage{UnfurlLinks: true} + > + > // After — use a helper or a variable + > t := true + > msg := slack.WebhookMessage{UnfurlLinks: &t} + > ``` + > + > Leaving the fields unset (`nil`) preserves the previous default behavior — Slack's + > server-side defaults apply (`unfurl_links=false`, `unfurl_media=true`). + +- **`User.Has2FA` is now `*bool`** — When using a bot token, Slack's `users.list` API omits + `has_2fa` entirely. With a plain `bool`, this was indistinguishable from explicitly `false`. + Now `nil` means absent/unknown, `false` means explicitly disabled, `true` means enabled. + ([#1121]) + + > [!WARNING] + > **Breaking change.** Code that reads `Has2FA` must handle the pointer: + > + > ```go + > // Before + > if user.Has2FA { ... } + > + > // After + > if user.Has2FA != nil && *user.Has2FA { ... } + > ``` + +- **`ListReactions` now uses cursor-based pagination** — `ListReactionsParameters` replaces + `Count`/`Page` with `Cursor`/`Limit`, and `ListReactions`/`ListReactionsContext` now return + `([]ReactedItem, string, error)` where the string is the next cursor, instead of + `([]ReactedItem, *Paging, error)`. ([#825]) + + > [!WARNING] + > **Breaking change.** Both the parameters and return signature have changed: + > + > ```go + > // Before + > params := slack.NewListReactionsParameters() + > params.Count = 100 + > params.Page = 2 + > items, paging, err := api.ListReactions(params) + > + > // After + > params := slack.NewListReactionsParameters() + > params.Limit = 100 + > items, nextCursor, err := api.ListReactions(params) + > // Use nextCursor for the next page: params.Cursor = nextCursor + > ``` + +- **`ListStars`/`GetStarred` now use cursor-based pagination** — `StarsParameters` replaces + `Count`/`Page` with `Cursor`/`Limit` (and adds `TeamID`), and `ListStars`/`ListStarsContext`/ + `GetStarred`/`GetStarredContext` now return `string` (next cursor) instead of `*Paging`. + Slack's `stars.list` API no longer returns `paging` data — only `response_metadata.next_cursor`. + + > [!WARNING] + > **Breaking change.** Both the parameters and return signature have changed: + > + > ```go + > // Before + > params := slack.NewStarsParameters() + > params.Count = 100 + > params.Page = 2 + > items, paging, err := api.ListStars(params) + > + > // After + > params := slack.NewStarsParameters() + > params.Limit = 100 + > items, nextCursor, err := api.ListStars(params) + > // Use nextCursor for the next page: params.Cursor = nextCursor + > ``` + +- **`GetAccessLogs` now uses cursor-based pagination** — `AccessLogParameters` replaces + `Count`/`Page` with `Cursor`/`Limit` (and adds `Before`), and `GetAccessLogs`/ + `GetAccessLogsContext` now return `string` (next cursor) instead of `*Paging`. + Slack's `team.accessLogs` API warns `use_cursor_pagination_instead` when using the old + parameters. + + > [!WARNING] + > **Breaking change.** Both the parameters and return signature have changed: + > + > ```go + > // Before + > params := slack.NewAccessLogParameters() + > params.Count = 100 + > params.Page = 2 + > logins, paging, err := api.GetAccessLogs(params) + > + > // After + > params := slack.NewAccessLogParameters() + > params.Limit = 100 + > logins, nextCursor, err := api.GetAccessLogs(params) + > // Use nextCursor for the next page: params.Cursor = nextCursor + > ``` + +### Fixed + +- **Socket Mode: large Ack payloads no longer silently fail** — Two issues caused `Ack()` + payloads to be silently dropped by Slack. First, gorilla/websocket's default 4KB write + buffer fragmented messages into WebSocket continuation frames that Slack does not + reassemble. The library now uses a 32KB write buffer. Second, Slack silently drops + Socket Mode responses at or above 20KB — `Ack()`, `Send()`, and `SendCtx()` now return + an error when the serialized response reaches this limit. ([#1196]) + + > [!WARNING] + > **Breaking change.** `Ack()` and `Send()` now return `error`. Existing call sites that + > don't capture the return value continue to compile without changes. + +- **`MsgOptionBlocks()` with no arguments now sends `blocks=[]`** — Previously, calling + `MsgOptionBlocks()` with no arguments or a nil spread was a silent no-op, making it + impossible to clear blocks from a message via `chat.update`. The Slack API requires an + explicit `blocks=[]` to reliably remove blocks. ([#1214]) + + > [!WARNING] + > **Breaking change.** `MsgOptionBlocks()` with no arguments now sends `blocks=[]` instead + > of being a no-op. If you were relying on this to be a no-op, remove the option entirely: + > + > ```go + > // Before — this was a no-op, now it sends blocks=[] + > api.PostMessage(ch, slack.MsgOptionBlocks(), slack.MsgOptionText("text", false)) + > + > // After — omit MsgOptionBlocks entirely to not set blocks + > api.PostMessage(ch, slack.MsgOptionText("text", false)) + > ``` + +- **`WorkflowButtonBlockElement` missing from `UnmarshalJSON`** — `workflow_button` blocks + now unmarshal correctly through `BlockElements`, `InputBlock`, and `Accessory` paths. + Also adds missing `multi_*_select` and `file_input` cases to `BlockElements.UnmarshalJSON`, + and fixes `toBlockElement` for `RichTextInputElement` and `WorkflowButtonElement`. ([#1539]) +- **`NewBlockHeader` nil pointer dereference** — passing a nil text object no longer panics. ([#1236]) +- **`ValidateUniqueBlockID` rejects empty block IDs** — multiple input blocks with no + explicit `block_id` set (empty string) were incorrectly flagged as duplicates, causing + `OpenView` to fail. ([#1184]) + +## [0.20.0] - 2026-03-21 + +> [!WARNING] +> `trigger_id` and `workflow_id` are NOT in any documentation or in any of the official +libraries, so exercise caution if you use these. + +### Added + +- **`workflow_id` and `trigger_id` in `Message`** — It seems that some types of messages, + e.g: `bot_message`, can carry `trigger_id` and `workflow_id`. +- **`RichTextQuote.Border` field** — optional border toggle (matches the docs now) +- **`RichTextPreformatted.Language` field** — enables syntax highlighting for preformatted + blocks + +### Fixed + +- **Remove embedding of `RichTextSection`** — `RichTextQuote` and `RichTextPreformatted` + are now flattened as they should have always been. This is a breaking change for anyone + using these structs directly. + +## [0.19.0] - 2026-03-04 + +### Added + +- **Optional HTTP retry for Web API** — Retries are off by default. Enable with `OptionRetry(n)` for 429-only retries or `OptionRetryConfig(cfg)` for full control including 5xx and connection errors with exponential backoff. ([#1532]) +- **`task_card` and `plan` agent blocks** — New block types for task cards and plan agent blocks. ([#1536]) + +### Changed + +- CI: bumped `actions/stale` from 10.1.1 to 10.2.0. ([#1534]) +- Use `golangci-lint` in Makefile. ([#1533]) + +## [0.18.0] - 2026-02-21 + +### Added + +- **`focus_on_load` support for remaining block elements** — Static/external/users/conversations/channels select, multi-select variants, datepicker, timepicker, plain_text_input, checkboxes, radio_buttons, and number_input. ([#1519]) +- **`PlainText` and `PreviewPlainText` fields on `File`** — Email file objects now include the plain text body fields instead of silently discarding them. ([#1522]) +- **Missing fields on `User`, `UserProfile`, and `EnterpriseUser`** — `who_can_share_contact_card`, `always_active`, `pronouns`, `image_1024`, `is_custom_image`, `status_text_canonical`, `huddle_state`, `huddle_state_expiration_ts`, `start_date`, and `is_primary_owner`. ([#1526]) +- **Work Objects support** — Chat unfurl with Work Object metadata, entity details (flexpane), `entity_details_requested` event, and associated types (`WorkObjectMetadata`, `WorkObjectEntity`, `WorkObjectExternalRef`). ([#1529]) +- **`admin.roles.*` API methods** — `admin.roles.listAssignments`, `admin.roles.addAssignments`, and `admin.roles.removeAssignments`. ([#1520]) + +### Fixed + +- **`UserProfile.Skype` JSON tag** — Corrected typo from `"skyp"` to `"skype"`. ([#1524]) +- **`assistant.threads.setSuggestedPrompts` title parameter** — Title is now sent when non-empty. ([#1528]) + +### Changed + +- CI test matrix updated: dropped Go 1.24, added Go 1.26; bumped golangci-lint to v2.10.1. ([#1530]) + +## [0.18.0-rc2] - 2026-01-28 + +### Added + +- **Audit Logs example** - New example demonstrating how to use the Audit Logs API. ([#1144]) +- **Admin Conversations API support** - Comprehensive support for `admin.conversations.*` + methods including core operations (archive, unarchive, create, delete, rename, invite, + search, lookup, getTeams, convertToPrivate, convertToPublic, disconnectShared, setTeams), + bulk operations (bulkArchive, bulkDelete, bulkMove), preferences, retention management, + restrict access controls, and EKM channel info. ([#1329]) + +### Changed + +- **BREAKING**: Removed deprecated `UploadFile`, `UploadFileContext`, and + `FileUploadParameters`. The `files.upload` API was discontinued by Slack on November + 12, 2025. ([#1481]) +- **BREAKING**: Renamed `UploadFileV2` → `UploadFile`, `UploadFileV2Context` → + `UploadFileContext`, and `UploadFileV2Parameters` → `UploadFileParameters`. The "V2" + suffix is no longer needed now that the old API is removed. ([#1481]) + +### Fixed + +- **File upload error wrapping** - `UploadFile` now wraps errors with the step name + (`GetUploadURLExternal`, `UploadToURL`, or `CompleteUploadExternal`) so callers can + identify which of the three upload steps failed. ([#1491]) +- **Audit Logs API endpoint** - Fixed `GetAuditLogs` to use the correct endpoint + (`api.slack.com`) instead of the regular API endpoint (`slack.com/api`). The Audit + Logs API requires a different base URL. Added `OptionAuditAPIURL` for testing. ([#1144]) +- **Socket mode websocket dial debugging** - Added debug logging when a custom dialer is + used including HTTP response status on dial failures. This helps diagnose proxy/TLS + issues like "bad handshake" errors. ([#1360]) +- **`MsgOptionPostMessageParameters` now passes `MetaData`** - Previously, metadata was + silently dropped when using `PostMessageParameters`. ([#1343]) + +## [0.18.0-rc1] - 2026-01-26 + +### Added + +- **Huddle support** - New `HuddleRoom`, `HuddleParticipantEvent`, and `HuddleRecording` + types for handling Slack huddle events (`huddle_thread` subtype messages). +- **Call block data parsing** - `CallBlock` now includes full call data when retrieved + from Slack messages, with new `CallBlockData`, `CallBlockDataV1`, and `CallBlockIconURLs` + types. ([#897]) +- **Chat Streaming API support** - New streaming API for real-time chat interactions + with example usage. ([#1506]) +- **Data Access API support** - Full support for Slack's Data Access API with + example implementation. ([#1439]) +- **Cursor-based pagination for `GetUsers`** - More efficient user retrieval + with cursor pagination. ([#1465]) +- **`GetAllConversations` with pagination** - Retrieve all conversations with + automatic pagination handling, including rate limit and server error handling. ([#1463]) +- **Table blocks support** - Parse and create table blocks with proper + unmarshaling. ([#1490], [#1511]) +- **Context actions block support** - New `context_actions` block type. ([#1495]) +- **Workflow button block element** - Support for `workflow_button` in block + elements. ([#1499]) +- **`loading_messages` parameter for `SetAssistantThreadsStatus`** - Optional + parameter to customize loading state messages. ([#1489]) +- **Attachment image fields** - Added `ImageBytes`, `ImageHeight`, and `ImageWidth` + fields to attachments. ([#1516]) +- **`RecordChannel` to conversation properties** - New property for conversation + metadata. ([#1513]) +- **Title argument for `CreateChannelCanvas`** - Canvas creation now supports + custom titles. ([#1483]) +- **`PostEphemeral` handler for slacktest** - Audit outgoing ephemeral messages + in test environments. ([#1517]) +- **`PreviewImageName` for remote files** - Customize preview image filename + instead of using the default `preview.jpg`. + +### Fixed + +- **`PublishView` no longer sends empty hash** - Prevents unnecessary payload + when hash is empty. ([#1515]) +- **`ImageBlockElement` validation** - Now properly validates that either + `imageURL` or `SlackFile` is provided. ([#1488]) +- **Rich text section channel return** - Correctly returns channel for section + channel rich text elements. ([#1472]) +- **`KickUserFromConversation` error handling** - Errors are now properly parsed + as a map structure. ([#1471]) + +### Changed + +- **BREAKING**: `GetReactions` now returns `ReactedItem` instead of `[]ItemReaction`. + This aligns the response with the actual Slack API, which includes the item itself + (message, file, or file_comment) alongside reactions. To migrate, use `resp.Reactions` + to access the slice of reactions. ([#1480]) +- **BREAKING**: `Settings` struct fields `Interactivity` and `EventSubscriptions` + are now pointers, allowing them to be omitted when empty. ([#1461]) +- Minimum Go version bumped to 1.24. ([#1504]) + +## [0.17.3] - 2025-07-04 + +Previous release. See [GitHub releases](https://github.com/slack-go/slack/releases/tag/v0.17.3) +for details. + +[#897]: https://github.com/slack-go/slack/issues/897 +[#1236]: https://github.com/slack-go/slack/issues/1236 +[#1257]: https://github.com/slack-go/slack/issues/1257 +[#1144]: https://github.com/slack-go/slack/issues/1144 +[#1329]: https://github.com/slack-go/slack/issues/1329 +[#1343]: https://github.com/slack-go/slack/issues/1343 +[#1360]: https://github.com/slack-go/slack/issues/1360 +[#1439]: https://github.com/slack-go/slack/pull/1439 +[#1461]: https://github.com/slack-go/slack/pull/1461 +[#1463]: https://github.com/slack-go/slack/pull/1463 +[#1465]: https://github.com/slack-go/slack/pull/1465 +[#1471]: https://github.com/slack-go/slack/pull/1471 +[#1472]: https://github.com/slack-go/slack/pull/1472 +[#1480]: https://github.com/slack-go/slack/pull/1480 +[#1483]: https://github.com/slack-go/slack/pull/1483 +[#1488]: https://github.com/slack-go/slack/pull/1488 +[#1489]: https://github.com/slack-go/slack/pull/1489 +[#1490]: https://github.com/slack-go/slack/pull/1490 +[#1491]: https://github.com/slack-go/slack/issues/1491 +[#1495]: https://github.com/slack-go/slack/pull/1495 +[#1497]: https://github.com/slack-go/slack/pull/1497 +[#1499]: https://github.com/slack-go/slack/pull/1499 +[#1504]: https://github.com/slack-go/slack/pull/1504 +[#1506]: https://github.com/slack-go/slack/pull/1506 +[#1511]: https://github.com/slack-go/slack/pull/1511 +[#1513]: https://github.com/slack-go/slack/pull/1513 +[#1515]: https://github.com/slack-go/slack/pull/1515 +[#1516]: https://github.com/slack-go/slack/pull/1516 +[#1517]: https://github.com/slack-go/slack/pull/1517 +[#1519]: https://github.com/slack-go/slack/pull/1519 +[#1520]: https://github.com/slack-go/slack/pull/1520 +[#1522]: https://github.com/slack-go/slack/pull/1522 +[#1524]: https://github.com/slack-go/slack/pull/1524 +[#1526]: https://github.com/slack-go/slack/pull/1526 +[#1528]: https://github.com/slack-go/slack/pull/1528 +[#1529]: https://github.com/slack-go/slack/pull/1529 +[#1530]: https://github.com/slack-go/slack/pull/1530 +[#1532]: https://github.com/slack-go/slack/pull/1532 +[#1533]: https://github.com/slack-go/slack/pull/1533 +[#1534]: https://github.com/slack-go/slack/pull/1534 +[#1536]: https://github.com/slack-go/slack/pull/1536 +[#596]: https://github.com/slack-go/slack/issues/596 +[#1541]: https://github.com/slack-go/slack/issues/1541 +[#1172]: https://github.com/slack-go/slack/issues/1172 +[#1076]: https://github.com/slack-go/slack/issues/1076 +[#1157]: https://github.com/slack-go/slack/issues/1157 +[#1196]: https://github.com/slack-go/slack/issues/1196 +[#1547]: https://github.com/slack-go/slack/pull/1547 + +[Unreleased]: https://github.com/slack-go/slack/compare/v0.23.1...HEAD +[0.23.1]: https://github.com/slack-go/slack/compare/v0.23.0...v0.23.1 +[0.23.0]: https://github.com/slack-go/slack/compare/v0.22.0...v0.23.0 +[0.22.0]: https://github.com/slack-go/slack/compare/v0.21.1...0.22.0 +[0.21.1]: https://github.com/slack-go/slack/compare/v0.21.0...v0.21.1 +[0.21.0]: https://github.com/slack-go/slack/compare/v0.20.0...v0.21.0 +[0.20.0]: https://github.com/slack-go/slack/compare/v0.19.0...v0.20.0 +[0.19.0]: https://github.com/slack-go/slack/compare/v0.18.0...v0.19.0 +[0.18.0]: https://github.com/slack-go/slack/compare/v0.18.0-rc2...v0.18.0 +[0.18.0-rc2]: https://github.com/slack-go/slack/releases/tag/v0.18.0-rc2 +[0.18.0-rc1]: https://github.com/slack-go/slack/releases/tag/v0.18.0-rc1 +[0.17.3]: https://github.com/slack-go/slack/releases/tag/v0.17.3 diff --git a/vendor/github.com/slack-go/slack/CONTRIBUTING.md b/vendor/github.com/slack-go/slack/CONTRIBUTING.md new file mode 100644 index 000000000..8b92d3531 --- /dev/null +++ b/vendor/github.com/slack-go/slack/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing Guide + +Welcome! We are glad that you want to contribute to our project! 💖 + +There are a just a few small guidelines you ask everyone to follow to make things a bit smoother and more consistent. + +## Opening Pull Requests + +1. It's generally best to start by opening a new issue describing the bug or feature you're intending to fix. Even if you think it's relatively minor, it's helpful to know what people are working on. Mention in the initial issue that you are planning to work on that bug or feature so that it can be assigned to you. + +2. Follow the normal process of [forking](https://help.github.com/articles/fork-a-repo) the project, and set up a new branch to work in. It's important that each group of changes be done in separate branches in order to ensure that a pull request only includes the commits related to that bug or feature. + +3. Any significant changes should almost always be accompanied by tests. The project already has some test coverage, so look at some of the existing tests if you're unsure how to go about it. + +4. Run `make pr-prep` to format your code and check that it passes all tests and linters. + +5. Do your best to have [well-formed commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) for each change. This provides consistency throughout the project, and ensures that commit messages are able to be formatted properly by various git tools. _Pull Request Titles_ should generally follow the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format to ease the release note process when cutting releases. + +6. Finally, push the commits to your fork and submit a [pull request](https://help.github.com/articles/creating-a-pull-request). NOTE: Please do not use force-push on PRs in this repo, as it makes it more difficult for reviewers to see what has changed since the last code review. We always perform "squash and merge" actions on PRs in this repo, so it doesn't matter how many commits your PR has, as they will end up being a single commit after merging. This is done to make a much cleaner `git log` history and helps to find regressions in the code using existing tools such as `git bisect`. + +## Code Comments + +Every exported method needs to have code comments that follow [Go Doc Comments](https://go.dev/doc/comment). A typical method's comments will look like this: + +```go +// PostMessage sends a message to a channel. +// +// Slack API docs: https://api.dev.slack.com/methods/chat.postMessage +func (api *Client) PostMessage(ctx context.Context, input PostMesssageInput) (PostMesssageOutput, error) { +... +} +``` + +The first line is the name of the method followed by a short description. This could also be a longer description if needed, but there is no need to repeat any details that are documented in Slack's documentation because users are expected to follow the documentation links to learn more. + +After the description comes a link to the Slack API documentation. + +## Other notes on code organization + +Currently, everything is defined in the main `slack` package, with API methods group separate files by the [Slack API Method Groupings](https://api.dev.slack.com/methods). diff --git a/vendor/github.com/slack-go/slack/Makefile b/vendor/github.com/slack-go/slack/Makefile index 727964016..a8104014d 100644 --- a/vendor/github.com/slack-go/slack/Makefile +++ b/vendor/github.com/slack-go/slack/Makefile @@ -7,7 +7,7 @@ help: @echo "" @echo " make deps : Fetch all dependencies" @echo " make fmt : Run go fmt to fix any formatting issues" - @echo " make lint : Use go vet to check for linting issues" + @echo " make lint : Run golangci-lint for linting issues" @echo " make test : Run all short tests" @echo " make test-race : Run all tests with race condition checking" @echo " make test-integration : Run all tests without limiting to short" @@ -22,7 +22,7 @@ fmt: @go fmt . lint: - @go vet . + @command -v golangci-lint >/dev/null 2>&1 && golangci-lint run ./... || go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 run ./... test: @go test -v -count=1 -timeout 300s -short ./... diff --git a/vendor/github.com/slack-go/slack/README.md b/vendor/github.com/slack-go/slack/README.md index 9618aebc3..53dd912d3 100644 --- a/vendor/github.com/slack-go/slack/README.md +++ b/vendor/github.com/slack-go/slack/README.md @@ -1,9 +1,7 @@ -Slack API in Go [![Go Reference](https://pkg.go.dev/badge/github.com/slack-go/slack.svg)](https://pkg.go.dev/github.com/slack-go/slack) +Slack API in Go [![Go Reference](https://pkg.go.dev/badge/github.com/slack-go/slack.svg)](https://pkg.go.dev/github.com/slack-go/slack) [![CI](https://github.com/slack-go/slack/actions/workflows/test.yml/badge.svg)](https://github.com/slack-go/slack/actions/workflows/test.yml) =============== -This is the original Slack library for Go created by Norberto Lopes, transferred to a GitHub organization. - -You can also chat with us on the #slack-go, #slack-go-ja Slack channel on the Gophers Slack. +You can chat with us on the [#slack-go](https://gophers.slack.com/archives/C02JQ98JHNC), [#slack-go-ja](https://gophers.slack.com/archives/C02HNL8EN3H) Slack channel on the [Gophers Slack](https://gophers.slack.com). ![logo](logo.png "icon") @@ -15,7 +13,12 @@ a fully managed way. There is currently no major version released. Therefore, minor version releases may include backward incompatible changes. -See [CHANGELOG.md](https://github.com/slack-go/slack/blob/master/CHANGELOG.md) or [Releases](https://github.com/slack-go/slack/releases) for more information about the changes. +See [Releases](https://github.com/slack-go/slack/releases) for more information about the changes. + +## Go Versions supported + +We support the same versions of Go as the officially supported Go versions (see [Go +Release Policy](https://go.dev/doc/devel/release#policy)). ## Installing @@ -29,24 +32,24 @@ See [CHANGELOG.md](https://github.com/slack-go/slack/blob/master/CHANGELOG.md) o ```golang import ( - "fmt" + "fmt" - "github.com/slack-go/slack" + "github.com/slack-go/slack" ) func main() { - api := slack.New("YOUR_TOKEN_HERE") - // If you set debugging, it will log all requests to the console - // Useful when encountering issues - // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true)) - groups, err := api.GetUserGroups(slack.GetUserGroupsOptionIncludeUsers(false)) - if err != nil { - fmt.Printf("%s\n", err) - return - } - for _, group := range groups { - fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) - } + api := slack.New("YOUR_TOKEN_HERE") + // If you set debugging, it will log all requests to the console + // Useful when encountering issues + // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true)) + groups, err := api.GetUserGroups(slack.GetUserGroupsOptionIncludeUsers(false)) + if err != nil { + fmt.Printf("%s\n", err) + return + } + for _, group := range groups { + fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) + } } ``` @@ -63,13 +66,21 @@ func main() { api := slack.New("YOUR_TOKEN_HERE") user, err := api.GetUserInfo("U023BECGF") if err != nil { - fmt.Printf("%s\n", err) - return + fmt.Printf("%s\n", err) + return } fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) } ``` +### HTTP retries + +Retries are off by default. Use **OptionRetry(n)** for 429-only retries, or **OptionRetryConfig(cfg)** for full control (connection, 429, opt-in 5xx). With a custom client, pass retry options after `OptionHTTPClient`. See package `slack` doc for handler details. + +```golang +api := slack.New("YOUR_TOKEN_HERE", slack.OptionRetry(3)) +``` + ## Minimal Socket Mode usage: See https://github.com/slack-go/slack/blob/master/examples/socketmode/socketmode.go diff --git a/vendor/github.com/slack-go/slack/TODO.txt b/vendor/github.com/slack-go/slack/TODO.txt deleted file mode 100644 index 8607960b8..000000000 --- a/vendor/github.com/slack-go/slack/TODO.txt +++ /dev/null @@ -1,3 +0,0 @@ -- Add more tests!!! -- Add support to have markdown hints - - See section Message Formatting at https://api.slack.com/docs/formatting diff --git a/vendor/github.com/slack-go/slack/admin.go b/vendor/github.com/slack-go/slack/admin.go index d51426b56..1b0d2178a 100644 --- a/vendor/github.com/slack-go/slack/admin.go +++ b/vendor/github.com/slack-go/slack/admin.go @@ -59,7 +59,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { - return fmt.Errorf("Failed to invite single-channel guest: %s", err) + return fmt.Errorf("failed to invite single-channel guest: %s", err) } return nil @@ -86,7 +86,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { - return fmt.Errorf("Failed to restricted account: %s", err) + return fmt.Errorf("failed to restricted account: %s", err) } return nil @@ -110,7 +110,7 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { - return fmt.Errorf("Failed to invite to team: %s", err) + return fmt.Errorf("failed to invite to team: %s", err) } return nil @@ -132,7 +132,7 @@ func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) err := api.adminRequest(ctx, "setRegular", teamName, values) if err != nil { - return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) + return fmt.Errorf("failed to change the user (%s) to a regular user: %s", user, err) } return nil @@ -154,7 +154,7 @@ func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, use err := api.adminRequest(ctx, "sendSSOBind", teamName, values) if err != nil { - return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) + return fmt.Errorf("failed to send SSO binding email for user (%s): %s", user, err) } return nil @@ -177,7 +177,7 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, err := api.adminRequest(ctx, "setUltraRestricted", teamName, values) if err != nil { - return fmt.Errorf("Failed to ultra-restrict account: %s", err) + return fmt.Errorf("failed to ultra-restrict account: %s", err) } return nil diff --git a/vendor/github.com/slack-go/slack/admin_conversations.go b/vendor/github.com/slack-go/slack/admin_conversations.go new file mode 100644 index 000000000..eba5d98ea --- /dev/null +++ b/vendor/github.com/slack-go/slack/admin_conversations.go @@ -0,0 +1,807 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" + "strconv" + "strings" +) + +// AdminConversationsInviteParams contains arguments for AdminConversationsInvite method call. +type AdminConversationsInviteParams struct { + ChannelID string + UserIDs []string +} + +// AdminConversationsInvite invites users to a channel. +// For more information see the admin.conversations.invite docs: +// https://api.slack.com/methods/admin.conversations.invite +func (api *Client) AdminConversationsInvite(ctx context.Context, params AdminConversationsInviteParams) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {params.ChannelID}, + "user_ids": {strings.Join(params.UserIDs, ",")}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.invite", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsArchive archives a public or private channel. +// For more information see the admin.conversations.archive docs: +// https://api.slack.com/methods/admin.conversations.archive +func (api *Client) AdminConversationsArchive(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.archive", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsUnarchive unarchives a public or private channel. +// For more information see the admin.conversations.unarchive docs: +// https://api.slack.com/methods/admin.conversations.unarchive +func (api *Client) AdminConversationsUnarchive(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.unarchive", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsRename renames a public or private channel. +// For more information see the admin.conversations.rename docs: +// https://api.slack.com/methods/admin.conversations.rename +func (api *Client) AdminConversationsRename(ctx context.Context, channelID, name string) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + "name": {name}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.rename", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsDelete deletes a public or private channel. +// For more information see the admin.conversations.delete docs: +// https://api.slack.com/methods/admin.conversations.delete +func (api *Client) AdminConversationsDelete(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.delete", values, response) + if err != nil { + return err + } + + return response.Err() +} + +type adminConversationsDisconnectSharedParams struct { + leavingTeamIDs []string +} + +// AdminConversationsDisconnectSharedOption is an option for AdminConversationsDisconnectShared. +type AdminConversationsDisconnectSharedOption func(*adminConversationsDisconnectSharedParams) + +// AdminConversationsDisconnectSharedOptionLeavingTeamIDs sets the team IDs of the workspaces to disconnect. +func AdminConversationsDisconnectSharedOptionLeavingTeamIDs(teamIDs []string) AdminConversationsDisconnectSharedOption { + return func(params *adminConversationsDisconnectSharedParams) { + params.leavingTeamIDs = teamIDs + } +} + +// AdminConversationsDisconnectShared disconnects a connected channel from one or more workspaces. +// For more information see the admin.conversations.disconnectShared docs: +// https://api.slack.com/methods/admin.conversations.disconnectShared +func (api *Client) AdminConversationsDisconnectShared(ctx context.Context, channelID string, options ...AdminConversationsDisconnectSharedOption) error { + params := adminConversationsDisconnectSharedParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + if len(params.leavingTeamIDs) > 0 { + values.Add("leaving_team_ids", strings.Join(params.leavingTeamIDs, ",")) + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.disconnectShared", values, response) + if err != nil { + return err + } + + return response.Err() +} + +type adminConversationsCreateParams struct { + description string + orgWide bool + teamID string +} + +// AdminConversationsCreateOption is an option for AdminConversationsCreate. +type AdminConversationsCreateOption func(*adminConversationsCreateParams) + +// AdminConversationsCreateOptionDescription sets the description of the channel. +func AdminConversationsCreateOptionDescription(description string) AdminConversationsCreateOption { + return func(params *adminConversationsCreateParams) { + params.description = description + } +} + +// AdminConversationsCreateOptionOrgWide sets whether the channel should be org-wide. +func AdminConversationsCreateOptionOrgWide(orgWide bool) AdminConversationsCreateOption { + return func(params *adminConversationsCreateParams) { + params.orgWide = orgWide + } +} + +// AdminConversationsCreateOptionTeamID sets the team ID where the channel should be created. +func AdminConversationsCreateOptionTeamID(teamID string) AdminConversationsCreateOption { + return func(params *adminConversationsCreateParams) { + params.teamID = teamID + } +} + +// AdminConversationsCreateResponse represents the response from admin.conversations.create. +type AdminConversationsCreateResponse struct { + SlackResponse + ChannelID string `json:"channel_id"` +} + +// AdminConversationsCreate creates a public or private channel-based conversation. +// For more information see the admin.conversations.create docs: +// https://api.slack.com/methods/admin.conversations.create +func (api *Client) AdminConversationsCreate(ctx context.Context, name string, isPrivate bool, options ...AdminConversationsCreateOption) (string, error) { + params := adminConversationsCreateParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "is_private": {strconv.FormatBool(isPrivate)}, + "name": {name}, + } + + if params.description != "" { + values.Add("description", params.description) + } + + if params.orgWide { + values.Add("org_wide", "true") + } + + if params.teamID != "" { + values.Add("team_id", params.teamID) + } + + response := &AdminConversationsCreateResponse{} + err := api.postMethod(ctx, "admin.conversations.create", values, response) + if err != nil { + return "", err + } + + return response.ChannelID, response.Err() +} + +// AdminConversationsGetTeamsParams contains arguments for AdminConversationsGetTeams method call. +type AdminConversationsGetTeamsParams struct { + ChannelID string + Cursor string + Limit int +} + +// AdminConversationsGetTeamsResponse represents the response from admin.conversations.getTeams. +type AdminConversationsGetTeamsResponse struct { + SlackResponse + TeamIDs []string `json:"team_ids"` +} + +// AdminConversationsGetTeams gets all the workspaces a given public or private channel is connected to within this Enterprise org. +// For more information see the admin.conversations.getTeams docs: +// https://api.slack.com/methods/admin.conversations.getTeams +func (api *Client) AdminConversationsGetTeams(ctx context.Context, params AdminConversationsGetTeamsParams) ([]string, string, error) { + values := url.Values{ + "token": {api.token}, + "channel_id": {params.ChannelID}, + } + + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + + if params.Limit > 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + + response := &AdminConversationsGetTeamsResponse{} + err := api.postMethod(ctx, "admin.conversations.getTeams", values, response) + if err != nil { + return nil, "", err + } + + return response.TeamIDs, response.ResponseMetadata.Cursor, response.Err() +} + +type adminConversationsSearchParams struct { + cursor string + limit int + query string + searchChannelType []string + sort string + sortDir string + teamIDs []string + connectedTeamIDs []string + totalCountOnly bool +} + +// AdminConversationsSearchOption is an option for AdminConversationsSearch. +type AdminConversationsSearchOption func(*adminConversationsSearchParams) + +// AdminConversationsSearchOptionCursor sets the cursor for pagination. +func AdminConversationsSearchOptionCursor(cursor string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.cursor = cursor + } +} + +// AdminConversationsSearchOptionLimit sets the maximum number of results to return. +func AdminConversationsSearchOptionLimit(limit int) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.limit = limit + } +} + +// AdminConversationsSearchOptionQuery sets the search query. +func AdminConversationsSearchOptionQuery(query string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.query = query + } +} + +// AdminConversationsSearchOptionSearchChannelTypes sets the channel types to search. +// Valid values: "private", "public", "private_exclude", "multi_workspace", "org_wide", "external_shared_exclude", "external_shared" +func AdminConversationsSearchOptionSearchChannelTypes(types []string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.searchChannelType = types + } +} + +// AdminConversationsSearchOptionSort sets the sort field. +// Valid values: "name", "member_count", "created" +func AdminConversationsSearchOptionSort(sort string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.sort = sort + } +} + +// AdminConversationsSearchOptionSortDir sets the sort direction. +// Valid values: "asc", "desc" +func AdminConversationsSearchOptionSortDir(sortDir string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.sortDir = sortDir + } +} + +// AdminConversationsSearchOptionTeamIDs filters results to channels in the specified teams. +func AdminConversationsSearchOptionTeamIDs(teamIDs []string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.teamIDs = teamIDs + } +} + +// AdminConversationsSearchOptionConnectedTeamIDs filters results to channels connected to the specified teams. +func AdminConversationsSearchOptionConnectedTeamIDs(teamIDs []string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.connectedTeamIDs = teamIDs + } +} + +// AdminConversationsSearchOptionTotalCountOnly when true, only returns the total count of matching channels. +func AdminConversationsSearchOptionTotalCountOnly(totalCountOnly bool) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.totalCountOnly = totalCountOnly + } +} + +// ChannelEmailAddress represents an email address associated with a channel. +type ChannelEmailAddress struct { + Address string `json:"address"` + CreatorID string `json:"creator_id"` + TeamID string `json:"team_id"` +} + +// AdminConversationOwnershipDetail represents ownership details for lists/canvas. +type AdminConversationOwnershipDetail struct { + Count int `json:"count,omitempty"` + TeamID string `json:"team_id,omitempty"` +} + +// AdminConversationLists represents lists/canvas information in admin conversations. +type AdminConversationLists struct { + OwnershipDetails []AdminConversationOwnershipDetail `json:"ownership_details,omitempty"` + TotalCount int `json:"total_count,omitempty"` +} + +// AdminConversation represents a conversation in admin API responses. +type AdminConversation struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Purpose string `json:"purpose,omitempty"` + MemberCount int `json:"member_count,omitempty"` + Created int64 `json:"created,omitempty"` + CreatorID string `json:"creator_id,omitempty"` + IsPrivate bool `json:"is_private,omitempty"` + IsArchived bool `json:"is_archived,omitempty"` + IsGeneral bool `json:"is_general,omitempty"` + LastActivityTimestamp int64 `json:"last_activity_ts,omitempty"` + IsFrozen bool `json:"is_frozen,omitempty"` + IsOrgDefault bool `json:"is_org_default,omitempty"` + IsOrgMandatory bool `json:"is_org_mandatory,omitempty"` + IsOrgShared bool `json:"is_org_shared,omitempty"` + IsExtShared bool `json:"is_ext_shared,omitempty"` + IsGlobalShared bool `json:"is_global_shared,omitempty"` + IsPendingExtShared bool `json:"is_pending_ext_shared,omitempty"` + IsDisconnectInProgress bool `json:"is_disconnect_in_progress,omitempty"` + ConnectedTeamIDs []string `json:"connected_team_ids,omitempty"` + ConnectedLimitedTeamIDs []string `json:"connected_limited_team_ids,omitempty"` + PendingConnectedTeamIDs []string `json:"pending_connected_team_ids,omitempty"` + InternalTeamIDs []string `json:"internal_team_ids,omitempty"` + InternalTeamIDsCount int `json:"internal_team_ids_count,omitempty"` + InternalTeamIDsSampleTeam string `json:"internal_team_ids_sample_team,omitempty"` + ContextTeamID string `json:"context_team_id,omitempty"` + ConversationHostID string `json:"conversation_host_id,omitempty"` + ChannelEmailAddresses []ChannelEmailAddress `json:"channel_email_addresses,omitempty"` + ChannelManagerCount int `json:"channel_manager_count,omitempty"` + ExternalUserCount int `json:"external_user_count,omitempty"` + Canvas *AdminConversationLists `json:"canvas,omitempty"` + Lists *AdminConversationLists `json:"lists,omitempty"` + Properties *Properties `json:"properties,omitempty"` +} + +// AdminConversationsSearchResponse represents the response from admin.conversations.search. +type AdminConversationsSearchResponse struct { + SlackResponse + Conversations []AdminConversation `json:"conversations"` + TotalCount int `json:"total_count"` + NextCursor string `json:"next_cursor"` +} + +// AdminConversationsSearch searches for public or private channels in an Enterprise organization. +// For more information see the admin.conversations.search docs: +// https://api.slack.com/methods/admin.conversations.search +func (api *Client) AdminConversationsSearch(ctx context.Context, options ...AdminConversationsSearchOption) (*AdminConversationsSearchResponse, error) { + params := adminConversationsSearchParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + } + + if params.cursor != "" { + values.Add("cursor", params.cursor) + } + + if params.limit > 0 { + values.Add("limit", strconv.Itoa(params.limit)) + } + + if params.query != "" { + values.Add("query", params.query) + } + + if len(params.searchChannelType) > 0 { + values.Add("search_channel_types", strings.Join(params.searchChannelType, ",")) + } + + if params.sort != "" { + values.Add("sort", params.sort) + } + + if params.sortDir != "" { + values.Add("sort_dir", params.sortDir) + } + + if len(params.teamIDs) > 0 { + values.Add("team_ids", strings.Join(params.teamIDs, ",")) + } + + if len(params.connectedTeamIDs) > 0 { + values.Add("connected_team_ids", strings.Join(params.connectedTeamIDs, ",")) + } + + if params.totalCountOnly { + values.Add("total_count_only", "true") + } + + response := &AdminConversationsSearchResponse{} + err := api.postMethod(ctx, "admin.conversations.search", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +type adminConversationsLookupParams struct { + cursor string + limit int + maxMemberCount int +} + +// AdminConversationsLookupOption is an option for AdminConversationsLookup. +type AdminConversationsLookupOption func(*adminConversationsLookupParams) + +// AdminConversationsLookupOptionCursor sets the cursor for pagination. +func AdminConversationsLookupOptionCursor(cursor string) AdminConversationsLookupOption { + return func(params *adminConversationsLookupParams) { + params.cursor = cursor + } +} + +// AdminConversationsLookupOptionLimit sets the maximum number of results to return. +func AdminConversationsLookupOptionLimit(limit int) AdminConversationsLookupOption { + return func(params *adminConversationsLookupParams) { + params.limit = limit + } +} + +// AdminConversationsLookupOptionMaxMemberCount filters to channels with at most this many members. +func AdminConversationsLookupOptionMaxMemberCount(maxMemberCount int) AdminConversationsLookupOption { + return func(params *adminConversationsLookupParams) { + params.maxMemberCount = maxMemberCount + } +} + +// AdminConversationsLookupResponse represents the response from admin.conversations.lookup. +type AdminConversationsLookupResponse struct { + SlackResponse + Channels []string `json:"channels"` +} + +// AdminConversationsLookup returns channels on the given team matching the specified filters. +// For more information see the admin.conversations.lookup docs: +// https://api.slack.com/methods/admin.conversations.lookup +func (api *Client) AdminConversationsLookup(ctx context.Context, teamIDs []string, lastMessageActivityBefore int64, options ...AdminConversationsLookupOption) ([]string, string, error) { + params := adminConversationsLookupParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "last_message_activity_before": {strconv.FormatInt(lastMessageActivityBefore, 10)}, + "team_ids": {strings.Join(teamIDs, ",")}, + } + + if params.cursor != "" { + values.Add("cursor", params.cursor) + } + + if params.limit > 0 { + values.Add("limit", strconv.Itoa(params.limit)) + } + + if params.maxMemberCount > 0 { + values.Add("max_member_count", strconv.Itoa(params.maxMemberCount)) + } + + response := &AdminConversationsLookupResponse{} + err := api.postMethod(ctx, "admin.conversations.lookup", values, response) + if err != nil { + return nil, "", err + } + + return response.Channels, response.ResponseMetadata.Cursor, response.Err() +} + +// AdminConversationsBulkArchive archives public or private channels in bulk. +// For more information see the admin.conversations.bulkArchive docs: +// https://api.slack.com/methods/admin.conversations.bulkArchive +func (api *Client) AdminConversationsBulkArchive(ctx context.Context, channelIDs []string) error { + values := url.Values{ + "token": {api.token}, + "channel_ids": {strings.Join(channelIDs, ",")}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.bulkArchive", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsBulkDelete deletes public or private channels in bulk. +// For more information see the admin.conversations.bulkDelete docs: +// https://api.slack.com/methods/admin.conversations.bulkDelete +func (api *Client) AdminConversationsBulkDelete(ctx context.Context, channelIDs []string) error { + values := url.Values{ + "token": {api.token}, + "channel_ids": {strings.Join(channelIDs, ",")}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.bulkDelete", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsBulkMoveParams contains arguments for AdminConversationsBulkMove method call. +type AdminConversationsBulkMoveParams struct { + ChannelIDs []string + TargetTeamID string +} + +// AdminConversationsBulkMove moves public or private channels in bulk. +// For more information see the admin.conversations.bulkMove docs: +// https://api.slack.com/methods/admin.conversations.bulkMove +func (api *Client) AdminConversationsBulkMove(ctx context.Context, params AdminConversationsBulkMoveParams) error { + values := url.Values{ + "token": {api.token}, + "channel_ids": {strings.Join(params.ChannelIDs, ",")}, + "target_team_id": {params.TargetTeamID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.bulkMove", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationPrefs represents conversation preferences. +type AdminConversationPrefs struct { + WhoCanPost *AdminConversationPref `json:"who_can_post,omitempty"` + CanThread *AdminConversationPref `json:"can_thread,omitempty"` + CanHuddle *AdminConversationPref `json:"can_huddle,omitempty"` + EnableAtHere *AdminConversationPrefEnabled `json:"enable_at_here,omitempty"` + EnableAtChannel *AdminConversationPrefEnabled `json:"enable_at_channel,omitempty"` +} + +// AdminConversationPrefEnabled represents an enabled/disabled preference. +type AdminConversationPrefEnabled struct { + Enabled bool `json:"enabled"` +} + +// AdminConversationPref represents a single conversation preference. +type AdminConversationPref struct { + Type []string `json:"type,omitempty"` + User []string `json:"user,omitempty"` +} + +// AdminConversationsGetConversationPrefsResponse represents the response from admin.conversations.getConversationPrefs. +type AdminConversationsGetConversationPrefsResponse struct { + SlackResponse + Prefs AdminConversationPrefs `json:"prefs"` +} + +// AdminConversationsGetConversationPrefs gets conversation preferences for a public or private channel. +// For more information see the admin.conversations.getConversationPrefs docs: +// https://api.slack.com/methods/admin.conversations.getConversationPrefs +func (api *Client) AdminConversationsGetConversationPrefs(ctx context.Context, channelID string) (*AdminConversationPrefs, error) { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &AdminConversationsGetConversationPrefsResponse{} + err := api.postMethod(ctx, "admin.conversations.getConversationPrefs", values, response) + if err != nil { + return nil, err + } + + return &response.Prefs, response.Err() +} + +// AdminConversationsSetConversationPrefsParams contains arguments for AdminConversationsSetConversationPrefs method call. +type AdminConversationsSetConversationPrefsParams struct { + ChannelID string + Prefs AdminConversationPrefs +} + +// AdminConversationsSetConversationPrefs sets conversation preferences for a public or private channel. +// For more information see the admin.conversations.setConversationPrefs docs: +// https://api.slack.com/methods/admin.conversations.setConversationPrefs +func (api *Client) AdminConversationsSetConversationPrefs(ctx context.Context, params AdminConversationsSetConversationPrefsParams) error { + prefsJSON, err := json.Marshal(params.Prefs) + if err != nil { + return err + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {params.ChannelID}, + "prefs": {string(prefsJSON)}, + } + + response := &SlackResponse{} + err = api.postMethod(ctx, "admin.conversations.setConversationPrefs", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsGetCustomRetentionResponse represents the response from admin.conversations.getCustomRetention. +type AdminConversationsGetCustomRetentionResponse struct { + SlackResponse + DurationDays int `json:"duration_days"` + IsPolicyEnabled bool `json:"is_policy_enabled"` +} + +// AdminConversationsGetCustomRetention gets a conversation's custom retention policy. +// For more information see the admin.conversations.getCustomRetention docs: +// https://api.slack.com/methods/admin.conversations.getCustomRetention +func (api *Client) AdminConversationsGetCustomRetention(ctx context.Context, channelID string) (*AdminConversationsGetCustomRetentionResponse, error) { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &AdminConversationsGetCustomRetentionResponse{} + err := api.postMethod(ctx, "admin.conversations.getCustomRetention", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// AdminConversationsSetCustomRetention sets a conversation's custom retention policy. +// For more information see the admin.conversations.setCustomRetention docs: +// https://api.slack.com/methods/admin.conversations.setCustomRetention +func (api *Client) AdminConversationsSetCustomRetention(ctx context.Context, channelID string, durationDays int) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + "duration_days": {strconv.Itoa(durationDays)}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.setCustomRetention", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsRemoveCustomRetention removes a conversation's custom retention policy. +// For more information see the admin.conversations.removeCustomRetention docs: +// https://api.slack.com/methods/admin.conversations.removeCustomRetention +func (api *Client) AdminConversationsRemoveCustomRetention(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.removeCustomRetention", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsSetTeamsParams contains arguments for AdminConversationsSetTeams +// method calls. +type AdminConversationsSetTeamsParams struct { + ChannelID string + OrgChannel *bool + TargetTeamIDs []string + TeamID *string +} + +// Set the workspaces in an Enterprise Grid organisation that connect to a public or +// private channel. +// See: https://api.slack.com/methods/admin.conversations.setTeams +func (api *Client) AdminConversationsSetTeams(ctx context.Context, params AdminConversationsSetTeamsParams) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {params.ChannelID}, + } + + if params.OrgChannel != nil { + values.Add("org_channel", strconv.FormatBool(*params.OrgChannel)) + } + + if len(params.TargetTeamIDs) > 0 { + values.Add("target_team_ids", strings.Join(params.TargetTeamIDs, ",")) // ["T123", "T456"] - > "T123,T456" + } + + if params.TeamID != nil { + values.Add("team_id", *params.TeamID) + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.setTeams", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// ConversationsConvertToPrivate converts a public channel to a private channel. To do +// this, you must have the admin.conversations:write scope. There are other requirements: +// you should read the Slack documentation for more details. +// See: https://api.slack.com/methods/admin.conversations.convertToPrivate +func (api *Client) AdminConversationsConvertToPrivate(ctx context.Context, channelID string) error { + values := url.Values{ + "token": []string{api.token}, + "channel_id": []string{channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.convertToPrivate", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// ConversationsConvertToPublic converts a private channel to a public channel. To do +// this, you must have the admin.conversations:write scope. There are other requirements: +// you should read the Slack documentation for more details. +// See: https://api.slack.com/methods/admin.conversations.convertToPublic +func (api *Client) AdminConversationsConvertToPublic(ctx context.Context, channelID string) error { + values := url.Values{ + "token": []string{api.token}, + "channel_id": []string{channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.convertToPublic", values, response) + if err != nil { + return err + } + + return response.Err() +} diff --git a/vendor/github.com/slack-go/slack/admin_conversations_ekm.go b/vendor/github.com/slack-go/slack/admin_conversations_ekm.go new file mode 100644 index 000000000..f4b4e14ef --- /dev/null +++ b/vendor/github.com/slack-go/slack/admin_conversations_ekm.go @@ -0,0 +1,101 @@ +package slack + +import ( + "context" + "net/url" + "strconv" + "strings" +) + +type adminConversationsEKMListOriginalConnectedChannelInfoParams struct { + channelIDs []string + teamIDs []string + cursor string + limit int +} + +// AdminConversationsEKMListOriginalConnectedChannelInfoOption is an option for +// AdminConversationsEKMListOriginalConnectedChannelInfo. +type AdminConversationsEKMListOriginalConnectedChannelInfoOption func(*adminConversationsEKMListOriginalConnectedChannelInfoParams) + +// AdminConversationsEKMListOriginalConnectedChannelInfoOptionChannelIDs filters results to specific channels. +func AdminConversationsEKMListOriginalConnectedChannelInfoOptionChannelIDs(channelIDs []string) AdminConversationsEKMListOriginalConnectedChannelInfoOption { + return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) { + params.channelIDs = channelIDs + } +} + +// AdminConversationsEKMListOriginalConnectedChannelInfoOptionTeamIDs filters results to specific teams. +func AdminConversationsEKMListOriginalConnectedChannelInfoOptionTeamIDs(teamIDs []string) AdminConversationsEKMListOriginalConnectedChannelInfoOption { + return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) { + params.teamIDs = teamIDs + } +} + +// AdminConversationsEKMListOriginalConnectedChannelInfoOptionCursor sets the cursor for pagination. +func AdminConversationsEKMListOriginalConnectedChannelInfoOptionCursor(cursor string) AdminConversationsEKMListOriginalConnectedChannelInfoOption { + return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) { + params.cursor = cursor + } +} + +// AdminConversationsEKMListOriginalConnectedChannelInfoOptionLimit sets the maximum number of results to return. +func AdminConversationsEKMListOriginalConnectedChannelInfoOptionLimit(limit int) AdminConversationsEKMListOriginalConnectedChannelInfoOption { + return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) { + params.limit = limit + } +} + +// AdminConversationsEKMOriginalConnectedChannelInfo represents channel info for EKM response. +type AdminConversationsEKMOriginalConnectedChannelInfo struct { + ID string `json:"id"` + OriginalConnectedHostID string `json:"original_connected_host_id"` + OriginalConnectedChannelID string `json:"original_connected_channel_id"` + InternalTeamIDs []string `json:"internal_team_ids_count"` +} + +// AdminConversationsEKMListOriginalConnectedChannelInfoResponse represents the response from +// admin.conversations.ekm.listOriginalConnectedChannelInfo. +type AdminConversationsEKMListOriginalConnectedChannelInfoResponse struct { + SlackResponse + Channels []AdminConversationsEKMOriginalConnectedChannelInfo `json:"channels"` +} + +// AdminConversationsEKMListOriginalConnectedChannelInfo lists the original connected channel +// information for Slack Connect channels. +// For more information see the admin.conversations.ekm.listOriginalConnectedChannelInfo docs: +// https://api.slack.com/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo +func (api *Client) AdminConversationsEKMListOriginalConnectedChannelInfo(ctx context.Context, options ...AdminConversationsEKMListOriginalConnectedChannelInfoOption) (*AdminConversationsEKMListOriginalConnectedChannelInfoResponse, error) { + params := adminConversationsEKMListOriginalConnectedChannelInfoParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + } + + if len(params.channelIDs) > 0 { + values.Add("channel_ids", strings.Join(params.channelIDs, ",")) + } + + if len(params.teamIDs) > 0 { + values.Add("team_ids", strings.Join(params.teamIDs, ",")) + } + + if params.cursor != "" { + values.Add("cursor", params.cursor) + } + + if params.limit > 0 { + values.Add("limit", strconv.Itoa(params.limit)) + } + + response := &AdminConversationsEKMListOriginalConnectedChannelInfoResponse{} + err := api.postMethod(ctx, "admin.conversations.ekm.listOriginalConnectedChannelInfo", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go b/vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go new file mode 100644 index 000000000..344579972 --- /dev/null +++ b/vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go @@ -0,0 +1,150 @@ +package slack + +import ( + "context" + "net/url" +) + +// AdminConversationsRestrictAccessAddGroup + +type adminConversationsRestrictAccessAddGroupParams struct { + teamID string +} + +// AdminConversationsRestrictAccessAddGroupOption is an option for AdminConversationsRestrictAccessAddGroup. +type AdminConversationsRestrictAccessAddGroupOption func(*adminConversationsRestrictAccessAddGroupParams) + +// AdminConversationsRestrictAccessAddGroupOptionTeamID sets the workspace where the channel exists. +// Required if using an org token. +func AdminConversationsRestrictAccessAddGroupOptionTeamID(teamID string) AdminConversationsRestrictAccessAddGroupOption { + return func(params *adminConversationsRestrictAccessAddGroupParams) { + params.teamID = teamID + } +} + +// AdminConversationsRestrictAccessAddGroup adds an allowlist of IDP groups +// for accessing a channel. +// For more information see the admin.conversations.restrictAccess.addGroup docs: +// https://api.slack.com/methods/admin.conversations.restrictAccess.addGroup +func (api *Client) AdminConversationsRestrictAccessAddGroup(ctx context.Context, channelID, groupID string, options ...AdminConversationsRestrictAccessAddGroupOption) error { + params := adminConversationsRestrictAccessAddGroupParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + "group_id": {groupID}, + } + + if params.teamID != "" { + values.Add("team_id", params.teamID) + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.restrictAccess.addGroup", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsRestrictAccessListGroups + +type adminConversationsRestrictAccessListGroupsParams struct { + teamID string +} + +// AdminConversationsRestrictAccessListGroupsOption is an option for AdminConversationsRestrictAccessListGroups. +type AdminConversationsRestrictAccessListGroupsOption func(*adminConversationsRestrictAccessListGroupsParams) + +// AdminConversationsRestrictAccessListGroupsOptionTeamID sets the workspace where the channel exists. +// Required if using an org token. +func AdminConversationsRestrictAccessListGroupsOptionTeamID(teamID string) AdminConversationsRestrictAccessListGroupsOption { + return func(params *adminConversationsRestrictAccessListGroupsParams) { + params.teamID = teamID + } +} + +// AdminConversationsRestrictAccessListGroupsResponse represents the response from +// admin.conversations.restrictAccess.listGroups. +type AdminConversationsRestrictAccessListGroupsResponse struct { + SlackResponse + GroupIDs []string `json:"group_ids"` +} + +// AdminConversationsRestrictAccessListGroups lists the allowlist of IDP groups +// for a private channel. +// For more information see the admin.conversations.restrictAccess.listGroups docs: +// https://api.slack.com/methods/admin.conversations.restrictAccess.listGroups +func (api *Client) AdminConversationsRestrictAccessListGroups(ctx context.Context, channelID string, options ...AdminConversationsRestrictAccessListGroupsOption) ([]string, error) { + params := adminConversationsRestrictAccessListGroupsParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + if params.teamID != "" { + values.Add("team_id", params.teamID) + } + + response := &AdminConversationsRestrictAccessListGroupsResponse{} + err := api.postMethod(ctx, "admin.conversations.restrictAccess.listGroups", values, response) + if err != nil { + return nil, err + } + + return response.GroupIDs, response.Err() +} + +// AdminConversationsRestrictAccessRemoveGroup + +type adminConversationsRestrictAccessRemoveGroupParams struct { + teamID string +} + +// AdminConversationsRestrictAccessRemoveGroupOption is an option for AdminConversationsRestrictAccessRemoveGroup. +type AdminConversationsRestrictAccessRemoveGroupOption func(*adminConversationsRestrictAccessRemoveGroupParams) + +// AdminConversationsRestrictAccessRemoveGroupOptionTeamID sets the workspace where the channel exists. +// Required if using an org token. +func AdminConversationsRestrictAccessRemoveGroupOptionTeamID(teamID string) AdminConversationsRestrictAccessRemoveGroupOption { + return func(params *adminConversationsRestrictAccessRemoveGroupParams) { + params.teamID = teamID + } +} + +// AdminConversationsRestrictAccessRemoveGroup removes an IDP group from the +// allowlist of a private channel. +// For more information see the admin.conversations.restrictAccess.removeGroup docs: +// https://api.slack.com/methods/admin.conversations.restrictAccess.removeGroup +func (api *Client) AdminConversationsRestrictAccessRemoveGroup(ctx context.Context, channelID, groupID string, options ...AdminConversationsRestrictAccessRemoveGroupOption) error { + params := adminConversationsRestrictAccessRemoveGroupParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + "group_id": {groupID}, + } + + if params.teamID != "" { + values.Add("team_id", params.teamID) + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.restrictAccess.removeGroup", values, response) + if err != nil { + return err + } + + return response.Err() +} diff --git a/vendor/github.com/slack-go/slack/admin_roles.go b/vendor/github.com/slack-go/slack/admin_roles.go new file mode 100644 index 000000000..676acf0bd --- /dev/null +++ b/vendor/github.com/slack-go/slack/admin_roles.go @@ -0,0 +1,204 @@ +package slack + +import ( + "context" + "net/url" + "strconv" + "strings" +) + +// AdminRolesAddAssignmentsParams contains arguments for AdminRolesAddAssignments method call. +type AdminRolesAddAssignmentsParams struct { + RoleID string + EntityIDs []string + UserIDs []string +} + +// AdminRolesRejectedUser represents a user that could not be assigned a role. +type AdminRolesRejectedUser struct { + ID string `json:"id"` + Error string `json:"error"` +} + +// AdminRolesRejectedEntity represents an entity that could not be assigned a role. +type AdminRolesRejectedEntity struct { + ID string `json:"id"` + Error string `json:"error"` +} + +// AdminRolesAddAssignmentsResponse represents the response from admin.roles.addAssignments. +type AdminRolesAddAssignmentsResponse struct { + SlackResponse + RejectedUsers []AdminRolesRejectedUser `json:"rejected_users"` + RejectedEntities []AdminRolesRejectedEntity `json:"rejected_entities"` +} + +// AdminRolesAddAssignments adds members to a specified role. +// For more information see the admin.roles.addAssignments docs: +// https://api.slack.com/methods/admin.roles.addAssignments +func (api *Client) AdminRolesAddAssignments(ctx context.Context, params AdminRolesAddAssignmentsParams) (*AdminRolesAddAssignmentsResponse, error) { + values := url.Values{ + "token": {api.token}, + "role_id": {params.RoleID}, + } + + if len(params.EntityIDs) > 0 { + values.Add("entity_ids", strings.Join(params.EntityIDs, ",")) + } + + if len(params.UserIDs) > 0 { + values.Add("user_ids", strings.Join(params.UserIDs, ",")) + } + + response := &AdminRolesAddAssignmentsResponse{} + err := api.postMethod(ctx, "admin.roles.addAssignments", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +type adminRolesListAssignmentsParams struct { + roleIDs []string + entityIDs []string + limit int + cursor string + sortDirection string +} + +// AdminRolesListAssignmentsOption is an option for AdminRolesListAssignments. +type AdminRolesListAssignmentsOption func(*adminRolesListAssignmentsParams) + +// AdminRolesListAssignmentsOptionRoleIDs filters results to the specified role IDs. +func AdminRolesListAssignmentsOptionRoleIDs(roleIDs []string) AdminRolesListAssignmentsOption { + return func(params *adminRolesListAssignmentsParams) { + params.roleIDs = roleIDs + } +} + +// AdminRolesListAssignmentsOptionEntityIDs filters results to the specified entity IDs. +func AdminRolesListAssignmentsOptionEntityIDs(entityIDs []string) AdminRolesListAssignmentsOption { + return func(params *adminRolesListAssignmentsParams) { + params.entityIDs = entityIDs + } +} + +// AdminRolesListAssignmentsOptionLimit sets the maximum number of results to return. +func AdminRolesListAssignmentsOptionLimit(limit int) AdminRolesListAssignmentsOption { + return func(params *adminRolesListAssignmentsParams) { + params.limit = limit + } +} + +// AdminRolesListAssignmentsOptionCursor sets the cursor for pagination. +func AdminRolesListAssignmentsOptionCursor(cursor string) AdminRolesListAssignmentsOption { + return func(params *adminRolesListAssignmentsParams) { + params.cursor = cursor + } +} + +// AdminRolesListAssignmentsOptionSortDir sets the sort direction. +// Valid values: "asc", "desc". +func AdminRolesListAssignmentsOptionSortDir(sortDir string) AdminRolesListAssignmentsOption { + return func(params *adminRolesListAssignmentsParams) { + params.sortDirection = sortDir + } +} + +// RoleAssignment represents a single role assignment. +type RoleAssignment struct { + RoleID string `json:"role_id"` + EntityID string `json:"entity_id,omitempty"` + UserID string `json:"user_id,omitempty"` + DateCreate int64 `json:"date_create,omitempty"` +} + +// AdminRolesListAssignmentsResponse represents the response from admin.roles.listAssignments. +type AdminRolesListAssignmentsResponse struct { + SlackResponse + RoleAssignments []RoleAssignment `json:"role_assignments"` + ResponseMetadata ResponseMetadata `json:"response_metadata"` +} + +// AdminRolesListAssignments lists assignments for roles. +// For more information see the admin.roles.listAssignments docs: +// https://api.slack.com/methods/admin.roles.listAssignments +func (api *Client) AdminRolesListAssignments(ctx context.Context, options ...AdminRolesListAssignmentsOption) (*AdminRolesListAssignmentsResponse, error) { + params := adminRolesListAssignmentsParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + } + + if len(params.roleIDs) > 0 { + values.Add("role_ids", strings.Join(params.roleIDs, ",")) + } + + if len(params.entityIDs) > 0 { + values.Add("entity_ids", strings.Join(params.entityIDs, ",")) + } + + if params.limit > 0 { + values.Add("limit", strconv.Itoa(params.limit)) + } + + if params.cursor != "" { + values.Add("cursor", params.cursor) + } + + if params.sortDirection != "" { + values.Add("sort_dir", params.sortDirection) + } + + response := &AdminRolesListAssignmentsResponse{} + err := api.postMethod(ctx, "admin.roles.listAssignments", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// AdminRolesRemoveAssignmentsParams contains arguments for AdminRolesRemoveAssignments method call. +type AdminRolesRemoveAssignmentsParams struct { + RoleID string + EntityIDs []string + UserIDs []string +} + +// AdminRolesRemoveAssignmentsResponse represents the response from admin.roles.removeAssignments. +type AdminRolesRemoveAssignmentsResponse struct { + SlackResponse + RejectedUsers []AdminRolesRejectedUser `json:"rejected_users"` + RejectedEntities []AdminRolesRejectedEntity `json:"rejected_entities"` +} + +// AdminRolesRemoveAssignments removes members from a specified role. +// For more information see the admin.roles.removeAssignments docs: +// https://api.slack.com/methods/admin.roles.removeAssignments +func (api *Client) AdminRolesRemoveAssignments(ctx context.Context, params AdminRolesRemoveAssignmentsParams) (*AdminRolesRemoveAssignmentsResponse, error) { + values := url.Values{ + "token": {api.token}, + "role_id": {params.RoleID}, + } + + if len(params.EntityIDs) > 0 { + values.Add("entity_ids", strings.Join(params.EntityIDs, ",")) + } + + if len(params.UserIDs) > 0 { + values.Add("user_ids", strings.Join(params.UserIDs, ",")) + } + + response := &AdminRolesRemoveAssignmentsResponse{} + err := api.postMethod(ctx, "admin.roles.removeAssignments", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/admin_teams.go b/vendor/github.com/slack-go/slack/admin_teams.go new file mode 100644 index 000000000..17ae3df33 --- /dev/null +++ b/vendor/github.com/slack-go/slack/admin_teams.go @@ -0,0 +1,153 @@ +package slack + +import ( + "context" + "net/url" + "strings" +) + +// AdminTeamSettings contains workspace settings returned by admin.teams.settings.info. +type AdminTeamSettings struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Domain string `json:"domain"` + EmailDomain string `json:"email_domain"` + AvatarBaseURL string `json:"avatar_base_url"` + IsVerified bool `json:"is_verified"` + Icon TeamSettingsIcon `json:"icon"` + EnterpriseID string `json:"enterprise_id"` + EnterpriseName string `json:"enterprise_name"` + EnterpriseDomain string `json:"enterprise_domain"` + DefaultChannels []string `json:"default_channels"` +} + +// TeamSettingsIcon contains team icon URLs and a default flag. +type TeamSettingsIcon struct { + ImageDefault bool `json:"image_default"` + Image34 string `json:"image_34"` + Image44 string `json:"image_44"` + Image68 string `json:"image_68"` + Image88 string `json:"image_88"` + Image102 string `json:"image_102"` + Image132 string `json:"image_132"` + Image230 string `json:"image_230"` +} + +// TeamDiscoverability represents the discoverability setting for a workspace. +type TeamDiscoverability string + +const ( + TeamDiscoverabilityOpen TeamDiscoverability = "open" + TeamDiscoverabilityInviteOnly TeamDiscoverability = "invite_only" + TeamDiscoverabilityClosed TeamDiscoverability = "closed" + TeamDiscoverabilityUnlisted TeamDiscoverability = "unlisted" +) + +type adminTeamSettingsInfoResponse struct { + Team AdminTeamSettings `json:"team"` + SlackResponse +} + +// AdminTeamsSettingsInfo returns workspace settings. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.info +func (api *Client) AdminTeamsSettingsInfo(ctx context.Context, teamID string) (*AdminTeamSettings, error) { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + } + + response := &adminTeamSettingsInfoResponse{} + err := api.postMethod(ctx, "admin.teams.settings.info", values, response) + if err != nil { + return nil, err + } + + return &response.Team, response.Err() +} + +// AdminTeamsSettingsSetDefaultChannels sets the default channels for a workspace. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels +func (api *Client) AdminTeamsSettingsSetDefaultChannels(ctx context.Context, teamID string, channelIDs ...string) error { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + "channel_ids": {strings.Join(channelIDs, ",")}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.teams.settings.setDefaultChannels", values, response) + if err != nil { + return err + } + return response.Err() +} + +// AdminTeamsSettingsSetDescription sets the description for a workspace. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription +func (api *Client) AdminTeamsSettingsSetDescription(ctx context.Context, teamID, description string) error { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + "description": {description}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.teams.settings.setDescription", values, response) + if err != nil { + return err + } + return response.Err() +} + +// AdminTeamsSettingsSetDiscoverability sets the discoverability for a workspace. +// The discoverability parameter must be one of: open, invite_only, closed, or unlisted. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability +func (api *Client) AdminTeamsSettingsSetDiscoverability(ctx context.Context, teamID string, discoverability TeamDiscoverability) error { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + "discoverability": {string(discoverability)}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.teams.settings.setDiscoverability", values, response) + if err != nil { + return err + } + return response.Err() +} + +// AdminTeamsSettingsSetIcon sets the icon for a workspace. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon +func (api *Client) AdminTeamsSettingsSetIcon(ctx context.Context, teamID, imageURL string) error { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + "image_url": {imageURL}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.teams.settings.setIcon", values, response) + if err != nil { + return err + } + return response.Err() +} + +// AdminTeamsSettingsSetName sets the name for a workspace. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setName +func (api *Client) AdminTeamsSettingsSetName(ctx context.Context, teamID, name string) error { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + "name": {name}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.teams.settings.setName", values, response) + if err != nil { + return err + } + return response.Err() +} diff --git a/vendor/github.com/slack-go/slack/apps.go b/vendor/github.com/slack-go/slack/apps.go index 7322b15b9..c75569fb6 100644 --- a/vendor/github.com/slack-go/slack/apps.go +++ b/vendor/github.com/slack-go/slack/apps.go @@ -35,7 +35,7 @@ func (api *Client) ListEventAuthorizationsContext(ctx context.Context, eventCont "event_context": eventContext, }) - err := postJSON(ctx, api.httpclient, api.endpoint+"apps.event.authorizations.list", api.appLevelToken, request, &resp, api) + err := api.postJSONMethod(ctx, "apps.event.authorizations.list", api.appLevelToken, request, &resp) if err != nil { return nil, err diff --git a/vendor/github.com/slack-go/slack/assistant.go b/vendor/github.com/slack-go/slack/assistant.go new file mode 100644 index 000000000..fb82cb64a --- /dev/null +++ b/vendor/github.com/slack-go/slack/assistant.go @@ -0,0 +1,377 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" + "strconv" + "strings" +) + +// AssistantThreadSetStatusParameters are the parameters for AssistantThreadSetStatus +type AssistantThreadsSetStatusParameters struct { + ChannelID string `json:"channel_id"` + Status string `json:"status"` + ThreadTS string `json:"thread_ts"` + LoadingMessages []string `json:"loading_messages,omitempty"` + Username string `json:"username,omitempty"` + IconURL string `json:"icon_url,omitempty"` + IconEmoji string `json:"icon_emoji,omitempty"` +} + +// AssistantThreadSetTitleParameters are the parameters for AssistantThreadSetTitle +type AssistantThreadsSetTitleParameters struct { + ChannelID string `json:"channel_id"` + ThreadTS string `json:"thread_ts"` + Title string `json:"title"` +} + +// AssistantThreadSetSuggestedPromptsParameters are the parameters for AssistantThreadSetSuggestedPrompts +type AssistantThreadsSetSuggestedPromptsParameters struct { + Title string `json:"title"` + ChannelID string `json:"channel_id"` + ThreadTS string `json:"thread_ts"` + Prompts []AssistantThreadsPrompt `json:"prompts"` +} + +// AssistantThreadPrompt is a suggested prompt for a thread +type AssistantThreadsPrompt struct { + Title string `json:"title"` + Message string `json:"message"` +} + +// AssistantSearchContextParameters are the parameters for AssistantSearchContext +type AssistantSearchContextParameters struct { + Query string `json:"query"` + ActionToken string `json:"action_token,omitempty"` + ChannelTypes []string `json:"channel_types,omitempty"` + ContentTypes []string `json:"content_types,omitempty"` + ContextChannelID string `json:"context_channel_id,omitempty"` + Cursor string `json:"cursor,omitempty"` + IncludeBots bool `json:"include_bots,omitempty"` + Limit int `json:"limit,omitempty"` + IncludeDeletedUsers bool `json:"include_deleted_users,omitempty"` + Before int64 `json:"before,omitempty"` + After int64 `json:"after,omitempty"` + IncludeContextMessages bool `json:"include_context_messages,omitempty"` + Sort string `json:"sort,omitempty"` + SortDir string `json:"sort_dir,omitempty"` + IncludeMessageBlocks bool `json:"include_message_blocks,omitempty"` + Highlight bool `json:"highlight,omitempty"` + TermClauses []string `json:"term_clauses,omitempty"` + Modifiers string `json:"modifiers,omitempty"` + IncludeArchivedChannels bool `json:"include_archived_channels,omitempty"` + DisableSemanticSearch bool `json:"disable_semantic_search,omitempty"` +} + +// AssistantSearchContextMessage represents a search result message +type AssistantSearchContextMessage struct { + AuthorUserID string `json:"author_user_id"` + AuthorName string `json:"author_name,omitempty"` + TeamID string `json:"team_id"` + ChannelID string `json:"channel_id"` + ChannelName string `json:"channel_name,omitempty"` + MessageTS string `json:"message_ts"` + Content string `json:"content"` + IsAuthorBot bool `json:"is_author_bot"` + Permalink string `json:"permalink"` + Blocks Blocks `json:"blocks,omitempty"` + ContextMessages *AssistantSearchContextMessageContext `json:"context_messages,omitempty"` +} + +// AssistantSearchContextMessageContext contains context messages surrounding a search result +type AssistantSearchContextMessageContext struct { + Before []AssistantSearchContextMessage `json:"before"` + After []AssistantSearchContextMessage `json:"after"` +} + +// AssistantSearchContextFile represents a search result file +type AssistantSearchContextFile struct { + UploaderUserID string `json:"uploader_user_id"` + AuthorUserID string `json:"author_user_id"` + AuthorName string `json:"author_name"` + TeamID string `json:"team_id"` + FileID string `json:"file_id"` + DateCreated int64 `json:"date_created"` + DateUpdated int64 `json:"date_updated"` + Title string `json:"title"` + FileType string `json:"file_type"` + Permalink string `json:"permalink"` + Content string `json:"content"` +} + +// AssistantSearchContextChannel represents a search result channel +type AssistantSearchContextChannel struct { + TeamID string `json:"team_id"` + CreatorUserID string `json:"creator_user_id"` + CreatorName string `json:"creator_name"` + DateCreated int64 `json:"date_created"` + DateUpdated int64 `json:"date_updated"` + Name string `json:"name"` + Topic string `json:"topic"` + Purpose string `json:"purpose"` + Permalink string `json:"permalink"` +} + +// AssistantSearchContextResults contains the search results +type AssistantSearchContextResults struct { + Messages []AssistantSearchContextMessage `json:"messages,omitempty"` + Files []AssistantSearchContextFile `json:"files,omitempty"` + Channels []AssistantSearchContextChannel `json:"channels,omitempty"` +} + +// AssistantSearchContextResponse is the response from assistant.search.context +type AssistantSearchContextResponse struct { + SlackResponse + Results AssistantSearchContextResults `json:"results"` + ResponseMetadata struct { + NextCursor string `json:"next_cursor"` + } `json:"response_metadata"` +} + +// AssistantThreadSetSuggestedPrompts sets the suggested prompts for a thread +func (p *AssistantThreadsSetSuggestedPromptsParameters) AddPrompt(title, message string) { + p.Prompts = append(p.Prompts, AssistantThreadsPrompt{ + Title: title, + Message: message, + }) +} + +// SetAssistantThreadsSugesstedPrompts sets the suggested prompts for a thread +// @see https://api.slack.com/methods/assistant.threads.setSuggestedPrompts +func (api *Client) SetAssistantThreadsSuggestedPrompts(params AssistantThreadsSetSuggestedPromptsParameters) (err error) { + return api.SetAssistantThreadsSuggestedPromptsContext(context.Background(), params) +} + +// SetAssistantThreadSuggestedPromptsContext sets the suggested prompts for a thread with a custom context +// @see https://api.slack.com/methods/assistant.threads.setSuggestedPrompts +func (api *Client) SetAssistantThreadsSuggestedPromptsContext(ctx context.Context, params AssistantThreadsSetSuggestedPromptsParameters) (err error) { + + values := url.Values{ + "token": {api.token}, + } + + if params.ThreadTS != "" { + values.Add("thread_ts", params.ThreadTS) + } + + values.Add("channel_id", params.ChannelID) + + if params.Title != "" { + values.Add("title", params.Title) + } + + // Send Prompts as JSON + prompts, err := json.Marshal(params.Prompts) + if err != nil { + return err + } + + values.Add("prompts", string(prompts)) + + response := struct { + SlackResponse + }{} + + err = api.postMethod(ctx, "assistant.threads.setSuggestedPrompts", values, &response) + if err != nil { + return + } + + return response.Err() +} + +// SetAssistantThreadsStatus sets the status of a thread. +// This method accepts either the chat:write or assistant:write scope. +// Note: the assistant:write scope is being deprecated in favor of chat:write. +// @see https://api.slack.com/methods/assistant.threads.setStatus +func (api *Client) SetAssistantThreadsStatus(params AssistantThreadsSetStatusParameters) (err error) { + return api.SetAssistantThreadsStatusContext(context.Background(), params) +} + +// SetAssistantThreadsStatusContext sets the status of a thread with a custom context. +// This method accepts either the chat:write or assistant:write scope. +// Note: the assistant:write scope is being deprecated in favor of chat:write. +// @see https://api.slack.com/methods/assistant.threads.setStatus +func (api *Client) SetAssistantThreadsStatusContext(ctx context.Context, params AssistantThreadsSetStatusParameters) (err error) { + + values := url.Values{ + "token": {api.token}, + } + + if params.ThreadTS != "" { + values.Add("thread_ts", params.ThreadTS) + } + + values.Add("channel_id", params.ChannelID) + + // Always send the status parameter, if empty, it will clear any existing status + values.Add("status", params.Status) + + if len(params.LoadingMessages) > 0 { + values.Add("loading_messages", strings.Join(params.LoadingMessages, ",")) + } + + if params.Username != "" { + values.Add("username", params.Username) + } + + if params.IconURL != "" { + values.Add("icon_url", params.IconURL) + } + + if params.IconEmoji != "" { + values.Add("icon_emoji", params.IconEmoji) + } + + response := struct { + SlackResponse + }{} + + err = api.postMethod(ctx, "assistant.threads.setStatus", values, &response) + if err != nil { + return + } + + return response.Err() +} + +// SetAssistantThreadsTitle sets the title of a thread +// @see https://api.slack.com/methods/assistant.threads.setTitle +func (api *Client) SetAssistantThreadsTitle(params AssistantThreadsSetTitleParameters) (err error) { + return api.SetAssistantThreadsTitleContext(context.Background(), params) +} + +// SetAssistantThreadsTitleContext sets the title of a thread with a custom context +// @see https://api.slack.com/methods/assistant.threads.setTitle +func (api *Client) SetAssistantThreadsTitleContext(ctx context.Context, params AssistantThreadsSetTitleParameters) (err error) { + + values := url.Values{ + "token": {api.token}, + } + + if params.ChannelID != "" { + values.Add("channel_id", params.ChannelID) + } + + if params.ThreadTS != "" { + values.Add("thread_ts", params.ThreadTS) + } + + if params.Title != "" { + values.Add("title", params.Title) + } + + response := struct { + SlackResponse + }{} + + err = api.postMethod(ctx, "assistant.threads.setTitle", values, &response) + if err != nil { + return + } + + return response.Err() + +} + +// SearchAssistantContext searches messages across the Slack organization +// @see https://api.slack.com/methods/assistant.search.context +func (api *Client) SearchAssistantContext(params AssistantSearchContextParameters) (*AssistantSearchContextResponse, error) { + return api.SearchAssistantContextContext(context.Background(), params) +} + +// SearchAssistantContextContext searches messages across the Slack organization with a custom context +// @see https://api.slack.com/methods/assistant.search.context +func (api *Client) SearchAssistantContextContext(ctx context.Context, params AssistantSearchContextParameters) (*AssistantSearchContextResponse, error) { + values := url.Values{ + "token": {api.token}, + } + + values.Add("query", params.Query) + + if params.ActionToken != "" { + values.Add("action_token", params.ActionToken) + } + + if len(params.ChannelTypes) > 0 { + values.Add("channel_types", strings.Join(params.ChannelTypes, ",")) + } + + if len(params.ContentTypes) > 0 { + values.Add("content_types", strings.Join(params.ContentTypes, ",")) + } + + if params.ContextChannelID != "" { + values.Add("context_channel_id", params.ContextChannelID) + } + + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + + if params.IncludeBots { + values.Add("include_bots", "true") + } + + if params.Limit > 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + + if params.IncludeDeletedUsers { + values.Add("include_deleted_users", "true") + } + + if params.Before > 0 { + values.Add("before", strconv.FormatInt(params.Before, 10)) + } + + if params.After > 0 { + values.Add("after", strconv.FormatInt(params.After, 10)) + } + + if params.IncludeContextMessages { + values.Add("include_context_messages", "true") + } + + if params.Sort != "" { + values.Add("sort", params.Sort) + } + + if params.SortDir != "" { + values.Add("sort_dir", params.SortDir) + } + + if params.IncludeMessageBlocks { + values.Add("include_message_blocks", "true") + } + + if params.Highlight { + values.Add("highlight", "true") + } + + if len(params.TermClauses) > 0 { + values.Add("term_clauses", strings.Join(params.TermClauses, ",")) + } + + if params.Modifiers != "" { + values.Add("modifiers", params.Modifiers) + } + + if params.IncludeArchivedChannels { + values.Add("include_archived_channels", "true") + } + + if params.DisableSemanticSearch { + values.Add("disable_semantic_search", "true") + } + + response := &AssistantSearchContextResponse{} + + err := api.postMethod(ctx, "assistant.search.context", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/attachments.go b/vendor/github.com/slack-go/slack/attachments.go index f4eb9b932..be04e9049 100644 --- a/vendor/github.com/slack-go/slack/attachments.go +++ b/vendor/github.com/slack-go/slack/attachments.go @@ -77,8 +77,11 @@ type Attachment struct { Pretext string `json:"pretext,omitempty"` Text string `json:"text,omitempty"` - ImageURL string `json:"image_url,omitempty"` - ThumbURL string `json:"thumb_url,omitempty"` + ImageURL string `json:"image_url,omitempty"` + ImageBytes int `json:"image_bytes,omitempty"` + ImageHeight int `json:"image_height,omitempty"` + ImageWidth int `json:"image_width,omitempty"` + ThumbURL string `json:"thumb_url,omitempty"` ServiceName string `json:"service_name,omitempty"` ServiceIcon string `json:"service_icon,omitempty"` diff --git a/vendor/github.com/slack-go/slack/audit.go b/vendor/github.com/slack-go/slack/audit.go index a3ea7ebdf..135a68d78 100644 --- a/vendor/github.com/slack-go/slack/audit.go +++ b/vendor/github.com/slack-go/slack/audit.go @@ -107,7 +107,8 @@ type AuditLogParameters struct { func (api *Client) auditLogsRequest(ctx context.Context, path string, values url.Values) (*AuditLogResponse, error) { response := &AuditLogResponse{} - err := api.getMethod(ctx, path, api.token, values, response) + // The Audit Logs API uses a different base URL (api.slack.com instead of slack.com/api) + _, err := getResource(ctx, api.httpclient, api.auditEndpoint+path, api.token, values, response, api) if err != nil { return nil, err } diff --git a/vendor/github.com/slack-go/slack/auth.go b/vendor/github.com/slack-go/slack/auth.go index 7fff1a168..972f59ea6 100644 --- a/vendor/github.com/slack-go/slack/auth.go +++ b/vendor/github.com/slack-go/slack/auth.go @@ -3,6 +3,7 @@ package slack import ( "context" "net/url" + "strconv" ) // AuthRevokeResponse contains our Auth response from the auth.revoke endpoint @@ -47,8 +48,9 @@ type listTeamsResponse struct { } type ListTeamsParameters struct { - Limit int - Cursor string + Limit int + Cursor string + IncludeIcon *bool } // ListTeams returns all workspaces a token can access. @@ -66,6 +68,9 @@ func (api *Client) ListTeamsContext(ctx context.Context, params ListTeamsParamet 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) diff --git a/vendor/github.com/slack-go/slack/block.go b/vendor/github.com/slack-go/slack/block.go index a3fb1a0a7..cc5d80f24 100644 --- a/vendor/github.com/slack-go/slack/block.go +++ b/vendor/github.com/slack-go/slack/block.go @@ -1,31 +1,36 @@ package slack -// @NOTE: Blocks are in beta and subject to change. - -// More Information: https://api.slack.com/block-kit - // MessageBlockType defines a named string type to define each block type // as a constant for use within the package. type MessageBlockType string const ( - MBTSection MessageBlockType = "section" - MBTDivider MessageBlockType = "divider" - MBTImage MessageBlockType = "image" - MBTAction MessageBlockType = "actions" - MBTContext MessageBlockType = "context" - MBTFile MessageBlockType = "file" - MBTInput MessageBlockType = "input" - MBTHeader MessageBlockType = "header" - MBTRichText MessageBlockType = "rich_text" - MBTCall MessageBlockType = "call" - MBTVideo MessageBlockType = "video" + MBTSection MessageBlockType = "section" + MBTDivider MessageBlockType = "divider" + MBTImage MessageBlockType = "image" + MBTAction MessageBlockType = "actions" + MBTContext MessageBlockType = "context" + MBTContextActions MessageBlockType = "context_actions" + MBTFile MessageBlockType = "file" + MBTInput MessageBlockType = "input" + MBTHeader MessageBlockType = "header" + MBTRichText MessageBlockType = "rich_text" + MBTCall MessageBlockType = "call" + MBTVideo MessageBlockType = "video" + MBTMarkdown MessageBlockType = "markdown" + MBTTable MessageBlockType = "table" + MBTTaskCard MessageBlockType = "task_card" + MBTPlan MessageBlockType = "plan" + MBTAlert MessageBlockType = "alert" + MBTCard MessageBlockType = "card" + MBTCarousel MessageBlockType = "carousel" ) // Block defines an interface all block types should implement // to ensure consistency between blocks. type Block interface { BlockType() MessageBlockType + ID() string } // Blocks is a convenience struct defined to allow dynamic unmarshalling of @@ -54,12 +59,14 @@ type BlockAction struct { 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 diff --git a/vendor/github.com/slack-go/slack/block_action.go b/vendor/github.com/slack-go/slack/block_action.go index c15e4a3f7..819c0ef0d 100644 --- a/vendor/github.com/slack-go/slack/block_action.go +++ b/vendor/github.com/slack-go/slack/block_action.go @@ -14,6 +14,11 @@ 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{ diff --git a/vendor/github.com/slack-go/slack/block_alert.go b/vendor/github.com/slack-go/slack/block_alert.go new file mode 100644 index 000000000..ddb151b44 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_alert.go @@ -0,0 +1,70 @@ +package slack + +// AlertLevel defines the severity for an AlertBlock. +type AlertLevel string + +const ( + AlertLevelDefault AlertLevel = "default" + AlertLevelInfo AlertLevel = "info" + AlertLevelWarning AlertLevel = "warning" + AlertLevelError AlertLevel = "error" + AlertLevelSuccess AlertLevel = "success" +) + +// AlertBlock defines a block of type alert used to surface a notification +// message with an optional severity level. +// +// Surface: modal only. Slack rejects alert blocks sent via chat.postMessage +// or the streaming APIs — use OpenView / UpdateView / PushView with a +// ModalViewRequest whose Blocks include the alert. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/alert-block/ +type AlertBlock struct { + Type MessageBlockType `json:"type"` + Text *TextBlockObject `json:"text"` + Level AlertLevel `json:"level,omitempty"` + BlockID string `json:"block_id,omitempty"` +} + +// BlockType returns the type of the block +func (s AlertBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s AlertBlock) ID() string { + return s.BlockID +} + +// AlertBlockOption allows configuration of options for a new alert block +type AlertBlockOption func(*AlertBlock) + +// AlertBlockOptionLevel sets the severity level for the alert block +func AlertBlockOptionLevel(level AlertLevel) AlertBlockOption { + return func(block *AlertBlock) { + block.Level = level + } +} + +// AlertBlockOptionBlockID sets the block ID for the alert block +func AlertBlockOptionBlockID(blockID string) AlertBlockOption { + return func(block *AlertBlock) { + block.BlockID = blockID + } +} + +// NewAlertBlock returns a new instance of an alert block +func NewAlertBlock(text *TextBlockObject, options ...AlertBlockOption) *AlertBlock { + block := AlertBlock{ + Type: MBTAlert, + Text: text, + } + + for _, option := range options { + if option != nil { + option(&block) + } + } + + return &block +} diff --git a/vendor/github.com/slack-go/slack/block_call.go b/vendor/github.com/slack-go/slack/block_call.go index 98f2c0255..621a8b644 100644 --- a/vendor/github.com/slack-go/slack/block_call.go +++ b/vendor/github.com/slack-go/slack/block_call.go @@ -7,6 +7,55 @@ type CallBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` CallID string `json:"call_id"` + // Call is populated by Slack when retrieving messages containing a call block. + // When creating a call block to post, only CallID is required. + // Note: The structure differs from the Call type used in API responses. + Call *CallBlockData `json:"call,omitempty"` + APIDecorationAvailable bool `json:"api_decoration_available,omitempty"` +} + +// CallBlockData represents the call data structure as it appears in CallBlocks. +// This differs from the Call type used in API responses - CallBlock data is nested under V1. +type CallBlockData struct { + V1 *CallBlockDataV1 `json:"v1,omitempty"` + MediaBackendType string `json:"media_backend_type,omitempty"` +} + +// CallBlockDataV1 contains the actual call information within a CallBlock. +type CallBlockDataV1 struct { + ID string `json:"id"` + AppID string `json:"app_id,omitempty"` + AppIconURLs *CallBlockIconURLs `json:"app_icon_urls,omitempty"` + DateStart int64 `json:"date_start"` + DateEnd int64 `json:"date_end"` + ActiveParticipants []CallParticipant `json:"active_participants,omitempty"` + AllParticipants []CallParticipant `json:"all_participants,omitempty"` + DisplayID string `json:"display_id,omitempty"` + JoinURL string `json:"join_url,omitempty"` + DesktopAppJoinURL string `json:"desktop_app_join_url,omitempty"` + Name string `json:"name,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + Channels []string `json:"channels,omitempty"` + IsDMCall bool `json:"is_dm_call"` + WasRejected bool `json:"was_rejected"` + WasMissed bool `json:"was_missed"` + WasAccepted bool `json:"was_accepted"` + HasEnded bool `json:"has_ended"` +} + +// CallBlockIconURLs contains app icon URLs at various sizes for a call integration. +type CallBlockIconURLs struct { + Image32 string `json:"image_32,omitempty"` + Image36 string `json:"image_36,omitempty"` + Image48 string `json:"image_48,omitempty"` + Image64 string `json:"image_64,omitempty"` + Image72 string `json:"image_72,omitempty"` + Image96 string `json:"image_96,omitempty"` + Image128 string `json:"image_128,omitempty"` + Image192 string `json:"image_192,omitempty"` + Image512 string `json:"image_512,omitempty"` + Image1024 string `json:"image_1024,omitempty"` + ImageOriginal string `json:"image_original,omitempty"` } // BlockType returns the type of the block @@ -14,10 +63,31 @@ func (s CallBlock) BlockType() MessageBlockType { return s.Type } -// NewFileBlock returns a new instance of a file block -func NewCallBlock(callID string) *CallBlock { - return &CallBlock{ +// ID returns the ID of the block +func (s CallBlock) ID() string { + return s.BlockID +} + +// CallBlockOption allows configuration of options for a new call block +type CallBlockOption func(*CallBlock) + +// CallBlockOptionBlockID sets the block_id for the call block +func CallBlockOptionBlockID(blockID string) CallBlockOption { + return func(block *CallBlock) { + block.BlockID = blockID + } +} + +// NewCallBlock returns a new instance of a call block +func NewCallBlock(callID string, options ...CallBlockOption) *CallBlock { + block := &CallBlock{ Type: MBTCall, CallID: callID, } + + for _, option := range options { + option(block) + } + + return block } diff --git a/vendor/github.com/slack-go/slack/block_card.go b/vendor/github.com/slack-go/slack/block_card.go new file mode 100644 index 000000000..ccee17420 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_card.go @@ -0,0 +1,90 @@ +package slack + +// CardBlock defines a block of type card used to display a rich, self-contained +// piece of content with an optional hero image, icon, title, subtitle, body, +// and action buttons. Cards can stand alone or be grouped inside a +// CarouselBlock. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/card-block/ +type CardBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + HeroImage *ImageBlockElement `json:"hero_image,omitempty"` + Icon *ImageBlockElement `json:"icon,omitempty"` + Title *TextBlockObject `json:"title,omitempty"` + Subtitle *TextBlockObject `json:"subtitle,omitempty"` + Body *TextBlockObject `json:"body,omitempty"` + Actions *BlockElements `json:"actions,omitempty"` +} + +// BlockType returns the type of the block +func (s CardBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s CardBlock) ID() string { + return s.BlockID +} + +// CardBlockOption allows configuration of options for a new card block +type CardBlockOption func(*CardBlock) + +// CardBlockOptionBlockID sets the block ID for the card block +func CardBlockOptionBlockID(blockID string) CardBlockOption { + return func(block *CardBlock) { + block.BlockID = blockID + } +} + +// NewCardBlock returns a new instance of a card block. Use the chainable +// With* methods or provide options to populate its fields. +func NewCardBlock(options ...CardBlockOption) *CardBlock { + block := CardBlock{ + Type: MBTCard, + } + + for _, option := range options { + if option != nil { + option(&block) + } + } + + return &block +} + +// WithTitle sets the title text for the CardBlock +func (s *CardBlock) WithTitle(title *TextBlockObject) *CardBlock { + s.Title = title + return s +} + +// WithSubtitle sets the subtitle text for the CardBlock +func (s *CardBlock) WithSubtitle(subtitle *TextBlockObject) *CardBlock { + s.Subtitle = subtitle + return s +} + +// WithBody sets the body text for the CardBlock +func (s *CardBlock) WithBody(body *TextBlockObject) *CardBlock { + s.Body = body + return s +} + +// WithIcon sets the icon image for the CardBlock +func (s *CardBlock) WithIcon(icon *ImageBlockElement) *CardBlock { + s.Icon = icon + return s +} + +// WithHeroImage sets the hero image for the CardBlock +func (s *CardBlock) WithHeroImage(heroImage *ImageBlockElement) *CardBlock { + s.HeroImage = heroImage + return s +} + +// WithActions sets the action buttons displayed at the bottom of the card +func (s *CardBlock) WithActions(elements ...BlockElement) *CardBlock { + s.Actions = &BlockElements{ElementSet: elements} + return s +} diff --git a/vendor/github.com/slack-go/slack/block_carousel.go b/vendor/github.com/slack-go/slack/block_carousel.go new file mode 100644 index 000000000..5d407db9c --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_carousel.go @@ -0,0 +1,42 @@ +package slack + +// CarouselBlock defines a block of type carousel that displays a scrollable +// list of cards. A carousel must contain between 1 and 10 cards. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/carousel-block/ +type CarouselBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Elements []*CardBlock `json:"elements"` +} + +// BlockType returns the type of the block +func (s CarouselBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s CarouselBlock) ID() string { + return s.BlockID +} + +// NewCarouselBlock returns a new instance of a carousel block containing the +// given cards. +func NewCarouselBlock(cards ...*CardBlock) *CarouselBlock { + return &CarouselBlock{ + Type: MBTCarousel, + Elements: cards, + } +} + +// WithBlockID sets the block ID for the CarouselBlock +func (s *CarouselBlock) WithBlockID(blockID string) *CarouselBlock { + s.BlockID = blockID + return s +} + +// AddCard appends a card to the carousel +func (s *CarouselBlock) AddCard(card *CardBlock) *CarouselBlock { + s.Elements = append(s.Elements, card) + return s +} diff --git a/vendor/github.com/slack-go/slack/block_context.go b/vendor/github.com/slack-go/slack/block_context.go index 384fee22b..879ee61ee 100644 --- a/vendor/github.com/slack-go/slack/block_context.go +++ b/vendor/github.com/slack-go/slack/block_context.go @@ -15,6 +15,11 @@ func (s ContextBlock) BlockType() MessageBlockType { return s.Type } +// ID returns the ID of the block +func (s ContextBlock) ID() string { + return s.BlockID +} + type ContextElements struct { Elements []MixedElement } diff --git a/vendor/github.com/slack-go/slack/block_context_actions.go b/vendor/github.com/slack-go/slack/block_context_actions.go new file mode 100644 index 000000000..d1cf532c3 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_context_actions.go @@ -0,0 +1,31 @@ +package slack + +// ContextActionsBlock defines data that is used to hold interactive action elements. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/context-actions-block/ +type ContextActionsBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Elements *BlockElements `json:"elements"` +} + +// BlockType returns the type of the block +func (s ContextActionsBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s ContextActionsBlock) ID() string { + return s.BlockID +} + +// NewContextActionsBlock returns a new instance of a Context Actions Block +func NewContextActionsBlock(blockID string, elements ...BlockElement) *ContextActionsBlock { + return &ContextActionsBlock{ + Type: MBTContextActions, + BlockID: blockID, + Elements: &BlockElements{ + ElementSet: elements, + }, + } +} diff --git a/vendor/github.com/slack-go/slack/block_conv.go b/vendor/github.com/slack-go/slack/block_conv.go index 26c57bbbb..b83ad8312 100644 --- a/vendor/github.com/slack-go/slack/block_conv.go +++ b/vendor/github.com/slack-go/slack/block_conv.go @@ -53,6 +53,8 @@ func (b *Blocks) UnmarshalJSON(data []byte) error { block = &ActionBlock{} case "context": block = &ContextBlock{} + case "context_actions": + block = &ContextActionsBlock{} case "divider": block = &DividerBlock{} case "file": @@ -63,6 +65,8 @@ func (b *Blocks) UnmarshalJSON(data []byte) error { block = &ImageBlock{} case "input": block = &InputBlock{} + case "markdown": + block = &MarkdownBlock{} case "rich_text": block = &RichTextBlock{} case "rich_text_input": @@ -73,8 +77,25 @@ func (b *Blocks) UnmarshalJSON(data []byte) error { block = &CallBlock{} case "video": block = &VideoBlock{} + case "table": + block = &TableBlock{} + case "task_card": + block = &TaskCardBlock{} + case "alert": + block = &AlertBlock{} + case "plan": + block = &PlanBlock{} + case "card": + block = &CardBlock{} + case "carousel": + block = &CarouselBlock{} default: - block = &UnknownBlock{} + b := &UnknownBlock{raw: r} + if err = json.Unmarshal(r, b); err != nil { + return err + } + blocks.BlockSet = append(blocks.BlockSet, b) + continue } err = json.Unmarshal(r, block) @@ -139,6 +160,12 @@ func (b *InputBlock) UnmarshalJSON(data []byte) error { e = &NumberInputBlockElement{} case "file_input": e = &FileInputBlockElement{} + case "feedback_buttons": + e = &FeedbackButtonsBlockElement{} + case "icon_button": + e = &IconButtonBlockElement{} + case "workflow_button": + e = &WorkflowButtonBlockElement{} default: return fmt.Errorf("unsupported block element type %v", s.TypeVal) } @@ -217,8 +244,18 @@ func (b *BlockElements) UnmarshalJSON(data []byte) error { blockElement = &RadioButtonsBlockElement{} case "static_select", "external_select", "users_select", "conversations_select", "channels_select": blockElement = &SelectBlockElement{} + case "multi_static_select", "multi_external_select", "multi_users_select", "multi_conversations_select", "multi_channels_select": + blockElement = &MultiSelectBlockElement{} case "number_input": blockElement = &NumberInputBlockElement{} + case "file_input": + blockElement = &FileInputBlockElement{} + case "feedback_buttons": + blockElement = &FeedbackButtonsBlockElement{} + case "icon_button": + blockElement = &IconButtonBlockElement{} + case "workflow_button": + blockElement = &WorkflowButtonBlockElement{} default: return fmt.Errorf("unsupported block element type %v", blockElementType) } @@ -339,6 +376,12 @@ func (a *Accessory) UnmarshalJSON(data []byte) error { return err } a.CheckboxGroupsBlockElement = element.(*CheckboxGroupsBlockElement) + case "workflow_button": + element, err := unmarshalBlockElement(r, &WorkflowButtonBlockElement{}) + if err != nil { + return err + } + a.WorkflowButtonElement = element.(*WorkflowButtonBlockElement) default: element, err := unmarshalBlockElement(r, &UnknownBlockElement{}) if err != nil { @@ -389,6 +432,12 @@ func toBlockElement(element *Accessory) BlockElement { if element.MultiSelectElement != nil { return element.MultiSelectElement } + if element.RichTextInputElement != nil { + return element.RichTextInputElement + } + if element.WorkflowButtonElement != nil { + return element.WorkflowButtonElement + } return nil } diff --git a/vendor/github.com/slack-go/slack/block_divider.go b/vendor/github.com/slack-go/slack/block_divider.go index 2d442ba11..e10d7b053 100644 --- a/vendor/github.com/slack-go/slack/block_divider.go +++ b/vendor/github.com/slack-go/slack/block_divider.go @@ -13,10 +13,14 @@ func (s DividerBlock) BlockType() MessageBlockType { return s.Type } +// ID returns the ID of the block +func (s DividerBlock) ID() string { + return s.BlockID +} + // NewDividerBlock returns a new instance of a divider block func NewDividerBlock() *DividerBlock { return &DividerBlock{ Type: MBTDivider, } - } diff --git a/vendor/github.com/slack-go/slack/block_element.go b/vendor/github.com/slack-go/slack/block_element.go index ad3b67006..128493407 100644 --- a/vendor/github.com/slack-go/slack/block_element.go +++ b/vendor/github.com/slack-go/slack/block_element.go @@ -3,20 +3,23 @@ package slack // https://api.slack.com/reference/messaging/block-elements const ( - METCheckboxGroups MessageElementType = "checkboxes" - METImage MessageElementType = "image" - METButton MessageElementType = "button" - METOverflow MessageElementType = "overflow" - METDatepicker MessageElementType = "datepicker" - METTimepicker MessageElementType = "timepicker" - METDatetimepicker MessageElementType = "datetimepicker" - METPlainTextInput MessageElementType = "plain_text_input" - METRadioButtons MessageElementType = "radio_buttons" - METRichTextInput MessageElementType = "rich_text_input" - METEmailTextInput MessageElementType = "email_text_input" - METURLTextInput MessageElementType = "url_text_input" - METNumber MessageElementType = "number_input" - METFileInput MessageElementType = "file_input" + METCheckboxGroups MessageElementType = "checkboxes" + METImage MessageElementType = "image" + METButton MessageElementType = "button" + METOverflow MessageElementType = "overflow" + METDatepicker MessageElementType = "datepicker" + METTimepicker MessageElementType = "timepicker" + METDatetimepicker MessageElementType = "datetimepicker" + METPlainTextInput MessageElementType = "plain_text_input" + METRadioButtons MessageElementType = "radio_buttons" + METRichTextInput MessageElementType = "rich_text_input" + METEmailTextInput MessageElementType = "email_text_input" + METURLTextInput MessageElementType = "url_text_input" + METNumber MessageElementType = "number_input" + METFileInput MessageElementType = "file_input" + METFeedbackButtons MessageElementType = "feedback_buttons" + METIconButton MessageElementType = "icon_button" + METWorkflowButton MessageElementType = "workflow_button" MixedElementImage MixedElementType = "mixed_image" MixedElementText MixedElementType = "mixed_text" @@ -58,34 +61,37 @@ type Accessory struct { SelectElement *SelectBlockElement MultiSelectElement *MultiSelectBlockElement CheckboxGroupsBlockElement *CheckboxGroupsBlockElement + WorkflowButtonElement *WorkflowButtonBlockElement UnknownElement *UnknownBlockElement } // NewAccessory returns a new Accessory for a given block element func NewAccessory(element BlockElement) *Accessory { - switch element.(type) { + switch element := element.(type) { case *ImageBlockElement: - return &Accessory{ImageElement: element.(*ImageBlockElement)} + return &Accessory{ImageElement: element} case *ButtonBlockElement: - return &Accessory{ButtonElement: element.(*ButtonBlockElement)} + return &Accessory{ButtonElement: element} case *OverflowBlockElement: - return &Accessory{OverflowElement: element.(*OverflowBlockElement)} + return &Accessory{OverflowElement: element} case *DatePickerBlockElement: - return &Accessory{DatePickerElement: element.(*DatePickerBlockElement)} + return &Accessory{DatePickerElement: element} case *TimePickerBlockElement: - return &Accessory{TimePickerElement: element.(*TimePickerBlockElement)} + return &Accessory{TimePickerElement: element} case *PlainTextInputBlockElement: - return &Accessory{PlainTextInputElement: element.(*PlainTextInputBlockElement)} + return &Accessory{PlainTextInputElement: element} case *RichTextInputBlockElement: - return &Accessory{RichTextInputElement: element.(*RichTextInputBlockElement)} + return &Accessory{RichTextInputElement: element} case *RadioButtonsBlockElement: - return &Accessory{RadioButtonsElement: element.(*RadioButtonsBlockElement)} + return &Accessory{RadioButtonsElement: element} case *SelectBlockElement: - return &Accessory{SelectElement: element.(*SelectBlockElement)} + return &Accessory{SelectElement: element} case *MultiSelectBlockElement: - return &Accessory{MultiSelectElement: element.(*MultiSelectBlockElement)} + return &Accessory{MultiSelectElement: element} case *CheckboxGroupsBlockElement: - return &Accessory{CheckboxGroupsBlockElement: element.(*CheckboxGroupsBlockElement)} + return &Accessory{CheckboxGroupsBlockElement: element} + case *WorkflowButtonBlockElement: + return &Accessory{WorkflowButtonElement: element} default: return &Accessory{UnknownElement: element.(*UnknownBlockElement)} } @@ -117,9 +123,10 @@ func (s UnknownBlockElement) ElementType() MessageElementType { // // More Information: https://api.slack.com/reference/messaging/block-elements#image type ImageBlockElement struct { - Type MessageElementType `json:"type"` - ImageURL string `json:"image_url"` - AltText string `json:"alt_text"` + Type MessageElementType `json:"type"` + ImageURL *string `json:"image_url,omitempty"` + AltText string `json:"alt_text"` + SlackFile *SlackFileObject `json:"slack_file,omitempty"` } // ElementType returns the type of the Element @@ -135,11 +142,21 @@ func (s ImageBlockElement) MixedElementType() MixedElementType { func NewImageBlockElement(imageURL, altText string) *ImageBlockElement { return &ImageBlockElement{ Type: METImage, - ImageURL: imageURL, + ImageURL: &imageURL, AltText: altText, } } +// NewImageBlockElementSlackFile returns a new instance of an image block element +// TODO: BREAKING CHANGE - This should be combined with the function above +func NewImageBlockElementSlackFile(slackFile *SlackFileObject, altText string) *ImageBlockElement { + return &ImageBlockElement{ + Type: METImage, + SlackFile: slackFile, + AltText: altText, + } +} + // Style is a style of Button element // https://api.slack.com/reference/block-kit/block-elements#button__fields type Style string @@ -231,6 +248,7 @@ type SelectBlockElement struct { Filter *SelectBlockElementFilter `json:"filter,omitempty"` MinQueryLength *int `json:"min_query_length,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // SelectBlockElementFilter allows to filter select element conversation options by type. @@ -318,9 +336,11 @@ type MultiSelectBlockElement struct { InitialUsers []string `json:"initial_users,omitempty"` InitialConversations []string `json:"initial_conversations,omitempty"` InitialChannels []string `json:"initial_channels,omitempty"` + Filter *SelectBlockElementFilter `json:"filter,omitempty"` MinQueryLength *int `json:"min_query_length,omitempty"` MaxSelectedItems *int `json:"max_selected_items,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -441,6 +461,7 @@ type DatePickerBlockElement struct { Placeholder *TextBlockObject `json:"placeholder,omitempty"` InitialDate string `json:"initial_date,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -467,6 +488,8 @@ type TimePickerBlockElement struct { Placeholder *TextBlockObject `json:"placeholder,omitempty"` InitialTime string `json:"initial_time,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + Timezone string `json:"timezone,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -490,6 +513,7 @@ type DateTimePickerBlockElement struct { 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 @@ -577,6 +601,7 @@ type PlainTextInputBlockElement struct { MinLength int `json:"min_length,omitempty"` MaxLength int `json:"max_length,omitempty"` DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } type DispatchActionConfig struct { @@ -664,6 +689,7 @@ type CheckboxGroupsBlockElement struct { Options []*OptionBlockObject `json:"options"` InitialOptions []*OptionBlockObject `json:"initial_options,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -690,6 +716,7 @@ type RadioButtonsBlockElement struct { Options []*OptionBlockObject `json:"options"` InitialOption *OptionBlockObject `json:"initial_option,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -720,6 +747,7 @@ type NumberInputBlockElement struct { MinValue string `json:"min_value,omitempty"` MaxValue string `json:"max_value,omitempty"` DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -797,3 +825,170 @@ func (s *FileInputBlockElement) WithMaxFiles(maxFiles int) *FileInputBlockElemen s.MaxFiles = maxFiles return s } + +// FeedbackButton defines a button within a feedback buttons element +type FeedbackButton struct { + Text *TextBlockObject `json:"text"` + Value string `json:"value"` + AccessibilityLabel string `json:"accessibility_label,omitempty"` +} + +// NewFeedbackButton returns a new instance of a feedback button +func NewFeedbackButton(text *TextBlockObject, value string) *FeedbackButton { + return &FeedbackButton{ + Text: text, + Value: value, + } +} + +// WithAccessibilityLabel sets the accessibility label for the feedback button +func (fb *FeedbackButton) WithAccessibilityLabel(label string) *FeedbackButton { + fb.AccessibilityLabel = label + return fb +} + +// FeedbackButtonsBlockElement defines an element that provides positive/negative feedback options +// +// More Information: https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element +type FeedbackButtonsBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + PositiveButton *FeedbackButton `json:"positive_button"` + NegativeButton *FeedbackButton `json:"negative_button"` +} + +// ElementType returns the type of the element +func (s FeedbackButtonsBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewFeedbackButtonsBlockElement returns a new instance of a feedback buttons element +func NewFeedbackButtonsBlockElement(actionID string, positiveButton, negativeButton *FeedbackButton) *FeedbackButtonsBlockElement { + return &FeedbackButtonsBlockElement{ + Type: METFeedbackButtons, + ActionID: actionID, + PositiveButton: positiveButton, + NegativeButton: negativeButton, + } +} + +// WithPositiveButton sets the positive button for the feedback buttons element +func (s *FeedbackButtonsBlockElement) WithPositiveButton(button *FeedbackButton) *FeedbackButtonsBlockElement { + s.PositiveButton = button + return s +} + +// WithNegativeButton sets the negative button for the feedback buttons element +func (s *FeedbackButtonsBlockElement) WithNegativeButton(button *FeedbackButton) *FeedbackButtonsBlockElement { + s.NegativeButton = button + return s +} + +// IconButtonBlockElement defines an element that displays icon-based interactive buttons +// +// More Information: https://docs.slack.dev/reference/block-kit/block-elements/icon-button-element +type IconButtonBlockElement struct { + Type MessageElementType `json:"type"` + Icon string `json:"icon"` + Text *TextBlockObject `json:"text"` + ActionID string `json:"action_id,omitempty"` + Value string `json:"value,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + AccessibilityLabel string `json:"accessibility_label,omitempty"` + VisibleToUserIDs []string `json:"visible_to_user_ids,omitempty"` +} + +// ElementType returns the type of the element +func (s IconButtonBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewIconButtonBlockElement returns a new instance of an icon button element +func NewIconButtonBlockElement(icon string, text *TextBlockObject, actionID string) *IconButtonBlockElement { + return &IconButtonBlockElement{ + Type: METIconButton, + Icon: icon, + Text: text, + ActionID: actionID, + } +} + +// WithValue sets the value for the icon button element +func (s *IconButtonBlockElement) WithValue(value string) *IconButtonBlockElement { + s.Value = value + return s +} + +// WithConfirm sets the confirmation dialog for the icon button element +func (s *IconButtonBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *IconButtonBlockElement { + s.Confirm = confirm + return s +} + +// WithAccessibilityLabel sets the accessibility label for the icon button element +func (s *IconButtonBlockElement) WithAccessibilityLabel(label string) *IconButtonBlockElement { + s.AccessibilityLabel = label + return s +} + +// WithVisibleToUserIDs sets the user IDs who can see the icon button element +func (s *IconButtonBlockElement) WithVisibleToUserIDs(userIDs []string) *IconButtonBlockElement { + s.VisibleToUserIDs = userIDs + return s +} + +// WorkflowTrigger defines the workflow to be executed when a workflow button is clicked +type WorkflowTrigger struct { + URL string `json:"url"` + CustomizableInputParameters []CustomizableInputParameter `json:"customizable_input_parameters,omitempty"` +} + +// CustomizableInputParameter defines a parameter that can be passed to a workflow +type CustomizableInputParameter struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// Workflow contains the trigger details for a workflow button +type Workflow struct { + Trigger *WorkflowTrigger `json:"trigger"` +} + +// WorkflowButtonBlockElement defines an element that triggers a workflow when clicked +// +// More Information: https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element +type WorkflowButtonBlockElement struct { + Type MessageElementType `json:"type"` + Text *TextBlockObject `json:"text"` + Workflow *Workflow `json:"workflow"` + ActionID string `json:"action_id"` + Style Style `json:"style,omitempty"` + AccessibilityLabel string `json:"accessibility_label,omitempty"` +} + +// ElementType returns the type of the element +func (s WorkflowButtonBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewWorkflowButtonBlockElement returns a new instance of a workflow button element +func NewWorkflowButtonBlockElement(text *TextBlockObject, workflow *Workflow, actionID string) *WorkflowButtonBlockElement { + return &WorkflowButtonBlockElement{ + Type: METWorkflowButton, + Text: text, + Workflow: workflow, + ActionID: actionID, + } +} + +// WithStyle sets the style for the workflow button element +func (s *WorkflowButtonBlockElement) WithStyle(style Style) *WorkflowButtonBlockElement { + s.Style = style + return s +} + +// WithAccessibilityLabel sets the accessibility label for the workflow button element +func (s *WorkflowButtonBlockElement) WithAccessibilityLabel(label string) *WorkflowButtonBlockElement { + s.AccessibilityLabel = label + return s +} diff --git a/vendor/github.com/slack-go/slack/block_file.go b/vendor/github.com/slack-go/slack/block_file.go index ac4453f79..2f669b02a 100644 --- a/vendor/github.com/slack-go/slack/block_file.go +++ b/vendor/github.com/slack-go/slack/block_file.go @@ -15,6 +15,11 @@ func (s FileBlock) BlockType() MessageBlockType { return s.Type } +// ID returns the ID of the block +func (s FileBlock) ID() string { + return s.BlockID +} + // NewFileBlock returns a new instance of a file block func NewFileBlock(blockID string, externalID string, source string) *FileBlock { return &FileBlock{ diff --git a/vendor/github.com/slack-go/slack/block_header.go b/vendor/github.com/slack-go/slack/block_header.go index 6dff4b883..4277057f7 100644 --- a/vendor/github.com/slack-go/slack/block_header.go +++ b/vendor/github.com/slack-go/slack/block_header.go @@ -14,6 +14,11 @@ 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) @@ -31,7 +36,9 @@ func NewHeaderBlock(textObj *TextBlockObject, options ...HeaderBlockOption) *Hea } for _, option := range options { - option(&block) + if option != nil { + option(&block) + } } return &block diff --git a/vendor/github.com/slack-go/slack/block_image.go b/vendor/github.com/slack-go/slack/block_image.go index b3d2cb8cf..2a914e5a0 100644 --- a/vendor/github.com/slack-go/slack/block_image.go +++ b/vendor/github.com/slack-go/slack/block_image.go @@ -12,6 +12,11 @@ type ImageBlock struct { 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. // @@ -36,3 +41,15 @@ func NewImageBlock(imageURL, altText, blockID string, title *TextBlockObject) *I Title: title, } } + +// NewImageBlockSlackFile returns an instance of a new Image Block type +// TODO: BREAKING CHANGE - This should be combined with the function above +func NewImageBlockSlackFile(slackFile *SlackFileObject, altText string, blockID string, title *TextBlockObject) *ImageBlock { + return &ImageBlock{ + Type: MBTImage, + SlackFile: slackFile, + AltText: altText, + BlockID: blockID, + Title: title, + } +} diff --git a/vendor/github.com/slack-go/slack/block_input.go b/vendor/github.com/slack-go/slack/block_input.go index 7c1272a64..f74eda6d2 100644 --- a/vendor/github.com/slack-go/slack/block_input.go +++ b/vendor/github.com/slack-go/slack/block_input.go @@ -18,6 +18,11 @@ 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{ diff --git a/vendor/github.com/slack-go/slack/block_json.go b/vendor/github.com/slack-go/slack/block_json.go new file mode 100644 index 000000000..43798f51e --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_json.go @@ -0,0 +1,110 @@ +package slack + +import ( + "encoding/json" + "fmt" +) + +// RawJSONBlock represents a block created from raw JSON that preserves +// the original JSON structure. This is useful for testing new Slack block types +// before the library has full support, or for using blocks copied from Block Kit Builder. +// +// The block stores the original JSON and outputs it unchanged during marshalling, +// ensuring no data is lost through the unmarshal/marshal cycle. +type RawJSONBlock struct { + Type MessageBlockType `json:"-"` + BlockID string `json:"-"` + raw json.RawMessage +} + +// BlockType returns the type of the block +func (r RawJSONBlock) BlockType() MessageBlockType { + return r.Type +} + +// ID returns the block_id of the block +func (r RawJSONBlock) ID() string { + return r.BlockID +} + +// MarshalJSON outputs the original JSON unchanged +func (r RawJSONBlock) MarshalJSON() ([]byte, error) { + return r.raw, nil +} + +// BlockFromJSON creates a RawJSONBlock from a JSON string that preserves +// the original JSON. This is useful for quickly testing blocks from Slack's +// Block Kit Builder or for incorporating new block types before the library +// has full support. +// +// The JSON can be either a single block object or an array of blocks. +// If an array is provided, only the first block is returned. +// +// The returned block stores the original JSON and outputs it unchanged during +// marshalling, ensuring no data is lost. +// +// Returns an error if the JSON is invalid, empty, or missing required fields. +// +// Example: +// +// block, err := slack.BlockFromJSON(`{"type": "section", "text": {"type": "mrkdwn", "text": "Hello"}}`) +// if err != nil { +// return err +// } +// blocks = append(blocks, block) +func BlockFromJSON(jsonStr string) (Block, error) { + var rawJSON json.RawMessage + var isArray bool + + // Try to unmarshal as an array first + var arrayTest []json.RawMessage + if err := json.Unmarshal([]byte(jsonStr), &arrayTest); err == nil && len(arrayTest) > 0 { + rawJSON = arrayTest[0] + isArray = true + } else { + // Try as a single block object + if err := json.Unmarshal([]byte(jsonStr), &rawJSON); err != nil { + return nil, fmt.Errorf("failed to unmarshal block JSON: %w", err) + } + isArray = false + } + + if !isArray && len(rawJSON) == 0 { + return nil, fmt.Errorf("no blocks found in JSON") + } + + // Extract minimal fields for Block interface + var minimal struct { + Type string `json:"type"` + BlockID string `json:"block_id"` + } + if err := json.Unmarshal(rawJSON, &minimal); err != nil { + return nil, fmt.Errorf("failed to extract block type: %w", err) + } + + if minimal.Type == "" { + return nil, fmt.Errorf("block missing required 'type' field") + } + + return RawJSONBlock{ + Type: MessageBlockType(minimal.Type), + BlockID: minimal.BlockID, + raw: rawJSON, + }, nil +} + +// MustBlockFromJSON creates a Block from a JSON string and panics if there's an error. +// This is primarily intended for use in tests or examples where the JSON is known to be valid. +// For production code, use BlockFromJSON which returns an error instead. +// +// Example: +// +// block := slack.MustBlockFromJSON(`{"type": "divider"}`) +// msg := slack.NewBlockMessage(block) +func MustBlockFromJSON(jsonStr string) Block { + block, err := BlockFromJSON(jsonStr) + if err != nil { + panic(fmt.Sprintf("MustBlockFromJSON: %v", err)) + } + return block +} diff --git a/vendor/github.com/slack-go/slack/block_markdown.go b/vendor/github.com/slack-go/slack/block_markdown.go new file mode 100644 index 000000000..e22a0d16d --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_markdown.go @@ -0,0 +1,34 @@ +package slack + +// MarkdownBlock defines a block that lets you use markdown to format your text. +// +// This block can be used with AI apps when you expect a markdown response from an LLM +// that can get lost in translation rendering in Slack. Providing it in a markdown block +// leaves the translating to Slack to ensure your message appears as intended. Note that +// passing a single block may result in multiple blocks after translation. +// +// More Information: https://api.slack.com/reference/block-kit/blocks#markdown +type MarkdownBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Text string `json:"text"` +} + +// BlockType returns the type of the block +func (s MarkdownBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s MarkdownBlock) ID() string { + return s.BlockID +} + +// NewMarkdownBlock returns an instance of a new Markdown Block type +func NewMarkdownBlock(blockID, text string) *MarkdownBlock { + return &MarkdownBlock{ + Type: MBTMarkdown, + BlockID: blockID, + Text: text, + } +} diff --git a/vendor/github.com/slack-go/slack/block_object.go b/vendor/github.com/slack-go/slack/block_object.go index a3e78d4ea..c1b64cc4c 100644 --- a/vendor/github.com/slack-go/slack/block_object.go +++ b/vendor/github.com/slack-go/slack/block_object.go @@ -11,7 +11,6 @@ import ( // BlockObject defines an interface that all block object types should // implement. -// @TODO: Is this interface needed? // blockObject object types const ( @@ -122,7 +121,7 @@ func unmarshalBlockObject(r json.RawMessage, object blockObject) (blockObject, e type TextBlockObject struct { Type string `json:"type"` Text string `json:"text"` - Emoji bool `json:"emoji,omitempty"` + Emoji *bool `json:"emoji,omitempty"` Verbatim bool `json:"verbatim,omitempty"` } @@ -142,9 +141,8 @@ func (s TextBlockObject) Validate() error { return errors.New("type must be either of plain_text or mrkdwn") } - // https://github.com/slack-go/slack/issues/881 - if s.Type == "mrkdwn" && s.Emoji { - return errors.New("emoji cannot be true in mrkdown") + 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 @@ -161,11 +159,28 @@ func (s TextBlockObject) Validate() error { } // NewTextBlockObject returns an instance of a new Text Block Object -func NewTextBlockObject(elementType, text string, emoji, verbatim bool) *TextBlockObject { +// +// If you want to create a mrkdwn object, you should set the emoji parameter to false. The +// reason is that Slack doesn't accept emoji in mrkdwn. +func NewTextBlockObject(elementType, text string, emoji bool, verbatim bool) *TextBlockObject { + // If we're trying to build a mrkdwn object, we can't send emoji at all. I think the + // right approach here is to be a bit clever, and not break the function interface. + // + // So, here's the plan: + // 1. If the type is mrkdwn, set emoji to nil, regardless of what the user passed in + // 2. Else, set emoji to the value passed in + var emojiPtr *bool + + if elementType == "mrkdwn" { + emojiPtr = nil + } else { + emojiPtr = &emoji + } + return &TextBlockObject{ Type: elementType, Text: text, - Emoji: emoji, + Emoji: emojiPtr, Verbatim: verbatim, } } diff --git a/vendor/github.com/slack-go/slack/block_plan.go b/vendor/github.com/slack-go/slack/block_plan.go new file mode 100644 index 000000000..c6e417490 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_plan.go @@ -0,0 +1,55 @@ +package slack + +// PlanBlock defines a block of type plan used by AI agents +// to group multiple task cards under a shared title. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/plan-block/ +type PlanBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Title string `json:"title"` + Tasks []TaskCardBlock `json:"tasks,omitempty"` +} + +// BlockType returns the type of the block +func (s PlanBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s PlanBlock) ID() string { + return s.BlockID +} + +// PlanBlockOption allows configuration of options for a new plan block +type PlanBlockOption func(*PlanBlock) + +// PlanBlockOptionBlockID sets the block ID for the plan block +func PlanBlockOptionBlockID(blockID string) PlanBlockOption { + return func(block *PlanBlock) { + block.BlockID = blockID + } +} + +// NewPlanBlock returns a new instance of a plan block +func NewPlanBlock(title string, options ...PlanBlockOption) *PlanBlock { + block := PlanBlock{ + Type: MBTPlan, + Title: title, + } + + for _, option := range options { + option(&block) + } + + return &block +} + +// WithTasks sets the tasks for the PlanBlock +func (s *PlanBlock) WithTasks(tasks ...*TaskCardBlock) *PlanBlock { + s.Tasks = make([]TaskCardBlock, len(tasks)) + for i, t := range tasks { + s.Tasks[i] = *t + } + return s +} diff --git a/vendor/github.com/slack-go/slack/block_rich_text.go b/vendor/github.com/slack-go/slack/block_rich_text.go index c6eb0b1ba..3b0ead2ee 100644 --- a/vendor/github.com/slack-go/slack/block_rich_text.go +++ b/vendor/github.com/slack-go/slack/block_rich_text.go @@ -16,6 +16,11 @@ 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"` @@ -98,6 +103,10 @@ func (u RichTextUnknown) RichTextElementType() RichTextElementType { return u.Type } +func (u RichTextUnknown) MarshalJSON() ([]byte, error) { + return []byte(u.Raw), nil +} + type RichTextListElementType string const ( @@ -110,6 +119,8 @@ type RichTextList struct { 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. @@ -122,7 +133,7 @@ func NewRichTextList(style RichTextListElementType, indent int, elements ...Rich } } -// ElementType returns the type of the Element +// RichTextElementType returns the type of the Element func (s RichTextList) RichTextElementType() RichTextElementType { return s.Type } @@ -132,6 +143,8 @@ func (e *RichTextList) UnmarshalJSON(b []byte) error { 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 @@ -174,6 +187,8 @@ func (e *RichTextList) UnmarshalJSON(b []byte) error { Elements: elems, Style: raw.Style, Indent: raw.Indent, + Border: raw.Border, + Offset: raw.Offset, } return nil } @@ -190,7 +205,8 @@ func (s RichTextSection) RichTextElementType() RichTextElementType { func (e *RichTextSection) UnmarshalJSON(b []byte) error { var raw struct { - RawElements []json.RawMessage `json:"elements"` + RawElements []json.RawMessage `json:"elements"` + Type RichTextElementType `json:"type"` } if string(b) == "{}" { return nil @@ -240,14 +256,19 @@ func (e *RichTextSection) UnmarshalJSON(b []byte) error { } elems = append(elems, elem) } + if raw.Type == "" { + raw.Type = RTESection + } *e = RichTextSection{ - Type: RTESection, + Type: raw.Type, Elements: elems, } return nil } -// NewRichTextSectionBlockElement . +// NewRichTextSection creates a new rich text section from the provided elements. The +// section type will default to "rich_text_section", as it's the only currently supported +// section type. func NewRichTextSection(elements ...RichTextSectionElement) *RichTextSection { return &RichTextSection{ Type: RTESection, @@ -277,10 +298,14 @@ type RichTextSectionElement interface { } type RichTextSectionTextStyle struct { - Bold bool `json:"bold,omitempty"` - Italic bool `json:"italic,omitempty"` - Strike bool `json:"strike,omitempty"` - Code bool `json:"code,omitempty"` + Bold bool `json:"bold,omitempty"` + Italic bool `json:"italic,omitempty"` + Strike bool `json:"strike,omitempty"` + Code bool `json:"code,omitempty"` + Underline bool `json:"underline,omitempty"` + Highlight bool `json:"highlight,omitempty"` + ClientHighlight bool `json:"client_highlight,omitempty"` + Unlink bool `json:"unlink,omitempty"` } type RichTextSectionTextElement struct { @@ -313,7 +338,7 @@ func (r RichTextSectionChannelElement) RichTextSectionElementType() RichTextSect func NewRichTextSectionChannelElement(channelID string, style *RichTextSectionTextStyle) *RichTextSectionChannelElement { return &RichTextSectionChannelElement{ - Type: RTSEText, + Type: RTSEChannel, ChannelID: channelID, Style: style, } @@ -340,7 +365,7 @@ func NewRichTextSectionUserElement(userID string, style *RichTextSectionTextStyl type RichTextSectionEmojiElement struct { Type RichTextSectionElementType `json:"type"` Name string `json:"name"` - SkinTone int `json:"skin_tone"` + SkinTone int `json:"skin_tone,omitempty"` Unicode string `json:"unicode,omitempty"` Style *RichTextSectionTextStyle `json:"style,omitempty"` } @@ -361,7 +386,7 @@ func NewRichTextSectionEmojiElement(name string, skinTone int, style *RichTextSe type RichTextSectionLinkElement struct { Type RichTextSectionElementType `json:"type"` URL string `json:"url"` - Text string `json:"text"` + Text string `json:"text,omitempty"` Style *RichTextSectionTextStyle `json:"style,omitempty"` } @@ -399,6 +424,7 @@ func NewRichTextSectionTeamElement(teamID string, style *RichTextSectionTextStyl type RichTextSectionUserGroupElement struct { Type RichTextSectionElementType `json:"type"` UsergroupID string `json:"usergroup_id"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` } func (r RichTextSectionUserGroupElement) RichTextSectionElementType() RichTextSectionElementType { @@ -475,8 +501,16 @@ func (r RichTextSectionUnknownElement) RichTextSectionElementType() RichTextSect return r.Type } +func (r RichTextSectionUnknownElement) MarshalJSON() ([]byte, error) { + return []byte(r.Raw), nil +} + // RichTextQuote represents rich_text_quote element type. -type RichTextQuote RichTextSection +type RichTextQuote struct { + Type RichTextElementType `json:"type"` + Elements []RichTextSectionElement `json:"elements"` + Border int `json:"border,omitempty"` +} // RichTextElementType returns the type of the Element func (s *RichTextQuote) RichTextElementType() RichTextElementType { @@ -489,15 +523,26 @@ func (s *RichTextQuote) UnmarshalJSON(b []byte) error { if err := json.Unmarshal(b, &rts); err != nil { return err } - *s = RichTextQuote(rts) - s.Type = RTEQuote + var standalone struct { + Border int `json:"border"` + } + if err := json.Unmarshal(b, &standalone); err != nil { + return err + } + *s = RichTextQuote{ + Type: RTEQuote, + Elements: rts.Elements, + Border: standalone.Border, + } return nil } // RichTextPreformatted represents rich_text_quote element type. type RichTextPreformatted struct { - RichTextSection - Border int `json:"border"` + Type RichTextElementType `json:"type"` + Elements []RichTextSectionElement `json:"elements"` + Border int `json:"border"` + Language string `json:"language,omitempty"` } // RichTextElementType returns the type of the Element @@ -521,15 +566,17 @@ func (s *RichTextPreformatted) UnmarshalJSON(b []byte) error { // original struct, which may become a maintenance burden (i.e. update the // fields in two places, should it ever change). var standalone struct { - Border int `json:"border"` + Border int `json:"border"` + Language string `json:"language"` } if err := json.Unmarshal(b, &standalone); err != nil { return err } *s = RichTextPreformatted{ - RichTextSection: rts, - Border: standalone.Border, + Type: RTEPreformatted, + Elements: rts.Elements, + Border: standalone.Border, + Language: standalone.Language, } - s.Type = RTEPreformatted return nil } diff --git a/vendor/github.com/slack-go/slack/block_section.go b/vendor/github.com/slack-go/slack/block_section.go index 01ffd5a1d..0d2352a03 100644 --- a/vendor/github.com/slack-go/slack/block_section.go +++ b/vendor/github.com/slack-go/slack/block_section.go @@ -9,6 +9,7 @@ type SectionBlock struct { BlockID string `json:"block_id,omitempty"` Fields []*TextBlockObject `json:"fields,omitempty"` Accessory *Accessory `json:"accessory,omitempty"` + Expand bool `json:"expand,omitempty"` } // BlockType returns the type of the block @@ -16,6 +17,11 @@ func (s SectionBlock) BlockType() MessageBlockType { return s.Type } +// ID returns the ID of the block +func (s SectionBlock) ID() string { + return s.BlockID +} + // SectionBlockOption allows configuration of options for a new section block type SectionBlockOption func(*SectionBlock) @@ -25,6 +31,15 @@ func SectionBlockOptionBlockID(blockID string) SectionBlockOption { } } +// SectionBlockOptionExpand allows long text to be auto-expanded when displaying +// +// @see https://api.slack.com/reference/block-kit/blocks#section +func SectionBlockOptionExpand(shouldExpand bool) SectionBlockOption { + return func(block *SectionBlock) { + block.Expand = shouldExpand + } +} + // NewSectionBlock returns a new instance of a section block to be rendered func NewSectionBlock(textObj *TextBlockObject, fields []*TextBlockObject, accessory *Accessory, options ...SectionBlockOption) *SectionBlock { block := SectionBlock{ diff --git a/vendor/github.com/slack-go/slack/block_table.go b/vendor/github.com/slack-go/slack/block_table.go new file mode 100644 index 000000000..5c7b0f13a --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_table.go @@ -0,0 +1,55 @@ +package slack + +// TableBlock defines a block that lets you use a table to display your data. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/table-block/ +type TableBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Rows [][]*RichTextBlock `json:"rows"` + ColumnSettings []ColumnSetting `json:"column_settings,omitempty"` +} + +type ColumnAlignment string + +const ( + ColumnAlignmentLeft ColumnAlignment = "left" + ColumnAlignmentCenter ColumnAlignment = "center" + ColumnAlignmentRight ColumnAlignment = "right" +) + +type ColumnSetting struct { + Align ColumnAlignment `json:"align"` + IsWrapped bool `json:"is_wrapped"` +} + +// BlockType returns the type of the block +func (s TableBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s TableBlock) ID() string { + return s.BlockID +} + +// WithColumnSettings sets the column settings for the Table Block +func (s *TableBlock) WithColumnSettings(columnSettings ...ColumnSetting) *TableBlock { + s.ColumnSettings = columnSettings + return s +} + +// AddRow adds a new row of cells to the Table Block +func (s *TableBlock) AddRow(cells ...*RichTextBlock) *TableBlock { + s.Rows = append(s.Rows, append([]*RichTextBlock{}, cells...)) + return s +} + +// NewTableBlock returns an instance of a Table Block type +func NewTableBlock(blockID string) *TableBlock { + return &TableBlock{ + Type: MBTTable, + BlockID: blockID, + Rows: make([][]*RichTextBlock, 0), + } +} diff --git a/vendor/github.com/slack-go/slack/block_task_card.go b/vendor/github.com/slack-go/slack/block_task_card.go new file mode 100644 index 000000000..2053a82d6 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_task_card.go @@ -0,0 +1,101 @@ +package slack + +// TaskCardStatus defines the status of a task card block. +type TaskCardStatus string + +const ( + TaskCardStatusPending TaskCardStatus = "pending" + TaskCardStatusInProgress TaskCardStatus = "in_progress" + TaskCardStatusComplete TaskCardStatus = "complete" + TaskCardStatusError TaskCardStatus = "error" +) + +// TaskCardSource represents a URL reference in a task card block. +type TaskCardSource struct { + Type string `json:"type"` + URL string `json:"url"` + Text string `json:"text"` +} + +// NewTaskCardSource creates a new TaskCardSource with type "url". +func NewTaskCardSource(url, text string) TaskCardSource { + return TaskCardSource{ + Type: "url", + URL: url, + Text: text, + } +} + +// TaskCardBlock defines a block of type task_card used by AI agents +// to display thinking steps and task execution. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/task-card-block/ +type TaskCardBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + TaskID string `json:"task_id"` + Title string `json:"title"` + Status TaskCardStatus `json:"status,omitempty"` + Details *RichTextBlock `json:"details,omitempty"` + Output *RichTextBlock `json:"output,omitempty"` + Sources []TaskCardSource `json:"sources,omitempty"` +} + +// BlockType returns the type of the block +func (s TaskCardBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s TaskCardBlock) ID() string { + return s.BlockID +} + +// TaskCardBlockOption allows configuration of options for a new task card block +type TaskCardBlockOption func(*TaskCardBlock) + +// TaskCardBlockOptionBlockID sets the block ID for the task card block +func TaskCardBlockOptionBlockID(blockID string) TaskCardBlockOption { + return func(block *TaskCardBlock) { + block.BlockID = blockID + } +} + +// NewTaskCardBlock returns a new instance of a task card block +func NewTaskCardBlock(taskID, title string, options ...TaskCardBlockOption) *TaskCardBlock { + block := TaskCardBlock{ + Type: MBTTaskCard, + TaskID: taskID, + Title: title, + } + + for _, option := range options { + option(&block) + } + + return &block +} + +// WithStatus sets the status for the TaskCardBlock +func (s *TaskCardBlock) WithStatus(status TaskCardStatus) *TaskCardBlock { + s.Status = status + return s +} + +// WithDetails sets the details rich text block for the TaskCardBlock +func (s *TaskCardBlock) WithDetails(details *RichTextBlock) *TaskCardBlock { + s.Details = details + return s +} + +// WithOutput sets the output rich text block for the TaskCardBlock +func (s *TaskCardBlock) WithOutput(output *RichTextBlock) *TaskCardBlock { + s.Output = output + return s +} + +// WithSources sets the sources for the TaskCardBlock +func (s *TaskCardBlock) WithSources(sources ...TaskCardSource) *TaskCardBlock { + s.Sources = sources + return s +} diff --git a/vendor/github.com/slack-go/slack/block_unknown.go b/vendor/github.com/slack-go/slack/block_unknown.go index 97054c73e..71b2a90c9 100644 --- a/vendor/github.com/slack-go/slack/block_unknown.go +++ b/vendor/github.com/slack-go/slack/block_unknown.go @@ -1,13 +1,36 @@ package slack -// UnknownBlock represents a block type that is not yet known. This block type exists to prevent Slack from introducing -// new and unknown block types that break this library. +import "encoding/json" + +// UnknownBlock represents a block type that is not yet known. This block type +// exists to prevent Slack from introducing new and unknown block types that +// break this library. It preserves the raw JSON so that unrecognized blocks +// survive round-trip marshaling. +// +// If you encounter an UnknownBlock for a block type that Slack documents, +// please open an issue at https://github.com/slack-go/slack/issues so we can +// add first-class support for it. type UnknownBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` + raw json.RawMessage } // BlockType returns the type of the block func (b UnknownBlock) BlockType() MessageBlockType { return b.Type } + +// ID returns the ID of the block +func (s UnknownBlock) ID() string { + return s.BlockID +} + +// MarshalJSON returns the original raw JSON if available, preserving all fields +func (b UnknownBlock) MarshalJSON() ([]byte, error) { + if b.raw != nil { + return b.raw, nil + } + type alias UnknownBlock + return json.Marshal(alias(b)) +} diff --git a/vendor/github.com/slack-go/slack/block_video.go b/vendor/github.com/slack-go/slack/block_video.go index 322c614f9..4d6739c46 100644 --- a/vendor/github.com/slack-go/slack/block_video.go +++ b/vendor/github.com/slack-go/slack/block_video.go @@ -22,6 +22,11 @@ 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{ diff --git a/vendor/github.com/slack-go/slack/channels.go b/vendor/github.com/slack-go/slack/channels.go index 88d567bff..d01ce823f 100644 --- a/vendor/github.com/slack-go/slack/channels.go +++ b/vendor/github.com/slack-go/slack/channels.go @@ -28,7 +28,7 @@ type Channel struct { func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) { response := &channelResponseFull{} - err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api) + _, err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api) if err != nil { return nil, err } diff --git a/vendor/github.com/slack-go/slack/chat.go b/vendor/github.com/slack-go/slack/chat.go index 96843d68d..a58b4e805 100644 --- a/vendor/github.com/slack-go/slack/chat.go +++ b/vendor/github.com/slack-go/slack/chat.go @@ -68,6 +68,9 @@ type PostMessageParameters struct { // 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 @@ -116,13 +119,13 @@ func (api *Client) ScheduleMessage(channelID, postAt string, options ...MsgOptio // ScheduleMessageContext sends a message to a channel with a custom context. // Slack API docs: https://api.slack.com/methods/chat.scheduleMessage func (api *Client) ScheduleMessageContext(ctx context.Context, channelID, postAt string, options ...MsgOption) (string, string, error) { - respChannel, respTimestamp, _, err := api.SendMessageContext( + respChannel, scheduledMessageID, _, err := api.SendMessageContext( ctx, channelID, MsgOptionSchedule(postAt), MsgOptionCompose(options...), ) - return respChannel, respTimestamp, err + return respChannel, scheduledMessageID, err } // PostMessage sends a message to a channel. @@ -206,6 +209,33 @@ func (api *Client) UnfurlMessageWithAuthURLContext(ctx context.Context, channelI return api.SendMessageContext(ctx, channelID, MsgOptionUnfurlAuthURL(timestamp, userAuthURL), MsgOptionCompose(options...)) } +// UnfurlMessageWorkObject unfurls a message with Work Objects metadata. +// For more details, see UnfurlMessageWorkObjectContext documentation. +func (api *Client) UnfurlMessageWorkObject(channelID, timestamp string, unfurls map[string]Attachment, metadata WorkObjectMetadata, options ...MsgOption) (string, string, string, error) { + return api.UnfurlMessageWorkObjectContext(context.Background(), channelID, timestamp, unfurls, metadata, options...) +} + +// UnfurlMessageWorkObjectContext unfurls a message with Work Objects metadata with a custom context. +// This enables rich Work Object previews as described in https://docs.slack.dev/messaging/work-objects/ +// unfurls may be nil to send only Work Object metadata (no legacy attachment unfurls). +func (api *Client) UnfurlMessageWorkObjectContext(ctx context.Context, channelID, timestamp string, unfurls map[string]Attachment, metadata WorkObjectMetadata, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(ctx, channelID, MsgOptionUnfurlWorkObject(timestamp, unfurls, metadata), MsgOptionCompose(options...)) +} + +// UnfurlMessageByID unfurls a link in the message composer using unfurl_id and source. +// Use this when Slack sends link_shared with unfurl_id (e.g. before the message is posted). +// For more details, see UnfurlMessageByIDContext documentation. +func (api *Client) UnfurlMessageByID(unfurlID, source string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { + return api.UnfurlMessageByIDContext(context.Background(), unfurlID, source, unfurls, options...) +} + +// UnfurlMessageByIDContext unfurls by unfurl_id and source with a custom context. +// Both unfurl_id and source must be provided together (alternative to channel + ts). +// Slack API docs: https://api.slack.com/methods/chat.unfurl +func (api *Client) UnfurlMessageByIDContext(ctx context.Context, unfurlID, source string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(ctx, "", MsgOptionUnfurlByID(unfurlID, source, unfurls), MsgOptionCompose(options...)) +} + // SendMessage more flexible method for configuring messages. // For more details, see SendMessageContext documentation. func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) { @@ -214,7 +244,7 @@ func (api *Client) SendMessage(channel string, options ...MsgOption) (string, st // SendMessageContext more flexible method for configuring messages with a custom context. // Slack API docs: https://api.slack.com/methods/chat.postMessage -func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestamp string, _text string, err error) { +func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestampOrScheduledMessageID string, _text string, err error) { var ( req *http.Request parser func(*chatResponseFull) responseParser @@ -234,11 +264,15 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt api.Debugf("Sending request: %s", redactToken(reqBody)) } - if err = doPost(ctx, api.httpclient, req, parser(&response), api); err != nil { + if _, err = doPost(api.httpclient, req, parser(&response), api); err != nil { return "", "", "", err } - return response.Channel, response.getMessageTimestamp(), response.Text, response.Err() + if response.ScheduledMessageID != "" { + return response.Channel, response.ScheduledMessageID, response.Text, response.Err() + } else { + return response.Channel, response.getMessageTimestamp(), response.Text, response.Err() + } } func redactToken(b []byte) []byte { @@ -300,6 +334,9 @@ const ( chatResponse sendMode = "chat.responseURL" chatMeMessage sendMode = "chat.meMessage" chatUnfurl sendMode = "chat.unfurl" + chatStartStream sendMode = "chat.startStream" + chatAppendStream sendMode = "chat.appendStream" + chatStopStream sendMode = "chat.stopStream" ) type sendConfig struct { @@ -338,13 +375,15 @@ func (t sendConfig) BuildRequestContext(ctx context.Context, token, channelID st deleteOriginal: t.deleteOriginal, }.BuildRequestContext(ctx) default: - return formSender{endpoint: t.endpoint, values: t.values}.BuildRequestContext(ctx) + return formSender{endpoint: t.endpoint, values: t.values, attachments: t.attachments, blocks: t.blocks}.BuildRequestContext(ctx) } } type formSender struct { - endpoint string - values url.Values + endpoint string + values url.Values + attachments []Attachment + blocks Blocks } func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { @@ -352,6 +391,22 @@ func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) respo } func (t formSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) { + if t.attachments != nil { + attachmentBytes, err := json.Marshal(t.attachments) + if err != nil { + return nil, nil, err + } + t.values.Set("attachments", string(attachmentBytes)) + } + + if t.blocks.BlockSet != nil { + blockBytes, err := json.Marshal(t.blocks.BlockSet) + if err != nil { + return nil, nil, err + } + t.values.Set("blocks", string(blockBytes)) + } + req, err := formReq(ctx, t.endpoint, t.values) return req, func(resp *chatResponseFull) responseParser { return newJSONParser(resp) @@ -461,6 +516,51 @@ func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption } } +// MsgOptionUnfurlByID unfurls using unfurl_id and source (e.g. when link is in the composer). +// Use instead of channel+ts when Slack provides unfurl_id in the link_shared event. +// unfurls may be nil; the API expects a JSON object so nil is sent as {}. +func MsgOptionUnfurlByID(unfurlID, source string, unfurls map[string]Attachment) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Del("channel") + config.values.Del("ts") + config.values.Set("unfurl_id", unfurlID) + config.values.Set("source", source) + if unfurls == nil { + unfurls = make(map[string]Attachment) + } + unfurlsStr, err := json.Marshal(unfurls) + if err == nil { + config.values.Set("unfurls", string(unfurlsStr)) + } + return err + } +} + +// MsgOptionUnfurlMetadataOnly sets chat.unfurl endpoint with only Work Object metadata (no unfurls). +func MsgOptionUnfurlMetadataOnly(timestamp string, metadata WorkObjectMetadata) MsgOption { + return MsgOptionCompose( + func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + return nil + }, + MsgOptionWorkObjectMetadata(metadata), + ) +} + +// MsgOptionUnfurlWorkObject unfurls a message with Work Objects metadata. +// When unfurls is nil, only metadata is sent (no legacy attachment unfurls). +func MsgOptionUnfurlWorkObject(timestamp string, unfurls map[string]Attachment, metadata WorkObjectMetadata) MsgOption { + if len(unfurls) > 0 { + return MsgOptionCompose( + MsgOptionUnfurl(timestamp, unfurls), + MsgOptionWorkObjectMetadata(metadata), + ) + } + return MsgOptionUnfurlMetadataOnly(timestamp, metadata) +} + // MsgOptionUnfurlAuthURL unfurls a message using an auth url based on the timestamp. func MsgOptionUnfurlAuthURL(timestamp string, userAuthURL string) MsgOption { return func(config *sendConfig) error { @@ -493,6 +593,23 @@ func MsgOptionUnfurlAuthMessage(timestamp string, msg string) MsgOption { } } +// MsgOptionUnfurlAuthBlocks sets Block Kit blocks for the auth prompt (overrides default buttons). +// See https://docs.slack.com/methods/chat.unfurl for user_auth_blocks. +func MsgOptionUnfurlAuthBlocks(timestamp string, blocks ...Block) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + if len(blocks) == 0 { + return nil + } + blocksStr, err := json.Marshal(blocks) + if err == nil { + config.values.Set("user_auth_blocks", string(blocksStr)) + } + return err + } +} + // MsgOptionResponseURL supplies a url to use as the endpoint. func MsgOptionResponseURL(url string, responseType string) MsgOption { return func(config *sendConfig) error { @@ -527,6 +644,7 @@ func MsgOptionDeleteOriginal(responseURL string) MsgOption { // MsgOptionAsUser whether or not to send the message as the user. func MsgOptionAsUser(b bool) MsgOption { return func(config *sendConfig) error { + //lint:ignore S1002 - we want to explicitly check against the constant if b != DEFAULT_MESSAGE_ASUSER { config.values.Set("as_user", "true") } @@ -571,33 +689,24 @@ func MsgOptionAttachments(attachments ...Attachment) MsgOption { config.attachments = attachments - // FIXME: We are setting the attachments on the message twice: above for - // the json version, and below for the html version. The marshalled bytes - // we put into config.values below don't work directly in the Msg version. - - attachmentBytes, err := json.Marshal(attachments) - if err == nil { - config.values.Set("attachments", string(attachmentBytes)) - } - - return err + return nil } } -// MsgOptionBlocks sets blocks for the message +// MsgOptionBlocks sets blocks for the message. +// Calling with no arguments or an empty slice sends "blocks=[]" to clear blocks. +// To skip setting blocks entirely, do not include this option. func MsgOptionBlocks(blocks ...Block) MsgOption { return func(config *sendConfig) error { - if blocks == nil { - return nil + if len(blocks) == 0 { + // Explicitly set to empty slice (not nil) so the sender + // knows to marshal "[]" and clear blocks on the message. + config.blocks.BlockSet = []Block{} + } else { + config.blocks.BlockSet = append(config.blocks.BlockSet, blocks...) } - config.blocks.BlockSet = append(config.blocks.BlockSet, blocks...) - - blocks, err := json.Marshal(blocks) - if err == nil { - config.values.Set("blocks", string(blocks)) - } - return err + return nil } } @@ -703,6 +812,31 @@ func MsgOptionMetadata(metadata SlackMetadata) MsgOption { } } +// MsgOptionWorkObjectMetadata sets Work Objects metadata for unfurls and messages +// This enables Work Objects support as described in https://docs.slack.dev/messaging/work-objects/ +// If metadata.Entities is nil, it is marshaled as [] so the API receives a valid entities array. +func MsgOptionWorkObjectMetadata(metadata WorkObjectMetadata) MsgOption { + return func(config *sendConfig) error { + metaToMarshal := metadata + if metaToMarshal.Entities == nil { + metaToMarshal.Entities = []WorkObjectEntity{} + } + meta, err := json.Marshal(metaToMarshal) + if err == nil { + config.values.Set("metadata", string(meta)) + } + return err + } +} + +// MsgOptionWorkObjectEntity creates Work Objects metadata with a single entity +// This is a convenience function for the common case of unfurling a single Work Object +func MsgOptionWorkObjectEntity(entity WorkObjectEntity) MsgOption { + return MsgOptionWorkObjectMetadata(WorkObjectMetadata{ + Entities: []WorkObjectEntity{entity}, + }) +} + // MsgOptionLinkNames finds and links user groups. Does not support linking individual users func MsgOptionLinkNames(linkName bool) MsgOption { return func(config *sendConfig) error { @@ -711,6 +845,91 @@ func MsgOptionLinkNames(linkName bool) MsgOption { } } +// MsgOptionFileIDs sets file IDs for the message +func MsgOptionFileIDs(fileIDs []string) MsgOption { + return func(config *sendConfig) error { + if len(fileIDs) == 0 { + return nil + } + + fileIDsBytes, err := json.Marshal(fileIDs) + if err != nil { + return err + } + + config.values.Set("file_ids", string(fileIDsBytes)) + return nil + } +} + +// MsgOptionStartStream starts a streaming message. +func MsgOptionStartStream() MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatStartStream) + return nil + } +} + +// MsgOptionAppendStream appends to a streaming message. +func MsgOptionAppendStream(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatAppendStream) + config.values.Add("ts", timestamp) + return nil + } +} + +// MsgOptionStopStream stops a streaming message. +func MsgOptionStopStream(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatStopStream) + config.values.Add("ts", timestamp) + return nil + } +} + +// MsgOptionRecipientTeamID sets the recipient team ID for streaming messages. +func MsgOptionRecipientTeamID(teamID string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("recipient_team_id", teamID) + return nil + } +} + +// MsgOptionRecipientUserID sets the recipient user ID for streaming messages. +func MsgOptionRecipientUserID(userID string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("recipient_user_id", userID) + return nil + } +} + +// MsgOptionMarkdownText sets the markdown text for streaming messages. +func MsgOptionMarkdownText(text string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("markdown_text", text) + return nil + } +} + +// TaskDisplayMode controls how task_card / task_update chunks render in a +// streamed message. Used with chat.startStream. +type TaskDisplayMode string + +const ( + TaskDisplayModeTimeline TaskDisplayMode = "timeline" + TaskDisplayModePlan TaskDisplayMode = "plan" +) + +// MsgOptionTaskDisplayMode sets task_display_mode on chat.startStream, +// controlling whether tasks render as a sequential timeline or a grouped plan. +func MsgOptionTaskDisplayMode(mode TaskDisplayMode) MsgOption { + return func(config *sendConfig) error { + config.values.Set("task_display_mode", string(mode)) + return nil + } +} + // 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 @@ -745,15 +964,19 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { config.values.Set("link_names", "1") } + //lint:ignore S1002 - we want to explicitly check against the constant if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS { config.values.Set("unfurl_links", "true") } // I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request. // Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side. + //lint:ignore S1002 - we want to explicitly check against the constants if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS { config.values.Set("unfurl_links", "false") } + + //lint:ignore S1002 - we want to explicitly check against the constant if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { config.values.Set("unfurl_media", "false") } @@ -763,6 +986,7 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { config.values.Set("icon_emoji", params.IconEmoji) } + //lint:ignore S1002 - we want to explicitly check against the constant if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { config.values.Set("mrkdwn", "false") } @@ -770,10 +994,21 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP { config.values.Set("thread_ts", params.ThreadTimestamp) } + //lint:ignore S1002 - we want to explicitly check against the constant if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST { config.values.Set("reply_broadcast", "true") } + if params.MetaData.EventType != "" { + if err := MsgOptionMetadata(params.MetaData)(config); err != nil { + return err + } + } + + if len(params.FileIDs) > 0 { + return MsgOptionFileIDs(params.FileIDs)(config) + } + return nil } } @@ -896,3 +1131,57 @@ func (api *Client) DeleteScheduledMessageContext(ctx context.Context, params *De return response.Ok, response.Err() } + +// StartStream starts a streaming message in a channel. +// For more details, see StartStreamContext documentation. +func (api *Client) StartStream(channelID string, options ...MsgOption) (string, string, error) { + return api.StartStreamContext(context.Background(), channelID, options...) +} + +// StartStreamContext starts a streaming message in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.startStream +func (api *Client) StartStreamContext(ctx context.Context, channelID string, options ...MsgOption) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + ctx, + channelID, + MsgOptionStartStream(), + MsgOptionCompose(options...), + ) + return respChannel, respTimestamp, err +} + +// AppendStream appends text to a streaming message. +// For more details, see AppendStreamContext documentation. +func (api *Client) AppendStream(channelID, timestamp string, options ...MsgOption) (string, string, error) { + return api.AppendStreamContext(context.Background(), channelID, timestamp, options...) +} + +// AppendStreamContext appends text to a streaming message with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.appendStream +func (api *Client) AppendStreamContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + ctx, + channelID, + MsgOptionAppendStream(timestamp), + MsgOptionCompose(options...), + ) + return respChannel, respTimestamp, err +} + +// StopStream stops a streaming message. +// For more details, see StopStreamContext documentation. +func (api *Client) StopStream(channelID, timestamp string, options ...MsgOption) (string, string, error) { + return api.StopStreamContext(context.Background(), channelID, timestamp, options...) +} + +// StopStreamContext stops a streaming message with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.stopStream +func (api *Client) StopStreamContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + ctx, + channelID, + MsgOptionStopStream(timestamp), + MsgOptionCompose(options...), + ) + return respChannel, respTimestamp, err +} diff --git a/vendor/github.com/slack-go/slack/chat_stream_chunks.go b/vendor/github.com/slack-go/slack/chat_stream_chunks.go new file mode 100644 index 000000000..fbdab3be9 --- /dev/null +++ b/vendor/github.com/slack-go/slack/chat_stream_chunks.go @@ -0,0 +1,95 @@ +package slack + +import ( + "encoding/json" +) + +// StreamChunkType identifies a chunk in the chat.startStream / chat.appendStream +// / chat.stopStream streaming-message protocol. +// +// More information: https://docs.slack.dev/reference/methods/chat.appendStream/ +type StreamChunkType string + +const ( + StreamChunkMarkdownText StreamChunkType = "markdown_text" + StreamChunkTaskUpdate StreamChunkType = "task_update" + StreamChunkPlanUpdate StreamChunkType = "plan_update" + StreamChunkBlocks StreamChunkType = "blocks" +) + +// StreamChunk represents a single chunk in the streaming-message chunks array. +type StreamChunk interface { + ChunkType() StreamChunkType +} + +// MarkdownTextChunk streams markdown-formatted text. +type MarkdownTextChunk struct { + Type StreamChunkType `json:"type"` + Text string `json:"text"` +} + +func (c MarkdownTextChunk) ChunkType() StreamChunkType { return c.Type } + +// NewMarkdownTextChunk returns a markdown_text chunk. +func NewMarkdownTextChunk(text string) MarkdownTextChunk { + return MarkdownTextChunk{Type: StreamChunkMarkdownText, Text: text} +} + +// TaskUpdateChunk streams a task status update that renders as a task card. +type TaskUpdateChunk struct { + Type StreamChunkType `json:"type"` + ID string `json:"id"` + Title string `json:"title"` + Status TaskCardStatus `json:"status,omitempty"` + Details string `json:"details,omitempty"` + Output string `json:"output,omitempty"` + Sources []TaskCardSource `json:"sources,omitempty"` +} + +func (c TaskUpdateChunk) ChunkType() StreamChunkType { return c.Type } + +// NewTaskUpdateChunk returns a task_update chunk with the given id and title. +func NewTaskUpdateChunk(id, title string) TaskUpdateChunk { + return TaskUpdateChunk{Type: StreamChunkTaskUpdate, ID: id, Title: title} +} + +// PlanUpdateChunk streams an update to the current plan's title. +type PlanUpdateChunk struct { + Type StreamChunkType `json:"type"` + Title string `json:"title"` +} + +func (c PlanUpdateChunk) ChunkType() StreamChunkType { return c.Type } + +// NewPlanUpdateChunk returns a plan_update chunk. +func NewPlanUpdateChunk(title string) PlanUpdateChunk { + return PlanUpdateChunk{Type: StreamChunkPlanUpdate, Title: title} +} + +// BlocksChunk streams a group of Block Kit blocks. Up to 50 blocks per chunk. +type BlocksChunk struct { + Type StreamChunkType `json:"type"` + Blocks []Block `json:"blocks"` +} + +func (c BlocksChunk) ChunkType() StreamChunkType { return c.Type } + +// NewBlocksChunk returns a blocks chunk containing the given blocks. +func NewBlocksChunk(blocks ...Block) BlocksChunk { + return BlocksChunk{Type: StreamChunkBlocks, Blocks: blocks} +} + +// MsgOptionChunks sets the `chunks` parameter for the streaming chat methods +// (chat.startStream / chat.appendStream / chat.stopStream). It is the +// transport for Block Kit agent-UI blocks (Alert, Card, Carousel, etc.) which +// chat.postMessage rejects as "Unsupported block type". +func MsgOptionChunks(chunks ...StreamChunk) MsgOption { + return func(config *sendConfig) error { + encoded, err := json.Marshal(chunks) + if err != nil { + return err + } + config.values.Set("chunks", string(encoded)) + return nil + } +} diff --git a/vendor/github.com/slack-go/slack/conversation.go b/vendor/github.com/slack-go/slack/conversation.go index 67d28be3b..a0ce707b0 100644 --- a/vendor/github.com/slack-go/slack/conversation.go +++ b/vendor/github.com/slack-go/slack/conversation.go @@ -7,6 +7,7 @@ import ( "net/url" "strconv" "strings" + "time" ) // Conversation is the foundation for IM and BaseGroupConversation @@ -28,6 +29,7 @@ type Conversation struct { IsPrivate bool `json:"is_private"` IsReadOnly bool `json:"is_read_only"` IsMpIM bool `json:"is_mpim"` + IsUserDeleted bool `json:"is_user_deleted"` Unlinked int `json:"unlinked"` NameNormalized string `json:"name_normalized"` NumMembers int `json:"num_members"` @@ -36,9 +38,10 @@ type Conversation struct { ConnectedTeamIDs []string `json:"connected_team_ids,omitempty"` SharedTeamIDs []string `json:"shared_team_ids,omitempty"` InternalTeamIDs []string `json:"internal_team_ids,omitempty"` - - // TODO support pending_shared - // TODO support previous_names + 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 @@ -66,9 +69,24 @@ type Purpose struct { LastSet JSONTime `json:"last_set"` } -// Properties contains the Canvas associated to the channel. +// Properties contains additional fields that appear based on the context of the conversation type Properties struct { - Canvas Canvas `json:"canvas"` + Canvas Canvas `json:"canvas"` + PostingRestrictedTo RestrictedTo `json:"posting_restricted_to"` + Tabs []Tab `json:"tabs"` + ThreadsRestrictedTo RestrictedTo `json:"threads_restricted_to"` + RecordChannel RecordChannel `json:"record_channel"` +} + +type RestrictedTo struct { + Type []string `json:"type"` + User []string `json:"user"` +} + +type Tab struct { + ID string `json:"id"` + Label string `json:"label"` + Type string `json:"type"` } type Canvas struct { @@ -77,6 +95,13 @@ type Canvas struct { QuipThreadId string `json:"quip_thread_id"` } +type RecordChannel struct { + RecordID string `json:"record_id"` + RecordType string `json:"record_type"` + RecordLabel string `json:"record_label"` + RecordLabelPlural string `json:"record_label_plural"` +} + type GetUsersInConversationParameters struct { ChannelID string Cursor string @@ -329,6 +354,46 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel return response.Channel, response.Err() } +/********************************************************************************** +The following functions are for inviting users to a channel but setting the `force` +parameter to true. We have added this so that we don't break the existing API. + +IMPORTANT: If we ever get here for _another_ parameter, we should consider refactoring +this to be more flexible. +*/ + +// 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) { @@ -425,7 +490,7 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI "user": {user}, } - response := SlackResponse{} + response := KickUserFromConversationSlackResponse{} err := api.postMethod(ctx, "conversations.kick", values, &response) if err != nil { return err @@ -624,6 +689,150 @@ type GetConversationsParameters struct { TeamID string } +// GetConversationsOption options for the GetAllConversationsContext method call. +type GetConversationsOption func(*ConversationPagination) + +// GetConversationsOptionLimit limit the number of conversations returned +func GetConversationsOptionLimit(n int) GetConversationsOption { + return func(p *ConversationPagination) { + p.limit = n + } +} + +// GetConversationsOptionExcludeArchived exclude archived conversations +func GetConversationsOptionExcludeArchived(exclude bool) GetConversationsOption { + return func(p *ConversationPagination) { + p.excludeArchived = exclude + } +} + +// GetConversationsOptionTypes filter conversations by type +func GetConversationsOptionTypes(types []string) GetConversationsOption { + return func(p *ConversationPagination) { + p.types = types + } +} + +// GetConversationsOptionTeamID include team Id +func GetConversationsOptionTeamID(teamId string) GetConversationsOption { + return func(p *ConversationPagination) { + p.teamId = teamId + } +} + +func newConversationPagination(c *Client, options ...GetConversationsOption) (cp ConversationPagination) { + cp = ConversationPagination{ + c: c, + limit: 200, // per slack api documentation. + } + + for _, opt := range options { + opt(&cp) + } + + return cp +} + +// ConversationPagination allows for paginating over the conversations +type ConversationPagination struct { + Conversations []Channel + limit int + excludeArchived bool + types []string + teamId string + previousResp *ResponseMetadata + c *Client +} + +// Done checks if the pagination has completed +func (ConversationPagination) Done(err error) bool { + return errors.Is(err, errPaginationComplete) +} + +// Failure checks if pagination failed. +func (t ConversationPagination) Failure(err error) error { + if t.Done(err) { + return nil + } + + return err +} + +func (t ConversationPagination) Next(ctx context.Context) (_ ConversationPagination, err error) { + if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") { + return t, errPaginationComplete + } + + t.previousResp = t.previousResp.initialize() + + values := url.Values{ + "token": {t.c.token}, + "limit": {strconv.Itoa(t.limit)}, + "cursor": {t.previousResp.Cursor}, + } + if t.excludeArchived { + values.Add("exclude_archived", strconv.FormatBool(t.excludeArchived)) + } + if t.types != nil { + values.Add("types", strings.Join(t.types, ",")) + } + if t.teamId != "" { + values.Add("team_id", t.teamId) + } + + response := struct { + Channels []Channel `json:"channels"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + + err = t.c.postMethod(ctx, "conversations.list", values, &response) + if err != nil { + return t, err + } + + if err := response.Err(); err != nil { + return t, err + } + + t.c.Debugf("GetAllConversationsContext: got %d conversations; cursor %s", len(response.Channels), response.ResponseMetaData.NextCursor) + t.Conversations = response.Channels + t.previousResp = &ResponseMetadata{Cursor: response.ResponseMetaData.NextCursor} + + return t, nil +} + +// GetConversationsPaginated fetches conversations in a paginated fashion, see GetAllConversationsContext for usage. +func (api *Client) GetConversationsPaginated(options ...GetConversationsOption) ConversationPagination { + return newConversationPagination(api, options...) +} + +// GetAllConversations returns the list of all conversations, handling pagination and rate limiting +func (api *Client) GetAllConversations(options ...GetConversationsOption) (results []Channel, err error) { + return api.GetAllConversationsContext(context.Background(), options...) +} + +// GetAllConversationsContext returns the list of all conversations with a custom context, handling pagination and rate limiting +func (api *Client) GetAllConversationsContext(ctx context.Context, options ...GetConversationsOption) (results []Channel, err error) { + results = []Channel{} + p := api.GetConversationsPaginated(options...) + for err == nil { + p, err = p.Next(ctx) + if err == nil { + results = append(results, p.Conversations...) + } else if rateLimitedError, ok := err.(*RateLimitedError); ok { + select { + case <-ctx.Done(): + err = ctx.Err() + case <-time.After(rateLimitedError.RetryAfter): + err = nil + } + } + } + + return results, p.Failure(err) +} + // GetConversations returns the list of channels in a Slack team. // For more details, see GetConversationsContext documentation. func (api *Client) GetConversations(params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) { @@ -760,13 +969,13 @@ type GetConversationHistoryResponse struct { Messages []Message `json:"messages"` } -// GetConversationHistory joins an existing conversation. +// GetConversationHistory retrieves the message history from the specified conversation. // For more details, see GetConversationHistoryContext documentation. func (api *Client) GetConversationHistory(params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) { return api.GetConversationHistoryContext(context.Background(), params) } -// GetConversationHistoryContext joins an existing conversation with a custom context. +// GetConversationHistoryContext retrieves the message history from the specified conversation with a custom context. // Slack API docs: https://api.slack.com/methods/conversations.history func (api *Client) GetConversationHistoryContext(ctx context.Context, params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) { values := url.Values{"token": {api.token}, "channel": {params.ChannelID}} @@ -827,21 +1036,55 @@ func (api *Client) MarkConversationContext(ctx context.Context, channel, ts stri return response.Err() } +// createChannelCanvasParams contains arguments for CreateChannelCanvas method call. +type createChannelCanvasParams struct { + title string + documentContent *DocumentContent +} + +// CreateChannelCanvasOption options for the CreateChannelCanvas method call. +type CreateChannelCanvasOption func(*createChannelCanvasParams) + +// CreateChannelCanvasOptionTitle sets the title of the canvas. +func CreateChannelCanvasOptionTitle(title string) CreateChannelCanvasOption { + return func(params *createChannelCanvasParams) { + params.title = title + } +} + +// CreateChannelCanvasOptionDocumentContent sets the document content of the canvas. +func CreateChannelCanvasOptionDocumentContent(documentContent DocumentContent) CreateChannelCanvasOption { + return func(params *createChannelCanvasParams) { + params.documentContent = &documentContent + } +} + // CreateChannelCanvas creates a new canvas in a channel. // For more details, see CreateChannelCanvasContext documentation. -func (api *Client) CreateChannelCanvas(channel string, documentContent DocumentContent) (string, error) { - return api.CreateChannelCanvasContext(context.Background(), channel, documentContent) +func (api *Client) CreateChannelCanvas(channel string, documentContent DocumentContent, options ...CreateChannelCanvasOption) (string, error) { + return api.CreateChannelCanvasContext(context.Background(), channel, documentContent, options...) } // CreateChannelCanvasContext creates a new canvas in a channel with a custom context. // Slack API docs: https://api.slack.com/methods/conversations.canvases.create -func (api *Client) CreateChannelCanvasContext(ctx context.Context, channel string, documentContent DocumentContent) (string, error) { +func (api *Client) CreateChannelCanvasContext(ctx context.Context, channel string, documentContent DocumentContent, options ...CreateChannelCanvasOption) (string, error) { + params := createChannelCanvasParams{ + documentContent: &documentContent, + } + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "channel_id": {channel}, } - if documentContent.Type != "" { - documentContentJSON, err := json.Marshal(documentContent) + if params.title != "" { + values.Add("title", params.title) + } + if params.documentContent != nil && params.documentContent.Type != "" { + documentContentJSON, err := json.Marshal(params.documentContent) if err != nil { return "", err } diff --git a/vendor/github.com/slack-go/slack/dialog.go b/vendor/github.com/slack-go/slack/dialog.go index f94113f4d..4c507fcdf 100644 --- a/vendor/github.com/slack-go/slack/dialog.go +++ b/vendor/github.com/slack-go/slack/dialog.go @@ -106,8 +106,7 @@ func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dial } response := &DialogOpenResponse{} - endpoint := api.endpoint + "dialog.open" - if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api); err != nil { + if err := api.postJSONMethod(ctx, "dialog.open", api.token, encoded, response); err != nil { return err } diff --git a/vendor/github.com/slack-go/slack/dnd.go b/vendor/github.com/slack-go/slack/dnd.go index 81eaf5024..4f6b35a58 100644 --- a/vendor/github.com/slack-go/slack/dnd.go +++ b/vendor/github.com/slack-go/slack/dnd.go @@ -7,6 +7,14 @@ import ( "strings" ) +// DNDOptionTeamID sets the team_id parameter for DND methods. Required after +// workspace migration when the API returns missing_argument: team_id. +func DNDOptionTeamID(teamID string) ParamOption { + return func(v *url.Values) { + v.Set("team_id", teamID) + } +} + type SnoozeDebug struct { SnoozeEndDate string `json:"snooze_end_date"` } @@ -52,7 +60,7 @@ func (api *Client) EndDND() error { } // EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context. -// Slack API docs: https://api.slack.com/methods/dnd.endDnd +// Slack API docs: https://docs.slack.dev/reference/methods/dnd.endDnd func (api *Client) EndDNDContext(ctx context.Context) error { values := url.Values{ "token": {api.token}, @@ -74,7 +82,7 @@ func (api *Client) EndSnooze() (*DNDStatus, error) { } // EndSnoozeContext ends the current user's snooze mode with a custom context. -// Slack API docs: https://api.slack.com/methods/dnd.endSnooze +// Slack API docs: https://docs.slack.dev/reference/methods/dnd.endSnooze func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { values := url.Values{ "token": {api.token}, @@ -89,19 +97,22 @@ func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { // GetDNDInfo provides information about a user's current Do Not Disturb settings. // For more information see the GetDNDInfoContext documentation. -func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) { - return api.GetDNDInfoContext(context.Background(), user) +func (api *Client) GetDNDInfo(user *string, options ...ParamOption) (*DNDStatus, error) { + return api.GetDNDInfoContext(context.Background(), user, options...) } // GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context. -// Slack API docs: https://api.slack.com/methods/dnd.info -func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) { +// Slack API docs: https://docs.slack.dev/reference/methods/dnd.info/ +func (api *Client) GetDNDInfoContext(ctx context.Context, user *string, options ...ParamOption) (*DNDStatus, error) { values := url.Values{ "token": {api.token}, } if user != nil { values.Set("user", *user) } + for _, opt := range options { + opt(&values) + } response, err := api.dndRequest(ctx, "dnd.info", values) if err != nil { @@ -112,17 +123,20 @@ func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDSta // GetDNDTeamInfo provides information about a user's current Do Not Disturb settings. // For more information see the GetDNDTeamInfoContext documentation. -func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) { - return api.GetDNDTeamInfoContext(context.Background(), users) +func (api *Client) GetDNDTeamInfo(users []string, options ...ParamOption) (map[string]DNDStatus, error) { + return api.GetDNDTeamInfoContext(context.Background(), users, options...) } // GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context. -// Slack API docs: https://api.slack.com/methods/dnd.teamInfo -func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) { +// Slack API docs: https://docs.slack.dev/reference/methods/dnd.teamInfo +func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string, options ...ParamOption) (map[string]DNDStatus, error) { values := url.Values{ "token": {api.token}, "users": {strings.Join(users, ",")}, } + for _, opt := range options { + opt(&values) + } response := &dndTeamInfoResponse{} if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil { @@ -145,7 +159,7 @@ func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { // SetSnoozeContext adjusts the snooze duration for a user's Do Not Disturb settings. // If a snooze session is not already active for the user, invoking this method will // begin one for the specified duration. -// Slack API docs: https://api.slack.com/methods/dnd.setSnooze +// Slack API docs: https://docs.slack.dev/reference/methods/dnd.setSnooze func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) { values := url.Values{ "token": {api.token}, diff --git a/vendor/github.com/slack-go/slack/entity.go b/vendor/github.com/slack-go/slack/entity.go new file mode 100644 index 000000000..815de4d75 --- /dev/null +++ b/vendor/github.com/slack-go/slack/entity.go @@ -0,0 +1,132 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" +) + +// EntityPresentDetailsParameters contains the parameters for entity.presentDetails API method +type EntityPresentDetailsParameters struct { + TriggerID string `json:"trigger_id"` + Metadata *EntityDetailsMetadata `json:"metadata,omitempty"` + Error *EntityDetailsError `json:"error,omitempty"` + UserAuthRequired bool `json:"user_auth_required,omitempty"` + UserAuthURL string `json:"user_auth_url,omitempty"` + UserAuthMessage string `json:"user_auth_message,omitempty"` +} + +// EntityDetailsMetadata represents the metadata for entity details +type EntityDetailsMetadata struct { + EntityType string `json:"entity_type"` + URL string `json:"url,omitempty"` + ExternalRef WorkObjectExternalRef `json:"external_ref,omitempty"` + EntityPayload map[string]interface{} `json:"entity_payload"` +} + +// EntityDetailsError represents an error response for entity details +type EntityDetailsError struct { + Status string `json:"status"` + CustomTitle string `json:"custom_title,omitempty"` + CustomMessage string `json:"custom_message,omitempty"` + MessageFormat string `json:"message_format,omitempty"` + Actions []EntityDetailsAction `json:"actions,omitempty"` +} + +// EntityDetailsAction represents an action button in entity details error +type EntityDetailsAction struct { + Text string `json:"text"` + ActionID string `json:"action_id"` + Value string `json:"value,omitempty"` + Style string `json:"style,omitempty"` + URL string `json:"url,omitempty"` + ProcessingState *EntityDetailsProcessingState `json:"processing_state,omitempty"` +} + +// EntityDetailsProcessingState represents the processing state of an action +type EntityDetailsProcessingState struct { + Enabled bool `json:"enabled"` +} + +// EntityPresentDetailsResponse represents the response from entity.presentDetails +type EntityPresentDetailsResponse struct { + SlackResponse +} + +// EntityPresentDetails presents entity details in the flexpane +// For more details, see EntityPresentDetailsContext documentation. +func (api *Client) EntityPresentDetails(params EntityPresentDetailsParameters) error { + return api.EntityPresentDetailsContext(context.Background(), params) +} + +// EntityPresentDetailsContext presents entity details in the flexpane with a custom context. +// Slack API docs: https://docs.slack.dev/reference/methods/entity.presentDetails +func (api *Client) EntityPresentDetailsContext(ctx context.Context, params EntityPresentDetailsParameters) error { + values := url.Values{ + "token": {api.token}, + "trigger_id": {params.TriggerID}, + } + + // Add metadata if provided + if params.Metadata != nil { + metadataJSON, err := json.Marshal(params.Metadata) + if err != nil { + return err + } + values.Set("metadata", string(metadataJSON)) + } + + // Add error if provided + if params.Error != nil { + errorJSON, err := json.Marshal(params.Error) + if err != nil { + return err + } + values.Set("error", string(errorJSON)) + } + + // Add user auth parameters if provided + if params.UserAuthRequired { + values.Set("user_auth_required", "true") + } + if params.UserAuthURL != "" { + values.Set("user_auth_url", params.UserAuthURL) + } + if params.UserAuthMessage != "" { + values.Set("user_auth_message", params.UserAuthMessage) + } + + response := &EntityPresentDetailsResponse{} + err := api.postMethod(ctx, "entity.presentDetails", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// EntityPresentDetailsWithMetadata is a convenience method for presenting entity details with metadata +func (api *Client) EntityPresentDetailsWithMetadata(triggerID string, metadata EntityDetailsMetadata) error { + return api.EntityPresentDetailsContext(context.Background(), EntityPresentDetailsParameters{ + TriggerID: triggerID, + Metadata: &metadata, + }) +} + +// EntityPresentDetailsWithError is a convenience method for presenting entity details with an error +func (api *Client) EntityPresentDetailsWithError(triggerID string, errPayload EntityDetailsError) error { + return api.EntityPresentDetailsContext(context.Background(), EntityPresentDetailsParameters{ + TriggerID: triggerID, + Error: &errPayload, + }) +} + +// EntityPresentDetailsWithAuth is a convenience method for presenting entity details with authentication required +func (api *Client) EntityPresentDetailsWithAuth(triggerID, authURL, authMessage string) error { + return api.EntityPresentDetailsContext(context.Background(), EntityPresentDetailsParameters{ + TriggerID: triggerID, + UserAuthRequired: true, + UserAuthURL: authURL, + UserAuthMessage: authMessage, + }) +} diff --git a/vendor/github.com/slack-go/slack/files.go b/vendor/github.com/slack-go/slack/files.go index b26317145..974ba0a43 100644 --- a/vendor/github.com/slack-go/slack/files.go +++ b/vendor/github.com/slack-go/slack/files.go @@ -89,6 +89,28 @@ type File struct { NumStars int `json:"num_stars"` IsStarred bool `json:"is_starred"` Shares Share `json:"shares"` + + Subject string `json:"subject"` + To []EmailFileUserInfo `json:"to"` + From []EmailFileUserInfo `json:"from"` + Cc []EmailFileUserInfo `json:"cc"` + Headers EmailHeaders `json:"headers"` + + PlainText string `json:"plain_text"` + PreviewPlainText string `json:"preview_plain_text"` +} + +type EmailFileUserInfo struct { + Address string `json:"address"` + Name string `json:"name"` + Original string `json:"original"` +} + +type EmailHeaders struct { + Date string `json:"date"` + InReplyTo string `json:"in_reply_to"` + ReplyTo string `json:"reply_to"` + MessageID string `json:"message_id"` } type Share struct { @@ -107,24 +129,6 @@ type ShareFileInfo struct { TeamID string `json:"team_id"` } -// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request. -// -// There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large, -// or provide a local file path in File to upload it from your filesystem. -// -// Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy. -type FileUploadParameters struct { - File string - Content string - Reader io.Reader - Filetype string - Filename string - Title string - InitialComment string - Channels []string - ThreadTimestamp string -} - // GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request type GetFilesParameters struct { User string @@ -148,7 +152,7 @@ type ListFilesParameters struct { Cursor string } -type UploadFileV2Parameters struct { +type UploadFileParameters struct { File string FileSize int Content string @@ -156,26 +160,27 @@ type UploadFileV2Parameters struct { Filename string Title string InitialComment string + Blocks Blocks Channel string ThreadTimestamp string AltTxt string - SnippetText string + SnippetType string } -type getUploadURLExternalParameters struct { - altText string - fileSize int - fileName string - snippetText string +type GetUploadURLExternalParameters struct { + AltTxt string + FileSize int + FileName string + SnippetType string } -type getUploadURLExternalResponse struct { +type GetUploadURLExternalResponse struct { UploadURL string `json:"upload_url"` FileID string `json:"file_id"` SlackResponse } -type uploadToURLParameters struct { +type UploadToURLParameters struct { UploadURL string Reader io.Reader File string @@ -188,14 +193,15 @@ type FileSummary struct { Title string `json:"title"` } -type completeUploadExternalParameters struct { - title string - channel string - initialComment string - threadTimestamp string +type CompleteUploadExternalParameters struct { + Files []FileSummary + Blocks Blocks + Channel string + InitialComment string + ThreadTimestamp string } -type completeUploadExternalResponse struct { +type CompleteUploadExternalResponse struct { SlackResponse Files []FileSummary `json:"files"` } @@ -304,6 +310,7 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter if params.Page != DEFAULT_FILES_PAGE { values.Add("page", strconv.Itoa(params.Page)) } + //lint:ignore S1002 - we want to explicitly check against the constant if params.ShowHidden != DEFAULT_FILES_SHOW_HIDDEN { values.Add("show_files_hidden_by_limit", strconv.FormatBool(params.ShowHidden)) } @@ -354,65 +361,6 @@ func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParamet return response.Files, ¶ms, nil } -// UploadFile uploads a file. -// -// Deprecated: Use [Client.UploadFileV2] instead. This will stop functioning on March 11, 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. This will stop functioning on March 11, 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 { @@ -495,20 +443,28 @@ func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) return &response.File, response.Comments, &response.Paging, nil } -// getUploadURLExternal gets a URL and fileID from slack which can later be used to upload a file. -func (api *Client) getUploadURLExternal(ctx context.Context, params getUploadURLExternalParameters) (*getUploadURLExternalResponse, error) { +// 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)}, + "filename": {params.FileName}, + "length": {strconv.Itoa(params.FileSize)}, } - if params.altText != "" { - values.Add("initial_comment", params.altText) + if params.AltTxt != "" { + values.Add("alt_txt", params.AltTxt) } - if params.snippetText != "" { - values.Add("thread_ts", params.snippetText) + if params.SnippetType != "" { + values.Add("snippet_type", params.SnippetType) } - response := &getUploadURLExternalResponse{} + response := &GetUploadURLExternalResponse{} err := api.postMethod(ctx, "files.getUploadURLExternal", values, response) if err != nil { return nil, err @@ -517,8 +473,9 @@ func (api *Client) getUploadURLExternal(ctx context.Context, params getUploadURL return response, response.Err() } -// uploadToURL uploads the file to the provided URL using post method -func (api *Client) uploadToURL(ctx context.Context, params uploadToURLParameters) (err error) { +// 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) @@ -531,28 +488,36 @@ func (api *Client) uploadToURL(ctx context.Context, params uploadToURLParameters return err } -// completeUploadExternal once files are uploaded, this completes the upload and shares it to the specified channel -func (api *Client) completeUploadExternal(ctx context.Context, fileID string, params completeUploadExternalParameters) (file *completeUploadExternalResponse, err error) { - request := []FileSummary{{ID: fileID, Title: params.title}} - requestBytes, err := json.Marshal(request) +// 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(requestBytes)}, + "files": {string(filesBytes)}, } - if params.channel != "" { - values.Add("channel_id", params.channel) + if params.Channel != "" { + values.Add("channel_id", params.Channel) } - if params.initialComment != "" { - values.Add("initial_comment", params.initialComment) + if params.InitialComment != "" { + values.Add("initial_comment", params.InitialComment) } - if params.threadTimestamp != "" { - values.Add("thread_ts", params.threadTimestamp) + if params.Blocks.BlockSet != nil && params.InitialComment == "" { + blocksBytes, err := json.Marshal(params.Blocks) + if err != nil { + return nil, err + } + values.Add("blocks", string(blocksBytes)) } - response := &completeUploadExternalResponse{} + 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 @@ -563,19 +528,19 @@ func (api *Client) completeUploadExternal(ctx context.Context, fileID string, pa return response, nil } -// UploadFileV2 uploads file to a given slack channel using 3 steps. -// For more details, see UploadFileV2Context documentation. -func (api *Client) UploadFileV2(params UploadFileV2Parameters) (*FileSummary, error) { - return api.UploadFileV2Context(context.Background(), params) +// UploadFile uploads file to a given slack channel using 3 steps. +// For more details, see UploadFileContext documentation. +func (api *Client) UploadFile(params UploadFileParameters) (*FileSummary, error) { + return api.UploadFileContext(context.Background(), params) } -// UploadFileV2Context uploads file to a given slack channel using 3 steps - +// UploadFileContext uploads file to a given slack channel using 3 steps - // 1. Get an upload URL using files.getUploadURLExternal API // 2. Send the file as a post to the URL provided by slack // 3. Complete the upload and share it to the specified channel using files.completeUploadExternal // // Slack Docs: https://api.slack.com/messaging/files#uploading_files -func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2Parameters) (file *FileSummary, err error) { +func (api *Client) UploadFileContext(ctx context.Context, params UploadFileParameters) (file *FileSummary, err error) { if params.Filename == "" { return nil, fmt.Errorf("file.upload.v2: filename cannot be empty") } @@ -583,17 +548,17 @@ func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2P return nil, fmt.Errorf("file.upload.v2: file size cannot be 0") } - u, err := api.getUploadURLExternal(ctx, getUploadURLExternalParameters{ - altText: params.AltTxt, - fileName: params.Filename, - fileSize: params.FileSize, - snippetText: params.SnippetText, + u, err := api.GetUploadURLExternalContext(ctx, GetUploadURLExternalParameters{ + AltTxt: params.AltTxt, + FileName: params.Filename, + FileSize: params.FileSize, + SnippetType: params.SnippetType, }) if err != nil { - return nil, err + return nil, fmt.Errorf("GetUploadURLExternal: %w", err) } - err = api.uploadToURL(ctx, uploadToURLParameters{ + err = api.UploadToURL(ctx, UploadToURLParameters{ UploadURL: u.UploadURL, Reader: params.Reader, File: params.File, @@ -601,17 +566,21 @@ func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2P Filename: params.Filename, }) if err != nil { - return nil, err - } - - c, err := api.completeUploadExternal(ctx, u.FileID, completeUploadExternalParameters{ - title: params.Title, - channel: params.Channel, - initialComment: params.InitialComment, - threadTimestamp: params.ThreadTimestamp, + return nil, fmt.Errorf("UploadToURL: %w", err) + } + + c, err := api.CompleteUploadExternalContext(ctx, CompleteUploadExternalParameters{ + Files: []FileSummary{{ + ID: u.FileID, + Title: params.Title, + }}, + Channel: params.Channel, + InitialComment: params.InitialComment, + ThreadTimestamp: params.ThreadTimestamp, + Blocks: params.Blocks, }) if err != nil { - return nil, err + return nil, fmt.Errorf("CompleteUploadExternal: %w", err) } if len(c.Files) != 1 { return nil, fmt.Errorf("file.upload.v2: something went wrong; received %d files instead of 1", len(c.Files)) diff --git a/vendor/github.com/slack-go/slack/function_execute.go b/vendor/github.com/slack-go/slack/function_execute.go index 4ec8f9f4c..97bc7e150 100644 --- a/vendor/github.com/slack-go/slack/function_execute.go +++ b/vendor/github.com/slack-go/slack/function_execute.go @@ -43,14 +43,13 @@ func (api *Client) FunctionCompleteSuccessContext(ctx context.Context, functionE option(r) } - endpoint := api.endpoint + "functions.completeSuccess" jsonData, err := json.Marshal(r) if err != nil { return err } response := &SlackResponse{} - if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { + if err := api.postJSONMethod(ctx, "functions.completeSuccess", api.token, jsonData, response); err != nil { return err } @@ -74,14 +73,13 @@ func (api *Client) FunctionCompleteErrorContext(ctx context.Context, functionExe } r.Error = errorMessage - endpoint := api.endpoint + "functions.completeError" jsonData, err := json.Marshal(r) if err != nil { return err } response := &SlackResponse{} - if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { + if err := api.postJSONMethod(ctx, "functions.completeError", api.token, jsonData, response); err != nil { return err } diff --git a/vendor/github.com/slack-go/slack/huddle.go b/vendor/github.com/slack-go/slack/huddle.go new file mode 100644 index 000000000..701236471 --- /dev/null +++ b/vendor/github.com/slack-go/slack/huddle.go @@ -0,0 +1,64 @@ +package slack + +// HuddleRoom represents a Slack huddle room as it appears in message events +// with subtype "huddle_thread". This is different from CallBlock which is used +// for external call integrations (Zoom, etc.). +type HuddleRoom struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + MediaServer string `json:"media_server,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + DateStart int64 `json:"date_start"` + DateEnd int64 `json:"date_end"` + Participants []string `json:"participants,omitempty"` + ParticipantHistory []string `json:"participant_history,omitempty"` + ParticipantsEvents map[string]HuddleParticipantEvent `json:"participants_events,omitempty"` + ParticipantsCameraOn []string `json:"participants_camera_on,omitempty"` + ParticipantsCameraOff []string `json:"participants_camera_off,omitempty"` + ParticipantsScreenshareOn []string `json:"participants_screenshare_on,omitempty"` + ParticipantsScreenshareOff []string `json:"participants_screenshare_off,omitempty"` + CanvasThreadTs string `json:"canvas_thread_ts,omitempty"` + ThreadRootTs string `json:"thread_root_ts,omitempty"` + Channels []string `json:"channels,omitempty"` + IsDMCall bool `json:"is_dm_call"` + WasRejected bool `json:"was_rejected"` + WasMissed bool `json:"was_missed"` + WasAccepted bool `json:"was_accepted"` + HasEnded bool `json:"has_ended"` + BackgroundID string `json:"background_id,omitempty"` + CanvasBackground string `json:"canvas_background,omitempty"` + IsPrewarmed bool `json:"is_prewarmed"` + IsScheduled bool `json:"is_scheduled"` + Recording *HuddleRecording `json:"recording,omitempty"` + Locale string `json:"locale,omitempty"` + AttachedFileIDs []string `json:"attached_file_ids,omitempty"` + MediaBackendType string `json:"media_backend_type,omitempty"` + DisplayID string `json:"display_id,omitempty"` + ExternalUniqueID string `json:"external_unique_id,omitempty"` + AppID string `json:"app_id,omitempty"` + CallFamily string `json:"call_family,omitempty"` + PendingInvitees map[string]any `json:"pending_invitees,omitempty"` + LastInviteStatusByUser map[string]any `json:"last_invite_status_by_user,omitempty"` + Knocks map[string]any `json:"knocks,omitempty"` + HuddleLink string `json:"huddle_link,omitempty"` +} + +// HuddleParticipantEvent tracks a participant's activity in a huddle. +type HuddleParticipantEvent struct { + UserTeam map[string]any `json:"user_team,omitempty"` + Joined bool `json:"joined"` + CameraOn bool `json:"camera_on"` + CameraOff bool `json:"camera_off"` + ScreenshareOn bool `json:"screenshare_on"` + ScreenshareOff bool `json:"screenshare_off"` +} + +// HuddleRecording contains recording status for a huddle. +type HuddleRecording struct { + CanRecordSummary string `json:"can_record_summary,omitempty"` + NoteTaking bool `json:"note_taking,omitempty"` + Summary bool `json:"summary,omitempty"` + SummaryStatus string `json:"summary_status,omitempty"` + Transcript bool `json:"transcript,omitempty"` + RecordingUser string `json:"recording_user,omitempty"` +} diff --git a/vendor/github.com/slack-go/slack/im.go b/vendor/github.com/slack-go/slack/im.go deleted file mode 100644 index 7c4bc2572..000000000 --- a/vendor/github.com/slack-go/slack/im.go +++ /dev/null @@ -1,21 +0,0 @@ -package slack - -type imChannel struct { - ID string `json:"id"` -} - -type imResponseFull struct { - NoOp bool `json:"no_op"` - AlreadyClosed bool `json:"already_closed"` - AlreadyOpen bool `json:"already_open"` - Channel imChannel `json:"channel"` - IMs []IM `json:"ims"` - History - SlackResponse -} - -// IM contains information related to the Direct Message channel -type IM struct { - Conversation - IsUserDeleted bool `json:"is_user_deleted"` -} diff --git a/vendor/github.com/slack-go/slack/info.go b/vendor/github.com/slack-go/slack/info.go index b06dffdbe..d5276f757 100644 --- a/vendor/github.com/slack-go/slack/info.go +++ b/vendor/github.com/slack-go/slack/info.go @@ -428,13 +428,16 @@ type Team struct { ID string `json:"id"` Name string `json:"name"` Domain string `json:"domain"` + Icons *Icons `json:"icon,omitempty"` } -// Icons XXX: needs further investigation +// Icons contains the image URLs for the team icons in various sizes type Icons struct { - Image36 string `json:"image_36,omitempty"` - Image48 string `json:"image_48,omitempty"` - Image72 string `json:"image_72,omitempty"` + Image36 string `json:"image_36,omitempty"` + Image48 string `json:"image_48,omitempty"` + Image72 string `json:"image_72,omitempty"` + Image132 string `json:"image_132,omitempty"` + Image230 string `json:"image_230,omitempty"` } // Info contains various details about the authenticated user and team. @@ -449,28 +452,3 @@ type infoResponseFull struct { Info SlackResponse } - -// GetBotByID is deprecated and returns nil -func (info Info) GetBotByID(botID string) *Bot { - return nil -} - -// GetUserByID is deprecated and returns nil -func (info Info) GetUserByID(userID string) *User { - return nil -} - -// GetChannelByID is deprecated and returns nil -func (info Info) GetChannelByID(channelID string) *Channel { - return nil -} - -// GetGroupByID is deprecated and returns nil -func (info Info) GetGroupByID(groupID string) *Group { - return nil -} - -// GetIMByID is deprecated and returns nil -func (info Info) GetIMByID(imID string) *IM { - return nil -} diff --git a/vendor/github.com/slack-go/slack/interactions.go b/vendor/github.com/slack-go/slack/interactions.go index 8c6414707..f658a72c2 100644 --- a/vendor/github.com/slack-go/slack/interactions.go +++ b/vendor/github.com/slack-go/slack/interactions.go @@ -3,6 +3,8 @@ package slack import ( "bytes" "encoding/json" + "errors" + "net/http" ) // InteractionType type of interactions @@ -33,30 +35,29 @@ const ( // 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"` - WorkflowStep InteractionWorkflowStep `json:"workflow_step"` + 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 @@ -75,6 +76,24 @@ 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) @@ -137,14 +156,6 @@ type Enterprise struct { Name string `json:"name"` } -type InteractionWorkflowStep struct { - WorkflowStepEditID string `json:"workflow_step_edit_id,omitempty"` - WorkflowID string `json:"workflow_id"` - StepID string `json:"step_id"` - Inputs *WorkflowStepInputs `json:"inputs,omitempty"` - Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` -} - // 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 { diff --git a/vendor/github.com/slack-go/slack/manifests.go b/vendor/github.com/slack-go/slack/manifests.go index 0a972a25d..59f6a70bc 100644 --- a/vendor/github.com/slack-go/slack/manifests.go +++ b/vendor/github.com/slack-go/slack/manifests.go @@ -187,11 +187,11 @@ type Display struct { // Settings is a group of settings corresponding to the Settings section of the app config pages. type Settings struct { - AllowedIPAddressRanges []string `json:"allowed_ip_address_ranges,omitempty" yaml:"allowed_ip_address_ranges,omitempty"` - EventSubscriptions EventSubscriptions `json:"event_subscriptions,omitempty" yaml:"event_subscriptions,omitempty"` - Interactivity Interactivity `json:"interactivity,omitempty" yaml:"interactivity,omitempty"` - OrgDeployEnabled bool `json:"org_deploy_enabled,omitempty" yaml:"org_deploy_enabled,omitempty"` - SocketModeEnabled bool `json:"socket_mode_enabled,omitempty" yaml:"socket_mode_enabled,omitempty"` + AllowedIPAddressRanges []string `json:"allowed_ip_address_ranges,omitempty" yaml:"allowed_ip_address_ranges,omitempty"` + EventSubscriptions *EventSubscriptions `json:"event_subscriptions,omitempty" yaml:"event_subscriptions,omitempty"` + Interactivity *Interactivity `json:"interactivity,omitempty" yaml:"interactivity,omitempty"` + OrgDeployEnabled bool `json:"org_deploy_enabled,omitempty" yaml:"org_deploy_enabled,omitempty"` + SocketModeEnabled bool `json:"socket_mode_enabled,omitempty" yaml:"socket_mode_enabled,omitempty"` } // EventSubscriptions is a group of settings that describe the Events API configuration @@ -269,8 +269,10 @@ type OAuthConfig struct { // OAuthScopes is a group of settings that describe permission scopes configuration type OAuthScopes struct { - Bot []string `json:"bot,omitempty" yaml:"bot,omitempty"` - User []string `json:"user,omitempty" yaml:"user,omitempty"` + Bot []string `json:"bot,omitempty" yaml:"bot,omitempty"` + User []string `json:"user,omitempty" yaml:"user,omitempty"` + BotOptional []string `json:"bot_optional,omitempty" yaml:"bot_optional,omitempty"` + UserOptional []string `json:"user_optional,omitempty" yaml:"user_optional,omitempty"` } // ManifestResponse is the response returned by the API for apps.manifest.x endpoints diff --git a/vendor/github.com/slack-go/slack/messages.go b/vendor/github.com/slack-go/slack/messages.go index 13a1ede98..332e15e99 100644 --- a/vendor/github.com/slack-go/slack/messages.go +++ b/vendor/github.com/slack-go/slack/messages.go @@ -17,6 +17,10 @@ type Message struct { Msg SubMessage *Msg `json:"message,omitempty"` PreviousMessage *Msg `json:"previous_message,omitempty"` + // Root is the message that was broadcast to the channel when the SubType is + // thread_broadcast. If this is not a thread_broadcast message event, this + // value is nil. + Root *Msg `json:"root,omitempty"` } // Msg SubTypes (https://api.slack.com/events/message) @@ -49,6 +53,7 @@ const ( MsgSubTypeUnpinnedItem = "unpinned_item" // [RTM] An item was unpinned from a channel MsgSubTypeEkmAccessDenied = "ekm_access_denied" // [Events API, RTM] Message content redacted due to Enterprise Key Management (EKM) MsgSubTypeChannelPostingPermissions = "channel_posting_permissions" // [Events API, RTM] The posting permissions for a channel changed + MsgSubTypeAssistantAppThread = "assistant_app_thread" // [Events API, RTM] The message is an app assistant thread ) // Msg contains information about a slack message @@ -83,6 +88,16 @@ type Msg struct { Icons *Icon `json:"icons,omitempty"` BotProfile *BotProfile `json:"bot_profile,omitempty"` + // These tend to be present in some of the messages, especially when triggered through + // a workflow. The API documentation is not clear about which ones are present in + // which messages, so we make them all optional. + // + // I'm adding them here for completeness but none of the Slack official libraries seem + // to support these fields. Be warned that they may be removed in future versions of + // the API, and that they may not be present in all messages. + TriggerID string `json:"trigger_id,omitempty"` + WorkflowID string `json:"workflow_id,omitempty"` + // channel_join, group_join Inviter string `json:"inviter,omitempty"` diff --git a/vendor/github.com/slack-go/slack/metadata.go b/vendor/github.com/slack-go/slack/metadata.go index a8c065046..a1e34e990 100644 --- a/vendor/github.com/slack-go/slack/metadata.go +++ b/vendor/github.com/slack-go/slack/metadata.go @@ -2,6 +2,37 @@ package slack // SlackMetadata https://api.slack.com/reference/metadata type SlackMetadata struct { - EventType string `json:"event_type"` - EventPayload map[string]interface{} `json:"event_payload"` + EventType string `json:"event_type"` + EventPayload map[string]any `json:"event_payload"` +} + +// Work Object entity type constants. +// See https://docs.slack.dev/messaging/work-objects/ +const ( + EntityTypeTask = "slack#/entities/task" + EntityTypeFile = "slack#/entities/file" + EntityTypeItem = "slack#/entities/item" + EntityTypeIncident = "slack#/entities/incident" + EntityTypeContentItem = "slack#/entities/content_item" +) + +// WorkObjectExternalRef represents an external reference for a Work Object +type WorkObjectExternalRef struct { + ID string `json:"id"` + Type string `json:"type,omitempty"` +} + +// WorkObjectEntity represents a single Work Object entity +type WorkObjectEntity struct { + AppUnfurlURL string `json:"app_unfurl_url,omitempty"` + URL string `json:"url"` + ExternalRef WorkObjectExternalRef `json:"external_ref"` + EntityType string `json:"entity_type"` + EntityPayload map[string]interface{} `json:"entity_payload"` +} + +// WorkObjectMetadata represents the metadata for Work Objects +// Used in chat.unfurl and chat.postMessage for Work Objects support +type WorkObjectMetadata struct { + Entities []WorkObjectEntity `json:"entities"` } diff --git a/vendor/github.com/slack-go/slack/migration.go b/vendor/github.com/slack-go/slack/migration.go new file mode 100644 index 000000000..fa6690ee3 --- /dev/null +++ b/vendor/github.com/slack-go/slack/migration.go @@ -0,0 +1,40 @@ +package slack + +import ( + "context" + "net/url" +) + +type migrationExchangeResponseFull struct { + TeamID string `json:"team_id"` + ToOld bool `json:"to_old"` + EnterpriseID string `json:"enterprise_id"` + UserIDMap map[string]string `json:"user_id_map"` + InvalidUserIDs []string `json:"invalid_user_ids"` + SlackResponse +} + +// MigrationExchange for Enterprise Grid workspaces, map local user IDs to global user IDs +func (api *Client) MigrationExchange(ctx context.Context, teamID string, toOld bool, users []string) (map[string]string, []string, error) { + values := url.Values{ + "users": users, + } + if teamID != "" { + values.Add("team_id", teamID) + } + if toOld { + values.Add("to_old", "true") + } + + response := &migrationExchangeResponseFull{} + err := api.getMethod(ctx, "migration.exchange", api.token, values, response) + if err != nil { + return nil, nil, err + } + + if err := response.Err(); err != nil { + return nil, nil, err + } + + return response.UserIDMap, response.InvalidUserIDs, nil +} diff --git a/vendor/github.com/slack-go/slack/misc.go b/vendor/github.com/slack-go/slack/misc.go index df779c1ad..46060a720 100644 --- a/vendor/github.com/slack-go/slack/misc.go +++ b/vendor/github.com/slack-go/slack/misc.go @@ -19,11 +19,220 @@ import ( "time" ) +// AppsManifestCreateResponseError ("/apps.manifest.create") +type AppsManifestCreateResponseError struct { + Code string `json:"code,omitempty"` + Message string `json:"message"` + Pointer string `json:"pointer"` + RelatedComponent string `json:"related_component,omitempty"` +} + +// ConversationsInviteResponseError ("/conversations.invite") +type ConversationsInviteResponseError struct { + Error string `json:"error"` + Ok bool `json:"ok"` + User string `json:"user"` +} + +func (t ConversationsInviteResponseError) Err() error { + if !t.Ok { + return fmt.Errorf("conversations invite error (user: %s): %s", t.User, t.Error) + } + return nil +} + +// SlackResponseErrors represents a union type for different error structures +type SlackResponseErrors struct { + AppsManifestCreateResponseError *AppsManifestCreateResponseError `json:"-"` + ConversationsInviteResponseError *ConversationsInviteResponseError `json:"-"` + Message *string `json:"-"` +} + +// MarshalJSON implements custom marshaling for SlackResponseErrors +func (e SlackResponseErrors) MarshalJSON() ([]byte, error) { + if e.AppsManifestCreateResponseError != nil { + return json.Marshal(e.AppsManifestCreateResponseError) + } + if e.ConversationsInviteResponseError != nil { + return json.Marshal(e.ConversationsInviteResponseError) + } + if e.Message != nil { + return json.Marshal(*e.Message) + } + return json.Marshal(nil) +} + +// UnmarshalJSON implements custom unmarshaling for SlackResponseErrors +func (e *SlackResponseErrors) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte("null")) { + return nil + } + + // Try to determine the error type by checking for unique fields + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + // If we can't unmarshal as object, try as string (fallback case) + // + // For more details on this specific problem look up issue + // https://github.com/slack-go/slack/issues/1446. + var stringError string + if stringErr := json.Unmarshal(data, &stringError); stringErr == nil { + e.Message = &stringError + return nil + } + return err + } + + if _, hasPointer := raw["pointer"]; hasPointer { + if _, hasMessage := raw["message"]; hasMessage { + var amc AppsManifestCreateResponseError + if err := json.Unmarshal(data, &amc); err != nil { + return err + } + e.AppsManifestCreateResponseError = &amc + return nil + } + } + + if _, hasUser := raw["user"]; hasUser { + if _, hasError := raw["error"]; hasError { + if _, hasOk := raw["ok"]; hasOk { + var ci ConversationsInviteResponseError + if err := json.Unmarshal(data, &ci); err != nil { + return err + } + e.ConversationsInviteResponseError = &ci + return nil + } + } + } + + return fmt.Errorf("unknown error structure: %s", string(data)) +} + // SlackResponse handles parsing out errors from the web api. type SlackResponse struct { - Ok bool `json:"ok"` - Error string `json:"error"` - ResponseMetadata ResponseMetadata `json:"response_metadata"` + Ok bool `json:"ok"` + Error string `json:"error"` + Warning string `json:"warning"` + Errors []SlackResponseErrors `json:"errors,omitempty"` + ResponseMetadata ResponseMetadata `json:"response_metadata"` +} + +// Warn returns warning information from the API response, or nil if there +// are no warnings. +func (t SlackResponse) Warn() *Warning { + if t.Warning == "" && len(t.ResponseMetadata.Warnings) == 0 { + return nil + } + return &Warning{ + Codes: strings.Split(t.Warning, ","), + Warnings: t.ResponseMetadata.Warnings, + } +} + +// warner is satisfied by any response type that can report warnings. +type warner interface { + Warn() *Warning +} + +// Warning provides warning information from the web API. +// https://docs.slack.dev/apis/web-api/#responses +type Warning struct { + Codes []string + Warnings []string +} + +// httpHeaderSetter is satisfied by response types that can store HTTP +// response headers. The response parser checks for this interface and +// injects headers before JSON decoding. +type httpHeaderSetter interface { + setHTTPResponseHeaders(http.Header) +} + +// responseHeaders is a mix-in for internal response types that need to +// capture HTTP response headers. Embedded in types like authTestResponseFull +// so the parser can store headers that are then propagated to the public +// response type (e.g. AuthTestResponse.Header). +type responseHeaders struct { + header http.Header +} + +func (r *responseHeaders) setHTTPResponseHeaders(h http.Header) { r.header = h } + +// KickUserFromConversationSlackResponse is a variant of SlackResponse that can handle the case where +// "errors" can be either an empty object {} or an array of errors. +// This addresses issue #1446 where conversations.kick endpoint returns {"ok":true,"errors":{}} +type KickUserFromConversationSlackResponse struct { + Ok bool `json:"ok"` + Error string `json:"error"` + Warning string `json:"warning"` + Errors []SlackResponseErrors `json:"-"` + ResponseMetadata ResponseMetadata `json:"response_metadata"` +} + +// UnmarshalJSON implements custom unmarshaling for KickUserFromConversationSlackResponse to handle +// the case where "errors" can be either an empty object {} or an array of errors +func (s *KickUserFromConversationSlackResponse) UnmarshalJSON(data []byte) error { + // First, unmarshal everything except errors + type Alias KickUserFromConversationSlackResponse + aux := &struct { + *Alias + ErrorsRaw json.RawMessage `json:"errors,omitempty"` + }{ + Alias: (*Alias)(s), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + // Handle the errors field + if len(aux.ErrorsRaw) > 0 { + // Check if it's an empty object by looking for just "{}" + trimmed := bytes.TrimSpace(aux.ErrorsRaw) + if bytes.Equal(trimmed, []byte("{}")) { + // Empty object, leave errors as nil/empty slice + s.Errors = nil + } else { + // Try to unmarshal as array of errors + var errors []SlackResponseErrors + if err := json.Unmarshal(aux.ErrorsRaw, &errors); err != nil { + return err + } + s.Errors = errors + } + } + + return nil +} + +// Warn returns warning information from the API response, or nil if there +// are no warnings. +func (s KickUserFromConversationSlackResponse) Warn() *Warning { + if s.Warning == "" && len(s.ResponseMetadata.Warnings) == 0 { + return nil + } + return &Warning{ + Codes: strings.Split(s.Warning, ","), + Warnings: s.ResponseMetadata.Warnings, + } +} + +// Err returns any API error present in the response. +func (s KickUserFromConversationSlackResponse) Err() error { + if s.Ok { + return nil + } + + // handle pure text based responses like chat.post + // which while they have a slack response in their data structure + // it doesn't actually get set during parsing. + if strings.TrimSpace(s.Error) == "" { + return nil + } + + return SlackErrorResponse{Err: s.Error, Errors: s.Errors, ResponseMetadata: s.ResponseMetadata} } func (t SlackResponse) Err() error { @@ -38,12 +247,13 @@ func (t SlackResponse) Err() error { return nil } - return SlackErrorResponse{Err: t.Error, ResponseMetadata: t.ResponseMetadata} + return SlackErrorResponse{Err: t.Error, Errors: t.Errors, ResponseMetadata: t.ResponseMetadata} } // SlackErrorResponse brings along the metadata of errors returned by the Slack API. type SlackErrorResponse struct { Err string + Errors []SlackResponseErrors ResponseMetadata ResponseMetadata } @@ -110,7 +320,7 @@ func formReq(ctx context.Context, endpoint string, values url.Values) (req *http return req, nil } -func jsonReq(ctx context.Context, endpoint string, body interface{}) (req *http.Request, err error) { +func jsonReq(ctx context.Context, endpoint string, body any) (req *http.Request, err error) { buffer := bytes.NewBuffer([]byte{}) if err = json.NewEncoder(buffer).Encode(body); err != nil { return nil, err @@ -124,20 +334,7 @@ func jsonReq(ctx context.Context, endpoint string, body interface{}) (req *http. return req, nil } -func parseResponseBody(body io.ReadCloser, intf interface{}, d Debug) error { - response, err := io.ReadAll(body) - if err != nil { - return err - } - - if d.Debug() { - d.Debugln("parseResponseBody", string(response)) - } - - return json.Unmarshal(response, intf) -} - -func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname, token string, values url.Values, intf interface{}, d Debug) error { +func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname, token string, values url.Values, intf any, d Debug) error { fullpath, err := filepath.Abs(fpath) if err != nil { return err @@ -151,7 +348,7 @@ func postLocalWithMultipartResponse(ctx context.Context, client httpClient, meth return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, token, values, file, intf, d) } -func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname, token string, values url.Values, r io.Reader, intf interface{}, d Debug) error { +func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname, token string, values url.Values, r io.Reader, intf any, d Debug) error { pipeReader, pipeWriter := io.Pipe() wr := multipart.NewWriter(pipeWriter) @@ -220,61 +417,67 @@ func createFormFields(mw *multipart.Writer, values url.Values) error { return nil } -func doPost(ctx context.Context, client httpClient, req *http.Request, parser responseParser, d Debug) error { +func doPost(client httpClient, req *http.Request, parser responseParser, d Debug) (http.Header, error) { resp, err := client.Do(req) if err != nil { - return err + return nil, err } defer resp.Body.Close() - err = checkStatusCode(resp, d) - if err != nil { - return err + if err = checkStatusCode(resp, d); err != nil { + return nil, err } - return parser(resp) + return resp.Header, parser(resp) } // post JSON. -func postJSON(ctx context.Context, client httpClient, endpoint, token string, json []byte, intf interface{}, d Debug) error { - reqBody := bytes.NewBuffer(json) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reqBody) +func postJSON(ctx context.Context, client httpClient, endpoint, token string, jsonBody []byte, intf any, d Debug) (http.Header, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(jsonBody)) if err != nil { - return err + return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - - return doPost(ctx, client, req, newJSONParser(intf), d) + // allow retry client to re-send the request body on 429/5xx. + req.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(jsonBody)), nil + } + return doPost(client, req, newJSONParser(intf), d) } // post a url encoded form. -func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d Debug) error { - reqBody := strings.NewReader(values.Encode()) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reqBody) +func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf any, d Debug) (http.Header, error) { + body := values.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(body)) if err != nil { - return err + return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - return doPost(ctx, client, req, newJSONParser(intf), d) + // allow retry client to re-send the request body on 429/5xx. + req.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader(body)), nil + } + return doPost(client, req, newJSONParser(intf), d) } -func getResource(ctx context.Context, client httpClient, endpoint, token string, values url.Values, intf interface{}, d Debug) error { +func getResource(ctx context.Context, client httpClient, endpoint, token string, values url.Values, intf any, d Debug) (http.Header, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { - return err + return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.URL.RawQuery = values.Encode() - return doPost(ctx, client, req, newJSONParser(intf), d) + return doPost(client, req, newJSONParser(intf), d) } -func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d Debug) error { +func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf any, d Debug) error { endpoint := fmt.Sprintf(WEBAPIURLFormat, teamName, method, time.Now().Unix()) - return postForm(ctx, client, endpoint, values, intf, d) + _, err := postForm(ctx, client, endpoint, values, intf, d) + return err } func logResponse(resp *http.Response, d Debug) error { @@ -297,16 +500,8 @@ func okJSONHandler(rw http.ResponseWriter, r *http.Request) { rw.Write(response) } -// timerReset safely reset a timer, see time.Timer.Reset for details. -func timerReset(t *time.Timer, d time.Duration) { - if !t.Stop() { - <-t.C - } - t.Reset(d) -} - func checkStatusCode(resp *http.Response, d Debug) error { - if resp.StatusCode == http.StatusTooManyRequests { + if resp.StatusCode == http.StatusTooManyRequests && resp.Header.Get("Retry-After") != "" { retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64) if err != nil { return err @@ -325,17 +520,24 @@ func checkStatusCode(resp *http.Response, d Debug) error { type responseParser func(*http.Response) error -func newJSONParser(dst interface{}) responseParser { +func newJSONParser(dst any) responseParser { return func(resp *http.Response) error { if dst == nil { return nil } + if hs, ok := dst.(httpHeaderSetter); ok { + hs.setHTTPResponseHeaders(resp.Header.Clone()) + } return json.NewDecoder(resp.Body).Decode(dst) } } -func newTextParser(dst interface{}) responseParser { +func newTextParser(dst any) responseParser { return func(resp *http.Response) error { + if dst == nil { + return nil + } + b, err := io.ReadAll(resp.Body) if err != nil { return err @@ -349,7 +551,7 @@ func newTextParser(dst interface{}) responseParser { } } -func newContentTypeParser(dst interface{}) responseParser { +func newContentTypeParser(dst any) responseParser { return func(req *http.Response) (err error) { var ( ctype string @@ -363,6 +565,10 @@ func newContentTypeParser(dst interface{}) responseParser { case "application/json": return newJSONParser(dst)(req) default: + // newTextParser doesn't use dst, so capture headers here. + if hs, ok := dst.(httpHeaderSetter); ok { + hs.setHTTPResponseHeaders(req.Header.Clone()) + } return newTextParser(dst)(req) } } diff --git a/vendor/github.com/slack-go/slack/mise.toml b/vendor/github.com/slack-go/slack/mise.toml new file mode 100644 index 000000000..912c02c98 --- /dev/null +++ b/vendor/github.com/slack-go/slack/mise.toml @@ -0,0 +1,4 @@ +[tools] +go = "1.25" +golangci-lint = "2.10.1" +"go:honnef.co/go/tools/cmd/staticcheck" = "2026.1" diff --git a/vendor/github.com/slack-go/slack/oauth.go b/vendor/github.com/slack-go/slack/oauth.go index 0c77eca40..f98284d23 100644 --- a/vendor/github.com/slack-go/slack/oauth.go +++ b/vendor/github.com/slack-go/slack/oauth.go @@ -2,6 +2,9 @@ package slack import ( "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" "net/url" ) @@ -79,16 +82,47 @@ type OpenIDConnectResponse struct { SlackResponse } +type oauthConfig struct { + apiURL string + codeVerifier string +} + +// OAuthOption configures package-level OAuth functions. +type OAuthOption func(*oauthConfig) + +// OAuthOptionAPIURL overrides the default Slack API URL. Useful for testing. +func OAuthOptionAPIURL(url string) OAuthOption { + return func(c *oauthConfig) { c.apiURL = url } +} + +// OAuthOptionCodeVerifier sets the PKCE code_verifier for the OAuth token exchange. +// Use this when your authorization request included a code_challenge. +func OAuthOptionCodeVerifier(verifier string) OAuthOption { + return func(c *oauthConfig) { c.codeVerifier = verifier } +} + +func resolveOAuthConfig(opts []OAuthOption) oauthConfig { + c := oauthConfig{apiURL: APIURL} + for _, o := range opts { + o(&c) + } + return c +} + +func resolveOAuthAPIURL(opts []OAuthOption) string { + return resolveOAuthConfig(opts).apiURL +} + // GetOAuthToken retrieves an AccessToken. // For more details, see GetOAuthTokenContext documentation. -func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) { - return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, err error) { + return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } // GetOAuthTokenContext retrieves an AccessToken with a custom context. // For more details, see GetOAuthResponseContext documentation. -func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) { - response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI) +func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, err error) { + response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI, opts...) if err != nil { return "", "", err } @@ -97,14 +131,14 @@ func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clie // GetBotOAuthToken retrieves top-level and bot AccessToken - https://api.slack.com/legacy/oauth#bot_user_access_tokens // For more details, see GetBotOAuthTokenContext documentation. -func GetBotOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, bot OAuthResponseBot, err error) { - return GetBotOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +func GetBotOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, bot OAuthResponseBot, err error) { + return GetBotOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } // GetBotOAuthTokenContext retrieves top-level and bot AccessToken with a custom context. // For more details, see GetOAuthResponseContext documentation. -func GetBotOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, bot OAuthResponseBot, err error) { - response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI) +func GetBotOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, bot OAuthResponseBot, err error) { + response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI, opts...) if err != nil { return "", "", OAuthResponseBot{}, err } @@ -113,13 +147,13 @@ func GetBotOAuthTokenContext(ctx context.Context, client httpClient, clientID, c // GetOAuthResponse retrieves OAuth response. // For more details, see GetOAuthResponseContext documentation. -func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) { - return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthResponse, err error) { + return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } // GetOAuthResponseContext retrieves OAuth response with custom context. // Slack API docs: https://api.slack.com/methods/oauth.access -func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) { +func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthResponse, err error) { values := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, @@ -127,7 +161,7 @@ func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, c "redirect_uri": {redirectURI}, } response := &OAuthResponse{} - if err = postForm(ctx, client, APIURL+"oauth.access", values, response, discard{}); err != nil { + if _, err = postForm(ctx, client, resolveOAuthAPIURL(opts)+"oauth.access", values, response, discard{}); err != nil { return nil, err } return response, response.Err() @@ -135,21 +169,28 @@ func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, c // GetOAuthV2Response gets a V2 OAuth access token response. // For more details, see GetOAuthV2ResponseContext documentation. -func GetOAuthV2Response(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthV2Response, err error) { - return GetOAuthV2ResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +func GetOAuthV2Response(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthV2Response, err error) { + return GetOAuthV2ResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } // GetOAuthV2ResponseContext with a context, gets a V2 OAuth access token response. +// For PKCE flows, pass OAuthOptionCodeVerifier and an empty clientSecret. // Slack API docs: https://api.slack.com/methods/oauth.v2.access -func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthV2Response, err error) { +func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthV2Response, err error) { + cfg := resolveOAuthConfig(opts) values := url.Values{ - "client_id": {clientID}, - "client_secret": {clientSecret}, - "code": {code}, - "redirect_uri": {redirectURI}, + "client_id": {clientID}, + "code": {code}, + "redirect_uri": {redirectURI}, + } + if clientSecret != "" { + values.Set("client_secret", clientSecret) + } + if cfg.codeVerifier != "" { + values.Set("code_verifier", cfg.codeVerifier) } response := &OAuthV2Response{} - if err = postForm(ctx, client, APIURL+"oauth.v2.access", values, response, discard{}); err != nil { + if _, err = postForm(ctx, client, cfg.apiURL+"oauth.v2.access", values, response, discard{}); err != nil { return nil, err } return response, response.Err() @@ -157,21 +198,92 @@ func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID, // RefreshOAuthV2Token with a context, gets a V2 OAuth access token response. // For more details, see RefreshOAuthV2TokenContext documentation. -func RefreshOAuthV2Token(client httpClient, clientID, clientSecret, refreshToken string) (resp *OAuthV2Response, err error) { - return RefreshOAuthV2TokenContext(context.Background(), client, clientID, clientSecret, refreshToken) +func RefreshOAuthV2Token(client httpClient, clientID, clientSecret, refreshToken string, opts ...OAuthOption) (resp *OAuthV2Response, err error) { + return RefreshOAuthV2TokenContext(context.Background(), client, clientID, clientSecret, refreshToken, opts...) } // RefreshOAuthV2TokenContext with a context, gets a V2 OAuth access token response. +// For PKCE public clients, pass an empty clientSecret. // Slack API docs: https://api.slack.com/methods/oauth.v2.access -func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID, clientSecret, refreshToken string) (resp *OAuthV2Response, err error) { +func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID, clientSecret, refreshToken string, opts ...OAuthOption) (resp *OAuthV2Response, err error) { values := url.Values{ "client_id": {clientID}, - "client_secret": {clientSecret}, "refresh_token": {refreshToken}, "grant_type": {"refresh_token"}, } + if clientSecret != "" { + values.Set("client_secret", clientSecret) + } response := &OAuthV2Response{} - if err = postForm(ctx, client, APIURL+"oauth.v2.access", values, response, discard{}); err != nil { + if _, err = postForm(ctx, client, resolveOAuthAPIURL(opts)+"oauth.v2.access", values, response, discard{}); err != nil { + return nil, err + } + return response, response.Err() +} + +// OpenIDConnectUserInfoResponse contains the response from openid.connect.userInfo. +// +// Some of the fields in the response to this method are preceded with https://slack.com/. +// These fields are Slack-specific, and they're from the perspective of Slack. +type OpenIDConnectUserInfoResponse struct { + Ok bool `json:"ok"` + + Sub string `json:"sub"` + + UserID string `json:"https://slack.com/user_id"` + TeamID string `json:"https://slack.com/team_id"` + + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + DateEmailVerified int64 `json:"date_email_verified"` + + Name string `json:"name"` + Picture string `json:"picture"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + Locale string `json:"locale"` + + TeamName string `json:"https://slack.com/team_name"` + TeamDomain string `json:"https://slack.com/team_domain"` + TeamImage34 string `json:"https://slack.com/team_image_34"` + TeamImage44 string `json:"https://slack.com/team_image_44"` + TeamImage68 string `json:"https://slack.com/team_image_68"` + TeamImage88 string `json:"https://slack.com/team_image_88"` + TeamImage102 string `json:"https://slack.com/team_image_102"` + TeamImage132 string `json:"https://slack.com/team_image_132"` + TeamImage230 string `json:"https://slack.com/team_image_230"` + + // `TeamImageDefault` indicates whether the image is a default one (true), or someone + // uploaded their own (false). + TeamImageDefault bool `json:"https://slack.com/team_image_default"` + + UserImage24 string `json:"https://slack.com/user_image_24"` + UserImage32 string `json:"https://slack.com/user_image_32"` + UserImage48 string `json:"https://slack.com/user_image_48"` + UserImage72 string `json:"https://slack.com/user_image_72"` + UserImage192 string `json:"https://slack.com/user_image_192"` + UserImage512 string `json:"https://slack.com/user_image_512"` + UserImage1024 string `json:"https://slack.com/user_image_1024"` + UserImageOriginal string `json:"https://slack.com/user_image_original"` + + SlackResponse +} + +// GetOpenIDConnectUserInfo returns the user info for the token. +// For more details, see GetOpenIDConnectUserInfoContext documentation. +func (api *Client) GetOpenIDConnectUserInfo() (*OpenIDConnectUserInfoResponse, error) { + return api.GetOpenIDConnectUserInfoContext(context.Background()) +} + +// GetOpenIDConnectUserInfoContext returns identity information about the user associated with the token. +// Slack API docs: https://docs.slack.dev/reference/methods/openid.connect.userInfo +func (api *Client) GetOpenIDConnectUserInfoContext(ctx context.Context) (*OpenIDConnectUserInfoResponse, error) { + values := url.Values{ + "token": {api.token}, + } + response := &OpenIDConnectUserInfoResponse{} + err := api.postMethod(ctx, "openid.connect.userInfo", values, response) + if err != nil { return nil, err } return response, response.Err() @@ -179,13 +291,13 @@ func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID // GetOpenIDConnectToken exchanges a temporary OAuth verifier code for an access token for Sign in with Slack. // For more details, see GetOpenIDConnectTokenContext documentation. -func GetOpenIDConnectToken(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OpenIDConnectResponse, err error) { - return GetOpenIDConnectTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +func GetOpenIDConnectToken(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OpenIDConnectResponse, err error) { + return GetOpenIDConnectTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } // GetOpenIDConnectTokenContext with a context, gets an access token for Sign in with Slack. // Slack API docs: https://api.slack.com/methods/openid.connect.token -func GetOpenIDConnectTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OpenIDConnectResponse, err error) { +func GetOpenIDConnectTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OpenIDConnectResponse, err error) { values := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, @@ -193,8 +305,26 @@ func GetOpenIDConnectTokenContext(ctx context.Context, client httpClient, client "redirect_uri": {redirectURI}, } response := &OpenIDConnectResponse{} - if err = postForm(ctx, client, APIURL+"openid.connect.token", values, response, discard{}); err != nil { + if _, err = postForm(ctx, client, resolveOAuthAPIURL(opts)+"openid.connect.token", values, response, discard{}); err != nil { return nil, err } return response, response.Err() } + +// GenerateCodeVerifier creates a cryptographically random PKCE code verifier +// string suitable for use with OAuth 2.0 PKCE flows. The returned string is +// 43 characters of URL-safe base64 (no padding). +func GenerateCodeVerifier() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +// GenerateCodeChallenge creates a PKCE code challenge from a code verifier +// using the S256 method (SHA-256 hash, base64url-encoded without padding). +func GenerateCodeChallenge(verifier string) string { + h := sha256.Sum256([]byte(verifier)) + return base64.RawURLEncoding.EncodeToString(h[:]) +} diff --git a/vendor/github.com/slack-go/slack/reactions.go b/vendor/github.com/slack-go/slack/reactions.go index 240f5ba92..18befa699 100644 --- a/vendor/github.com/slack-go/slack/reactions.go +++ b/vendor/github.com/slack-go/slack/reactions.go @@ -33,44 +33,53 @@ func NewGetReactionsParameters() GetReactionsParameters { } type getReactionsResponseFull struct { - Type string - M struct { - Reactions []ItemReaction + Type string + Channel string `json:"channel,omitempty"` // channel is at the root level for message types + M struct { + *Message // message structure already contains reactions } `json:"message"` F struct { + *File Reactions []ItemReaction } `json:"file"` FC struct { + *Comment Reactions []ItemReaction } `json:"comment"` SlackResponse } -func (res getReactionsResponseFull) extractReactions() []ItemReaction { - switch res.Type { +func (res getReactionsResponseFull) extractReactedItem() ReactedItem { + item := ReactedItem{} + item.Type = res.Type + + switch item.Type { case "message": - return res.M.Reactions + item.Channel = res.Channel + item.Message = res.M.Message + item.Reactions = res.M.Reactions case "file": - return res.F.Reactions + item.File = res.F.File + item.Reactions = res.F.Reactions case "file_comment": - return res.FC.Reactions + item.File = res.F.File + item.Comment = res.FC.Comment + item.Reactions = res.FC.Reactions } - return []ItemReaction{} + return item } const ( - DEFAULT_REACTIONS_USER = "" - DEFAULT_REACTIONS_COUNT = 100 - DEFAULT_REACTIONS_PAGE = 1 - DEFAULT_REACTIONS_FULL = false + DEFAULT_REACTIONS_USER = "" + DEFAULT_REACTIONS_FULL = false ) // ListReactionsParameters is the inputs to find all reactions by a user. type ListReactionsParameters struct { User string TeamID string - Count int - Page int + Cursor string + Limit int Full bool } @@ -78,10 +87,8 @@ type ListReactionsParameters struct { // performed by a user. func NewListReactionsParameters() ListReactionsParameters { return ListReactionsParameters{ - User: DEFAULT_REACTIONS_USER, - Count: DEFAULT_REACTIONS_COUNT, - Page: DEFAULT_REACTIONS_PAGE, - Full: DEFAULT_REACTIONS_FULL, + User: DEFAULT_REACTIONS_USER, + Full: DEFAULT_REACTIONS_FULL, } } @@ -101,8 +108,8 @@ type listReactionsResponseFull struct { Reactions []ItemReaction } `json:"comment"` } - Paging `json:"paging"` SlackResponse + ResponseMetadata `json:"response_metadata"` } func (res listReactionsResponseFull) extractReactedItems() []ReactedItem { @@ -200,15 +207,15 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item return response.Err() } -// GetReactions returns details about the reactions on an item. +// GetReactions returns item and details about the reactions on an item. // For more details, see GetReactionsContext documentation. -func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { +func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) (ReactedItem, error) { return api.GetReactionsContext(context.Background(), item, params) } -// GetReactionsContext returns details about the reactions on an item with a custom context. +// GetReactionsContext returns item and details about the reactions on an item with a custom context. // Slack API docs: https://api.slack.com/methods/reactions.get -func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { +func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) (ReactedItem, error) { values := url.Values{ "token": {api.token}, } @@ -224,31 +231,31 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params if item.Comment != "" { values.Set("file_comment", item.Comment) } - if params.Full != DEFAULT_REACTIONS_FULL { + if params.Full { values.Set("full", strconv.FormatBool(params.Full)) } response := &getReactionsResponseFull{} if err := api.postMethod(ctx, "reactions.get", values, response); err != nil { - return nil, err + return ReactedItem{}, err } if err := response.Err(); err != nil { - return nil, err + return ReactedItem{}, err } - return response.extractReactions(), nil + return response.extractReactedItem(), nil } // ListReactions returns information about the items a user reacted to. // For more details, see ListReactionsContext documentation. -func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) { +func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, string, error) { return api.ListReactionsContext(context.Background(), params) } // ListReactionsContext returns information about the items a user reacted to with a custom context. // Slack API docs: https://api.slack.com/methods/reactions.list -func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) { +func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, string, error) { values := url.Values{ "token": {api.token}, } @@ -258,25 +265,25 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction if params.TeamID != "" { values.Add("team_id", params.TeamID) } - if params.Count != DEFAULT_REACTIONS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) + if params.Cursor != "" { + values.Add("cursor", params.Cursor) } - if params.Page != DEFAULT_REACTIONS_PAGE { - values.Add("page", strconv.Itoa(params.Page)) + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) } - if params.Full != DEFAULT_REACTIONS_FULL { + if params.Full { values.Add("full", strconv.FormatBool(params.Full)) } response := &listReactionsResponseFull{} err := api.postMethod(ctx, "reactions.list", values, response) if err != nil { - return nil, nil, err + return nil, "", err } if err := response.Err(); err != nil { - return nil, nil, err + return nil, "", err } - return response.extractReactedItems(), &response.Paging, nil + return response.extractReactedItems(), response.ResponseMetadata.Cursor, nil } diff --git a/vendor/github.com/slack-go/slack/remotefiles.go b/vendor/github.com/slack-go/slack/remotefiles.go index 42639a178..51d3e8565 100644 --- a/vendor/github.com/slack-go/slack/remotefiles.go +++ b/vendor/github.com/slack-go/slack/remotefiles.go @@ -56,6 +56,10 @@ type RemoteFile struct { // ExternalID is a user defined GUID, ExternalURL is where the remote file can be accessed, // and Title is the name of the file. // +// PreviewImage is a file path to upload as preview. PreviewImageReader is an io.Reader +// alternative. When using PreviewImageReader, set PreviewImageName to specify the filename +// with proper extension (e.g., "preview.jpg") to preserve image format. +// // For more details: // https://api.slack.com/methods/files.remote.add type RemoteFileParameters struct { @@ -66,6 +70,7 @@ type RemoteFileParameters struct { IndexableFileContents string PreviewImage string PreviewImageReader io.Reader + PreviewImageName string // filename for PreviewImageReader (e.g., "preview.jpg") } // ListRemoteFilesParameters contains arguments for the ListRemoteFiles method. @@ -124,7 +129,11 @@ func (api *Client) AddRemoteFileContext(ctx context.Context, params RemoteFilePa if params.PreviewImage != "" { err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.add", params.PreviewImage, "preview_image", api.token, values, response, api) } else if params.PreviewImageReader != nil { - err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.add", "preview.png", "preview_image", api.token, values, params.PreviewImageReader, response, api) + name := params.PreviewImageName + if name == "" { + name = "preview.png" + } + err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.add", name, "preview_image", api.token, values, params.PreviewImageReader, response, api) } else { response, err = api.remoteFileRequest(ctx, "files.remote.add", values) } @@ -214,7 +223,7 @@ func (api *Client) ShareRemoteFile(channels []string, externalID, fileID string) // ShareRemoteFileContext shares a remote file to channels with a custom context. // Slack API docs: https://api.slack.com/methods/files.remote.share func (api *Client) ShareRemoteFileContext(ctx context.Context, channels []string, externalID, fileID string) (file *RemoteFile, err error) { - if channels == nil || len(channels) == 0 { + if len(channels) == 0 { return nil, ErrParametersMissing } if fileID == "" && externalID == "" { @@ -266,8 +275,14 @@ func (api *Client) UpdateRemoteFileContext(ctx context.Context, fileID string, p if params.IndexableFileContents != "" { values.Add("indexable_file_contents", params.IndexableFileContents) } - if params.PreviewImageReader != nil { - err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.update", "preview.png", "preview_image", api.token, values, params.PreviewImageReader, response, api) + if params.PreviewImage != "" { + err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.update", params.PreviewImage, "preview_image", api.token, values, response, api) + } else if params.PreviewImageReader != nil { + name := params.PreviewImageName + if name == "" { + name = "preview.png" + } + err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.update", name, "preview_image", api.token, values, params.PreviewImageReader, response, api) } else { values.Add("token", api.token) response, err = api.remoteFileRequest(ctx, "files.remote.update", values) diff --git a/vendor/github.com/slack-go/slack/retry.go b/vendor/github.com/slack-go/slack/retry.go new file mode 100644 index 000000000..d2f62117d --- /dev/null +++ b/vendor/github.com/slack-go/slack/retry.go @@ -0,0 +1,307 @@ +package slack + +// Optional HTTP retries improve reliability when Slack is busy or the network is flaky. +// Retries are off by default; use OptionRetry or OptionRetryConfig to turn them on. +// +// Retry behavior is driven by pluggable handlers (parity with the Python SDK: +// https://github.com/slackapi/python-slack-sdk). When Handlers is nil, only rate limit +// (429) is retried (NewRateLimitErrorRetryHandler). Use +// AllBuiltinRetryHandlers(cfg) for connection + 429; ConnectionOnlyRetryHandlers(cfg) for +// connection-only; add NewServerErrorRetryHandler(cfg) to also retry 5xx. +// +// File uploads and other requests that stream the body cannot be retried (the body is sent once). +// Regular API calls (form or JSON) are retried when a handler matches (429, 5xx, or connection error). + +import ( + "context" + "errors" + "fmt" + "io" + "math/rand/v2" + "net/http" + "strconv" + "strings" + "time" + + "github.com/slack-go/slack/internal/backoff" +) + +// minRetryAfter429 is the minimum wait before retrying after 429 when Retry-After is missing +// or zero, to avoid tight retry loops when using a partial RetryConfig. +const minRetryAfter429 = time.Second + +// RetryState holds the current attempt and max retries; passed to handlers. +// Backoff is set by retryClient for use by handlers that want exponential backoff (e.g. connection, server error). +// Handlers may call Backoff.Duration() when retrying; each call advances the backoff for the next retry. +type RetryState struct { + Attempt int // current attempt (0-based) + MaxRetries int + Backoff *backoff.Backoff // optional; used by connection/server handlers for exponential backoff +} + +// RetryHandler decides whether to retry a request and how long to wait. +// The first handler that returns (true, wait) wins. resp may be nil (connection failure); err may be nil (got response). +type RetryHandler interface { + ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (retry bool, wait time.Duration) +} + +// RetryConfig configures HTTP retry behavior. +// When MaxRetries is 0, retries are disabled. +// If Handlers is nil, only rate limit (429) is retried (see DefaultRetryHandlers). +type RetryConfig struct { + // MaxRetries is the maximum number of retry attempts (0 = no retries, 1 = one retry, etc.). + MaxRetries int + // Handlers is the list of handlers to consult; nil means 429 only (DefaultRetryHandlers). + Handlers []RetryHandler + // RetryAfterDuration is used for 429 when the Retry-After header is missing or invalid. + RetryAfterDuration time.Duration + // RetryAfterJitter adds random jitter [0, RetryAfterJitter] to 429 wait to avoid thundering herd (0 = no jitter). + RetryAfterJitter time.Duration + // BackoffInitial is the initial backoff for 5xx and connection errors. + BackoffInitial time.Duration + // BackoffMax caps the backoff duration. + BackoffMax time.Duration + // BackoffJitter adds random jitter [0, BackoffJitter] to backoff to avoid thundering herd (0 to disable). + BackoffJitter time.Duration +} + +// DefaultRetryConfig returns a retry config with sensible defaults. +func DefaultRetryConfig() RetryConfig { + return RetryConfig{ + MaxRetries: 3, + RetryAfterDuration: 60 * time.Second, + RetryAfterJitter: 1 * time.Second, + BackoffInitial: 100 * time.Millisecond, + BackoffMax: 30 * time.Second, + BackoffJitter: 50 * time.Millisecond, + } +} + +// connectionErrorRetryHandler retries on connection errors (e.g. connection reset). +type connectionErrorRetryHandler struct{} + +// NewConnectionErrorRetryHandler returns a handler that retries on connection errors. +func NewConnectionErrorRetryHandler() RetryHandler { + return &connectionErrorRetryHandler{} +} + +func (h *connectionErrorRetryHandler) ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (bool, time.Duration) { + if err == nil || resp != nil { + return false, 0 + } + if !isRetryableConnError(err) || !requestRetryable(req) { + return false, 0 + } + if state.Attempt >= state.MaxRetries { + return false, 0 + } + // Backoff is always set by retryClient.Do(). + wait := state.Backoff.Duration() + return true, wait +} + +// rateLimitErrorRetryHandler retries on 429 Too Many Requests using Retry-After or config. +type rateLimitErrorRetryHandler struct { + cfg RetryConfig +} + +// NewRateLimitErrorRetryHandler returns a handler that retries on 429. +func NewRateLimitErrorRetryHandler(cfg RetryConfig) RetryHandler { + return &rateLimitErrorRetryHandler{cfg: cfg} +} + +func (h *rateLimitErrorRetryHandler) ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (bool, time.Duration) { + if resp == nil || resp.StatusCode != http.StatusTooManyRequests || !requestRetryable(req) { + return false, 0 + } + if state.Attempt >= state.MaxRetries { + return false, 0 + } + dur := h.cfg.RetryAfterDuration + if s := resp.Header.Get("Retry-After"); s != "" { + // Parsing only integer seconds is appropriate for Slack (API sends seconds; RFC 7231 also allows HTTP-date). + if sec, parseErr := strconv.ParseInt(strings.TrimSpace(s), 10, 64); parseErr == nil { + if sec > 0 { + dur = time.Duration(sec) * time.Second + } else { + dur = minRetryAfter429 // Retry-After: 0 means use minimum delay + } + } + } + dur = max(dur, minRetryAfter429) + if h.cfg.RetryAfterJitter > 0 { + dur += time.Duration(rand.IntN(int(h.cfg.RetryAfterJitter))) + } + return true, dur +} + +// serverErrorRetryHandler retries on 5xx server errors (opt-in). +type serverErrorRetryHandler struct{} + +// NewServerErrorRetryHandler returns a handler that retries on 5xx. Opt-in; not in DefaultRetryHandlers, ConnectionOnlyRetryHandlers, or AllBuiltinRetryHandlers. +func NewServerErrorRetryHandler(cfg RetryConfig) RetryHandler { + return &serverErrorRetryHandler{} +} + +func (h *serverErrorRetryHandler) ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (bool, time.Duration) { + if resp == nil || resp.StatusCode < http.StatusInternalServerError || !requestRetryable(req) { + return false, 0 + } + if state.Attempt >= state.MaxRetries { + return false, 0 + } + // Backoff is always set by retryClient.Do(). + wait := state.Backoff.Duration() + return true, wait +} + +// DefaultRetryHandlers returns the default handler when retries are on: rate limit (429) only. +// Used when Handlers is nil. Use AllBuiltinRetryHandlers(cfg) for connection + 429. +func DefaultRetryHandlers(cfg RetryConfig) []RetryHandler { + return []RetryHandler{NewRateLimitErrorRetryHandler(cfg)} +} + +// ConnectionOnlyRetryHandlers returns connection-only handlers (no 429 retries). +func ConnectionOnlyRetryHandlers() []RetryHandler { + return []RetryHandler{NewConnectionErrorRetryHandler()} +} + +// AllBuiltinRetryHandlers returns connection + rate limit (429) handlers; no 5xx. +func AllBuiltinRetryHandlers(cfg RetryConfig) []RetryHandler { + return []RetryHandler{ + NewConnectionErrorRetryHandler(), + NewRateLimitErrorRetryHandler(cfg), + } +} + +// retryClient wraps an httpClient and retries according to config.Handlers. +type retryClient struct { + client httpClient + config RetryConfig + debug Debug // optional; when set and Debug() is true, retries are logged +} + +var _ httpClient = (*retryClient)(nil) + +// handlers returns the list of retry handlers. Empty Handlers slice is treated like nil (default 429 only). +func (c *retryClient) handlers() []RetryHandler { + if len(c.config.Handlers) > 0 { + return c.config.Handlers + } + return DefaultRetryHandlers(c.config) +} + +func (c *retryClient) logRetry(attempt int, reason string, detail any) { + if c.debug == nil || !c.debug.Debug() { + return + } + c.debug.Debugf("slack retry: %s (attempt %d/%d), detail: %v", reason, attempt+1, c.config.MaxRetries+1, detail) +} + +// requestRetryable reports whether the request can be safely retried: either it has no body +// (e.g. GET) or the body can be replayed via GetBody (e.g. POST with GetBody set). +// Requests with a non-nil body and nil GetBody (e.g. streaming uploads) must not be retried. +func requestRetryable(req *http.Request) bool { + return req.Body == nil || req.GetBody != nil +} + +// sleepWithContext sleeps for up to d, or until ctx is done. Returns true if the full duration +// elapsed, false if ctx was cancelled (caller should return ctx.Err()). +func sleepWithContext(ctx context.Context, d time.Duration) bool { + if d <= 0 { + return true + } + timer := time.NewTimer(d) + defer timer.Stop() + select { + case <-timer.C: + return true + case <-ctx.Done(): + return false + } +} + +func (c *retryClient) Do(req *http.Request) (*http.Response, error) { + handlers := c.handlers() + bo := &backoff.Backoff{ + Initial: c.config.BackoffInitial, + Max: c.config.BackoffMax, + Jitter: c.config.BackoffJitter, + } + var lastErr error + maxAttempts := c.config.MaxRetries + 1 + + for attempt := range maxAttempts { + state := &RetryState{Attempt: attempt, MaxRetries: c.config.MaxRetries, Backoff: bo} + + // Rewind body for retries (POST/PUT with GetBody). + if attempt > 0 && req.GetBody != nil { + newBody, err := req.GetBody() + if err != nil { + return nil, err + } + req.Body = newBody + } + + resp, err := c.client.Do(req) + if err != nil { + lastErr = err + } + + // Success: got response and status is not 429/5xx. + if err == nil && resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode < http.StatusInternalServerError { + return resp, nil + } + + // No retries left or request body cannot be replayed — return now. + if attempt >= c.config.MaxRetries || !requestRetryable(req) { + if err != nil { + return nil, err + } + return resp, nil + } + + // First handler that wants to retry wins. + var wait time.Duration + retry := false + for _, h := range handlers { + if r, w := h.ShouldRetry(state, req, resp, err); r { + retry, wait = true, w + break + } + } + if !retry { + if err != nil { + return nil, err + } + return resp, nil + } + + // Log, discard response body if we have one, sleep, then next attempt. + if err != nil { + c.logRetry(attempt, "connection error", err) + } else { + reason := fmt.Sprintf("%d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) + c.logRetry(attempt, reason, wait) + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + } + if !sleepWithContext(req.Context(), wait) { + return nil, req.Context().Err() + } + } + + return nil, lastErr +} + +func isRetryableConnError(err error) bool { + for ; err != nil; err = errors.Unwrap(err) { + s := err.Error() + if strings.Contains(s, "connection reset") || + strings.Contains(s, "connection refused") || + strings.Contains(s, "EOF") { + return true + } + } + return false +} diff --git a/vendor/github.com/slack-go/slack/rtm.go b/vendor/github.com/slack-go/slack/rtm.go index ef6ba3434..9b30eb315 100644 --- a/vendor/github.com/slack-go/slack/rtm.go +++ b/vendor/github.com/slack-go/slack/rtm.go @@ -25,6 +25,9 @@ const ( // StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info block. // // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. +// +// Deprecated: Use [ConnectRTM] instead. +// For more details, see: https://api.slack.com/changelog/2021-10-rtm-start-to-stop func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout) defer cancel() @@ -35,6 +38,9 @@ func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { // StartRTMContext calls the "rtm.start" endpoint and returns the provided URL and the full Info block with a custom context. // // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. +// +// Deprecated: Use [ConnectRTMContext] instead. +// For more details, see: https://api.slack.com/changelog/2021-10-rtm-start-to-stop func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) { response := &infoResponseFull{} err = api.postMethod(ctx, "rtm.start", url.Values{"token": {api.token}}, response) diff --git a/vendor/github.com/slack-go/slack/search.go b/vendor/github.com/slack-go/slack/search.go index d27497aae..9df433501 100644 --- a/vendor/github.com/slack-go/slack/search.go +++ b/vendor/github.com/slack-go/slack/search.go @@ -89,7 +89,7 @@ func NewSearchParameters() SearchParameters { } } -func (api *Client) _search(ctx context.Context, path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) { +func (api *Client) _search(ctx context.Context, path, query string, params SearchParameters) (response *searchResponseFull, error error) { values := url.Values{ "token": {api.token}, "query": {query}, @@ -103,7 +103,7 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc if params.SortDirection != DEFAULT_SEARCH_SORT_DIR { values.Add("sort_dir", params.SortDirection) } - if params.Highlight != DEFAULT_SEARCH_HIGHLIGHT { + if params.Highlight { values.Add("highlight", strconv.Itoa(1)) } if params.Count != DEFAULT_SEARCH_COUNT { @@ -128,7 +128,7 @@ func (api *Client) Search(query string, params SearchParameters) (*SearchMessage } func (api *Client) SearchContext(ctx context.Context, query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) { - response, err := api._search(ctx, "search.all", query, params, true, true) + response, err := api._search(ctx, "search.all", query, params) if err != nil { return nil, nil, err } @@ -140,7 +140,7 @@ func (api *Client) SearchFiles(query string, params SearchParameters) (*SearchFi } func (api *Client) SearchFilesContext(ctx context.Context, query string, params SearchParameters) (*SearchFiles, error) { - response, err := api._search(ctx, "search.files", query, params, true, false) + response, err := api._search(ctx, "search.files", query, params) if err != nil { return nil, err } @@ -152,7 +152,7 @@ func (api *Client) SearchMessages(query string, params SearchParameters) (*Searc } func (api *Client) SearchMessagesContext(ctx context.Context, query string, params SearchParameters) (*SearchMessages, error) { - response, err := api._search(ctx, "search.messages", query, params, false, true) + response, err := api._search(ctx, "search.messages", query, params) if err != nil { return nil, err } diff --git a/vendor/github.com/slack-go/slack/security.go b/vendor/github.com/slack-go/slack/security.go index 451035293..8124c2c09 100644 --- a/vendor/github.com/slack-go/slack/security.go +++ b/vendor/github.com/slack-go/slack/security.go @@ -30,6 +30,10 @@ func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifi bsignature []byte ) + if secret == "" { + return SecretsVerifier{}, ErrInvalidConfiguration + } + signature := header.Get(hSignature) stimestamp := header.Get(hTimestamp) @@ -42,7 +46,7 @@ func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifi } hash := hmac.New(sha256.New, []byte(secret)) - if _, err = hash.Write([]byte(fmt.Sprintf("v0:%s:", stimestamp))); err != nil { + if _, err = fmt.Fprintf(hash, "v0:%s:", stimestamp); err != nil { return SecretsVerifier{}, err } @@ -95,7 +99,7 @@ func (v SecretsVerifier) Ensure() error { if v.d != nil && v.d.Debug() { v.d.Debugln(fmt.Sprintf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed))) } - return fmt.Errorf("Computed unexpected signature of: %s", hex.EncodeToString(computed)) + return fmt.Errorf("computed unexpected signature of: %s", hex.EncodeToString(computed)) } func abs64(n int64) int64 { diff --git a/vendor/github.com/slack-go/slack/slack.go b/vendor/github.com/slack-go/slack/slack.go index 756106fe4..45b9cd3bc 100644 --- a/vendor/github.com/slack-go/slack/slack.go +++ b/vendor/github.com/slack-go/slack/slack.go @@ -12,6 +12,8 @@ import ( const ( // APIURL of the slack api. APIURL = "https://slack.com/api/" + // AuditAPIURL is the base URL for the Audit Logs API. + AuditAPIURL = "https://api.slack.com/" // WEBAPIURLFormat ... WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d" ) @@ -44,27 +46,32 @@ type AuthTestResponse struct { TeamID string `json:"team_id"` UserID string `json:"user_id"` // EnterpriseID is only returned when an enterprise id present - EnterpriseID string `json:"enterprise_id,omitempty"` - BotID string `json:"bot_id"` + EnterpriseID string `json:"enterprise_id,omitempty"` + BotID string `json:"bot_id"` + Header http.Header `json:"-"` } type authTestResponseFull struct { SlackResponse AuthTestResponse + responseHeaders } -// Client for the slack api. type ParamOption func(*url.Values) +// Client for the slack api. type Client struct { token string appLevelToken string configToken string configRefreshToken string endpoint string + auditEndpoint string debug bool log ilogger httpclient httpClient + onWarning func(path string, request any, w *Warning) + onResponseHeaders func(path string, headers http.Header) } // Option defines an option for a Client @@ -91,11 +98,44 @@ func OptionLog(l logger) func(*Client) { } } +// OptionOnWarning sets a callback invoked whenever an API response contains +// warnings. The callback receives the API method path (e.g. +// "conversations.join"), the request payload ([url.Values] for form-encoded +// requests or []byte for JSON requests), and a [Warning] with the warning +// codes and messages. +// +// Example: +// +// api := slack.New("YOUR_TOKEN", +// slack.OptionOnWarning(func(path string, request any, w *slack.Warning) { +// log.Printf("slack warnings for %s: codes=%v warnings=%v", path, w.Codes, w.Warnings) +// }), +// ) +func OptionOnWarning(fn func(path string, request any, w *Warning)) func(*Client) { + return func(c *Client) { + c.onWarning = fn + } +} + +// OptionOnResponseHeaders sets a callback invoked after every API request +// with the API method path and the HTTP response headers. This allows +// accessing headers like X-OAuth-Scopes and X-Ratelimit-* for any request. +func OptionOnResponseHeaders(fn func(path string, headers http.Header)) func(*Client) { + return func(c *Client) { + c.onResponseHeaders = fn + } +} + // OptionAPIURL set the url for the client. only useful for testing. func OptionAPIURL(u string) func(*Client) { return func(c *Client) { c.endpoint = u } } +// OptionAuditAPIURL set the url for the Audit Logs API. only useful for testing. +func OptionAuditAPIURL(u string) func(*Client) { + return func(c *Client) { c.auditEndpoint = u } +} + // OptionAppLevelToken sets an app-level token for the client. func OptionAppLevelToken(token string) func(*Client) { return func(c *Client) { c.appLevelToken = token } @@ -111,13 +151,47 @@ func OptionConfigRefreshToken(token string) func(*Client) { return func(c *Client) { c.configRefreshToken = token } } +// OptionRetry enables HTTP retries for rate limit (429) only; 5xx and connection errors are not retried. +// Uses DefaultRetryHandlers. Use OptionRetryConfig with AllBuiltinRetryHandlers for connection + 429. +// If maxRetries is zero or negative, the client is not wrapped (no retries). +// When using a custom HTTP client, pass OptionRetry after OptionHTTPClient so the retry wrapper is applied to it. +func OptionRetry(maxRetries int) func(*Client) { + return func(c *Client) { + if maxRetries <= 0 { + return + } + cfg := DefaultRetryConfig() + cfg.MaxRetries = maxRetries + cfg.Handlers = DefaultRetryHandlers(cfg) + c.httpclient = &retryClient{client: c.httpclient, config: cfg, debug: c} + } +} + +// OptionRetryConfig enables HTTP retries with a custom config. +// If config.MaxRetries is 0, the client is not wrapped (no retries). +// If config.Handlers is nil, DefaultRetryHandlers(cfg) is used (429 only). +// When using a custom HTTP client, pass OptionRetryConfig after OptionHTTPClient so the retry wrapper is applied to it. +func OptionRetryConfig(config RetryConfig) func(*Client) { + return func(c *Client) { + if config.MaxRetries <= 0 { + return + } + cfg := config + if cfg.Handlers == nil { + cfg.Handlers = DefaultRetryHandlers(cfg) + } + c.httpclient = &retryClient{client: c.httpclient, config: cfg, debug: c} + } +} + // New builds a slack client from the provided token and options. func New(token string, options ...Option) *Client { s := &Client{ - token: token, - endpoint: APIURL, - httpclient: &http.Client{}, - log: log.New(os.Stderr, "slack-go/slack", log.LstdFlags|log.Lshortfile), + token: token, + endpoint: APIURL, + auditEndpoint: AuditAPIURL, + httpclient: &http.Client{}, + log: log.New(os.Stderr, "slack-go/slack", log.LstdFlags|log.Lshortfile), } for _, opt := range options { @@ -141,18 +215,19 @@ func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestRespo return nil, err } + responseFull.AuthTestResponse.Header = responseFull.responseHeaders.header return &responseFull.AuthTestResponse, responseFull.Err() } // Debugf print a formatted debug line. -func (api *Client) Debugf(format string, v ...interface{}) { +func (api *Client) Debugf(format string, v ...any) { if api.debug { api.log.Output(2, fmt.Sprintf(format, v...)) } } // Debugln print a debug line. -func (api *Client) Debugln(v ...interface{}) { +func (api *Client) Debugln(v ...any) { if api.debug { api.log.Output(2, fmt.Sprintln(v...)) } @@ -164,11 +239,42 @@ func (api *Client) Debug() bool { } // post to a slack web method. -func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf interface{}) error { - return postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api) +func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf any) error { + headers, err := postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api) + api.checkWarnings(intf, path, values) + api.fireResponseHeaders(path, headers) + return err } // get a slack web method. -func (api *Client) getMethod(ctx context.Context, path string, token string, values url.Values, intf interface{}) error { - return getResource(ctx, api.httpclient, api.endpoint+path, token, values, intf, api) +func (api *Client) getMethod(ctx context.Context, path string, token string, values url.Values, intf any) error { + headers, err := getResource(ctx, api.httpclient, api.endpoint+path, token, values, intf, api) + api.checkWarnings(intf, path, values) + api.fireResponseHeaders(path, headers) + return err +} + +// postJSONMethod posts JSON to a slack web method. +func (api *Client) postJSONMethod(ctx context.Context, path string, token string, jsonBody []byte, intf any) error { + headers, err := postJSON(ctx, api.httpclient, api.endpoint+path, token, jsonBody, intf, api) + api.checkWarnings(intf, path, jsonBody) + api.fireResponseHeaders(path, headers) + return err +} + +func (api *Client) checkWarnings(intf any, path string, request any) { + if api.onWarning == nil { + return + } + if w, ok := intf.(warner); ok { + if warning := w.Warn(); warning != nil { + api.onWarning(path, request, warning) + } + } +} + +func (api *Client) fireResponseHeaders(path string, headers http.Header) { + if api.onResponseHeaders != nil && headers != nil { + api.onResponseHeaders(path, headers) + } } diff --git a/vendor/github.com/slack-go/slack/slackevents/action_events.go b/vendor/github.com/slack-go/slack/slackevents/action_events.go index c6016f107..e7e4ed105 100644 --- a/vendor/github.com/slack-go/slack/slackevents/action_events.go +++ b/vendor/github.com/slack-go/slack/slackevents/action_events.go @@ -6,18 +6,25 @@ import ( "github.com/slack-go/slack" ) +// Deprecated: MessageActionResponse is associated with [MessageAction] which cannot +// handle block_actions. Use [slack.InteractionCallback] instead. type MessageActionResponse struct { ResponseType string `json:"response_type"` ReplaceOriginal bool `json:"replace_original"` Text string `json:"text"` } +// Deprecated: MessageActionEntity is associated with [MessageAction] which cannot +// handle block_actions. Use [slack.InteractionCallback] instead. type MessageActionEntity struct { ID string `json:"id"` Domain string `json:"domain"` Name string `json:"name"` } +// Deprecated: MessageAction cannot represent block_actions payloads. Use +// [slack.InteractionCallback] instead, which handles all interaction types. +// See [slack.InteractionCallbackParse] for parsing from an HTTP request. type MessageAction struct { Type string `json:"type"` Actions []slack.AttachmentAction `json:"actions"` diff --git a/vendor/github.com/slack-go/slack/slackevents/inner_events.go b/vendor/github.com/slack-go/slack/slackevents/inner_events.go index 8f8effaae..676a005e1 100644 --- a/vendor/github.com/slack-go/slack/slackevents/inner_events.go +++ b/vendor/github.com/slack-go/slack/slackevents/inner_events.go @@ -3,6 +3,8 @@ package slackevents import ( + "encoding/json" + "github.com/slack-go/slack" ) @@ -12,6 +14,40 @@ type EventsAPIInnerEvent struct { Data interface{} } +// AssistantThreadMessageEvent is an (inner) EventsAPI subscribable event. +type AssistantThreadStartedEvent struct { + Type string `json:"type"` + AssistantThread AssistantThread `json:"assistant_thread"` + EventTimestamp string `json:"event_ts"` +} + +// AssistantThreadChangedEvent is an (inner) EventsAPI subscribable event. +type AssistantThreadContextChangedEvent struct { + Type string `json:"type"` + AssistantThread AssistantThread `json:"assistant_thread"` + EventTimestamp string `json:"event_ts"` +} + +// AssistantThread is an object that represents a thread of messages between a user and an assistant. +type AssistantThread struct { + UserID string `json:"user_id"` + Context AssistantThreadContext `json:"context"` + ChannelID string `json:"channel_id"` + ThreadTimeStamp string `json:"thread_ts"` +} + +// AssistantThreadActionToken contains the action token for Data Access API queries +type AssistantThreadActionToken struct { + ActionToken string `json:"action_token"` +} + +// AssistantThreadContext is an object that represents the context of an assistant thread. +type AssistantThreadContext struct { + ChannelID string `json:"channel_id"` + TeamID string `json:"team_id"` + EnterpriseID string `json:"enterprise_id"` +} + // AppMentionEvent is an (inner) EventsAPI subscribable event. type AppMentionEvent struct { Type string `json:"type"` @@ -29,18 +65,27 @@ type AppMentionEvent struct { // BotID is filled out when a bot triggers the app_mention event BotID string `json:"bot_id,omitempty"` + // Fields shared with message events + Blocks slack.Blocks `json:"blocks,omitempty"` + Attachments []slack.Attachment `json:"attachments,omitempty"` + Files []slack.File `json:"files,omitempty"` + Upload bool `json:"upload,omitempty"` + // When the app is mentioned in the edited message Edited *Edited `json:"edited,omitempty"` + + // AssistantThread contains action token for Data Access API queries when app is mentioned + AssistantThread *AssistantThreadActionToken `json:"assistant_thread,omitempty"` } // AppHomeOpenedEvent Your Slack app home was opened. type AppHomeOpenedEvent struct { - Type string `json:"type"` - User string `json:"user"` - Channel string `json:"channel"` - EventTimeStamp string `json:"event_ts"` - Tab string `json:"tab"` - View slack.View `json:"view"` + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` + EventTimeStamp string `json:"event_ts"` + Tab string `json:"tab"` + View *slack.View `json:"view,omitempty"` } // AppUninstalledEvent Your Slack app was uninstalled. @@ -116,6 +161,15 @@ type ChannelRenameInfo struct { Created int `json:"created"` } +// ChannelUnsharedEvent represents a channel has been unshared with an external workspace event +type ChannelUnsharedEvent struct { + Type string `json:"type"` + PreviouslyConnectedTeamID string `json:"previously_connected_team_id"` + Channel string `json:"channel"` + IsExtShared bool `json:"is_ext_shared"` + EventTimestamp string `json:"event_ts"` +} + // GroupDeletedEvent represents the Group deleted event type GroupDeletedEvent struct { Type string `json:"type"` @@ -231,33 +285,46 @@ type SharedLinks struct { URL string `json:"url"` } +const ( + ChannelTypeChannel = "channel" // Public channel message + ChannelTypeGroup = "group" // Private channel message + ChannelTypeIM = "im" // Direct message + ChannelTypeMPIM = "mpim" // Multiparty direct message +) + // MessageEvent occurs when a variety of types of messages has been posted. // Parse ChannelType to see which // if ChannelType = "group", this is a private channel message // if ChannelType = "channel", this message was sent to a channel // if ChannelType = "im", this is a private message -// if ChannelType = "mim", A message was posted in a multiparty direct message channel -// TODO: Improve this so that it is not required to manually parse ChannelType +// if ChannelType = "mpim", A message was posted in a multiparty direct message channel type MessageEvent struct { // Basic Message Event - https://api.slack.com/events/message - ClientMsgID string `json:"client_msg_id"` - Type string `json:"type"` - User string `json:"user"` - Text string `json:"text"` - ThreadTimeStamp string `json:"thread_ts"` - TimeStamp string `json:"ts"` - Channel string `json:"channel"` - ChannelType string `json:"channel_type"` - EventTimeStamp string `json:"event_ts"` + ClientMsgID string `json:"client_msg_id"` + Type string `json:"type"` + User string `json:"user"` + Text string `json:"text"` + Blocks slack.Blocks `json:"blocks,omitempty"` + ThreadTimeStamp string `json:"thread_ts"` + TimeStamp string `json:"ts"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + EventTimeStamp string `json:"event_ts"` // When Message comes from a channel that is shared between workspaces UserTeam string `json:"user_team,omitempty"` SourceTeam string `json:"source_team,omitempty"` + // When we get a 'message' event with no subtype, i.e. telling us about a new + // message, the message information is stored at the top level. But when we get + // a 'message_changed' event, the message information is stored in + // the Message property. This is really hard to represent nicely in Go, so we use + // a custom JSON unmarshaller to populate the Message field in both cases. + Message *slack.Msg `json:"message,omitempty"` + // Root is set if the SubType is `thread_broadcast`. + Root *slack.Msg `json:"root,omitempty"` // Edited Message - Message *MessageEvent `json:"message,omitempty"` - PreviousMessage *MessageEvent `json:"previous_message,omitempty"` - Edited *Edited `json:"edited,omitempty"` + PreviousMessage *slack.Msg `json:"previous_message,omitempty"` // Deleted Message DeletedTimeStamp string `json:"deleted_ts,omitempty"` @@ -266,22 +333,57 @@ type MessageEvent struct { SubType string `json:"subtype,omitempty"` // bot_message (https://api.slack.com/events/message/bot_message) - BotID string `json:"bot_id,omitempty"` - Username string `json:"username,omitempty"` - Icons *Icon `json:"icons,omitempty"` - - Upload bool `json:"upload"` - Files []File `json:"files"` - - Blocks slack.Blocks `json:"blocks,omitempty"` - Attachments []slack.Attachment `json:"attachments,omitempty"` - - Metadata slack.SlackMetadata `json:"metadata,omitempty"` - - // Root is the message that was broadcast to the channel when the SubType is - // thread_broadcast. If this is not a thread_broadcast message event, this - // value is nil. - Root *MessageEvent `json:"root"` + BotID string `json:"bot_id,omitempty"` + Username string `json:"username,omitempty"` + Icons *Icon `json:"icons,omitempty"` + WorkflowID string `json:"workflow_id,omitempty"` + + // AssistantThread contains action token for Data Access API queries in message events + AssistantThread *AssistantThreadActionToken `json:"assistant_thread,omitempty"` + + // Huddle-related fields (subtype "huddle_thread") + Room *slack.HuddleRoom `json:"room,omitempty"` + NoNotifications bool `json:"no_notifications,omitempty"` + Permalink string `json:"permalink,omitempty"` +} + +func (e *MessageEvent) IsIM() bool { return e.ChannelType == ChannelTypeIM } +func (e *MessageEvent) IsChannel() bool { return e.ChannelType == ChannelTypeChannel } +func (e *MessageEvent) IsGroup() bool { return e.ChannelType == ChannelTypeGroup } +func (e *MessageEvent) IsMpIM() bool { return e.ChannelType == ChannelTypeMPIM } + +// UnmarshalJSON implements the json.Unmarshaler interface for MessageEvent. +// This custom unmarshaler handles both regular messages and message_changed events +// by normalizing the message data into the Message field. +func (e *MessageEvent) UnmarshalJSON(data []byte) error { + // First, unmarshal into an anonymous struct to avoid infinite recursion + // when calling json.Unmarshal on the MessageEvent type itself + type MessageEventAlias MessageEvent + alias := struct { + MessageEventAlias + }{} + + if err := json.Unmarshal(data, &alias.MessageEventAlias); err != nil { + return err + } + + // Copy all fields from alias to the original struct + *e = MessageEvent(alias.MessageEventAlias) + + // Now check if there's no Message field (which would happen for regular messages) + if e.Message == nil { + // For regular messages, the message content is at the top level, + // so we need to unmarshal the data again into a slack.Msg + msg := &slack.Msg{} + if err := json.Unmarshal(data, msg); err != nil { + return err + } + + // Set the Message field to the unmarshaled msg + e.Message = msg + } + + return nil } // MemberJoinedChannelEvent A member joined a public or private channel @@ -374,14 +476,6 @@ type EmojiChangedEvent struct { Value string `json:"value,omitempty"` } -// WorkflowStepExecuteEvent is fired, if a workflow step of your app is invoked -type WorkflowStepExecuteEvent struct { - Type string `json:"type"` - CallbackID string `json:"callback_id"` - WorkflowStep EventWorkflowStep `json:"workflow_step"` - EventTimestamp string `json:"event_ts"` -} - // MessageMetadataPostedEvent is sent, if a message with metadata is posted type MessageMetadataPostedEvent struct { Type string `json:"type"` @@ -423,15 +517,6 @@ type MessageMetadataDeletedEvent struct { DeletedTimestamp string `json:"deleted_ts"` } -type EventWorkflowStep struct { - WorkflowStepExecuteID string `json:"workflow_step_execute_id"` - WorkflowID string `json:"workflow_id"` - WorkflowInstanceID string `json:"workflow_instance_id"` - StepID string `json:"step_id"` - Inputs *slack.WorkflowStepInputs `json:"inputs,omitempty"` - Outputs *[]slack.WorkflowStepOutput `json:"outputs,omitempty"` -} - // JSONTime exists so that we can have a String method converting the date type JSONTime int64 @@ -773,9 +858,9 @@ type SubteamMembersChangedEvent struct { DatePreviousUpdate int `json:"date_previous_update"` DateUpdate int64 `json:"date_update"` AddedUsers []string `json:"added_users"` - AddedUsersCount string `json:"added_users_count"` + AddedUsersCount int `json:"added_users_count"` RemovedUsers []string `json:"removed_users"` - RemovedUsersCount string `json:"removed_users_count"` + RemovedUsersCount int `json:"removed_users_count"` } type SubteamSelfAddedEvent struct { @@ -957,11 +1042,11 @@ type FunctionExecutedEvent struct { DateUpdated int64 `json:"date_updated"` DateDeleted int64 `json:"date_deleted"` } `json:"function"` - Inputs map[string]string `json:"inputs"` - FunctionExecutionID string `json:"function_execution_id"` - WorkflowExecutionID string `json:"workflow_execution_id"` - EventTs string `json:"event_ts"` - BotAccessToken string `json:"bot_access_token"` + Inputs map[string]interface{} `json:"inputs"` + FunctionExecutionID string `json:"function_execution_id"` + WorkflowExecutionID string `json:"workflow_execution_id"` + EventTs string `json:"event_ts"` + BotAccessToken string `json:"bot_access_token"` } type InviteRequestedEvent struct { @@ -1064,6 +1149,36 @@ type UserStatusChangedEvent struct { EventTS string `json:"event_ts"` } +// EntityDetailsRequestedEvent is sent when entity details are requested +// This event is fired when a user clicks on a Work Object link and Slack requests +// details about the entity from your app. +type EntityDetailsRequestedEvent struct { + Type string `json:"type"` + User string `json:"user"` + ExternalRef EntityDetailsRequestedExternalRef `json:"external_ref"` + EntityURL string `json:"entity_url"` + Link EntityDetailsRequestedLink `json:"link"` + AppUnfurlURL string `json:"app_unfurl_url"` + EventTS string `json:"event_ts"` + TriggerID string `json:"trigger_id"` + UserLocale string `json:"user_locale"` + Channel string `json:"channel,omitempty"` + MessageTs string `json:"message_ts,omitempty"` + ThreadTs string `json:"thread_ts,omitempty"` +} + +// EntityDetailsRequestedExternalRef represents the external reference in entity_details_requested event +type EntityDetailsRequestedExternalRef struct { + ID string `json:"id"` + Type string `json:"type,omitempty"` +} + +// EntityDetailsRequestedLink represents the link information in entity_details_requested event +type EntityDetailsRequestedLink struct { + URL string `json:"url"` + Domain string `json:"domain"` +} + type Actor struct { ID string `json:"id"` Name string `json:"name"` @@ -1111,56 +1226,114 @@ type SharedChannelInviteRequestedEvent struct { type EventsAPIType string const ( - // AppMention is an Events API subscribable event - AppMention = EventsAPIType("app_mention") + // AppDeleted is an event when an app is deleted from a workspace + AppDeleted = EventsAPIType("app_deleted") // AppHomeOpened Your Slack app home was opened AppHomeOpened = EventsAPIType("app_home_opened") + // AppInstalled is an event when an app is installed to a workspace + AppInstalled = EventsAPIType("app_installed") + // AppMention is an Events API subscribable event + AppMention = EventsAPIType("app_mention") + // AppRequested is an event when a user requests to install an app to a workspace + AppRequested = EventsAPIType("app_requested") // AppUninstalled Your Slack app was uninstalled. AppUninstalled = EventsAPIType("app_uninstalled") + // AppUninstalledTeam is an event when an app is uninstalled from a team + AppUninstalledTeam = EventsAPIType("app_uninstalled_team") + // AssistantThreadContextChanged Your Slack AI Assistant has changed the context of a thread + AssistantThreadContextChanged = EventsAPIType("assistant_thread_context_changed") + // AssistantThreadStarted Your Slack AI Assistant has started a new thread + AssistantThreadStarted = EventsAPIType("assistant_thread_started") + // CallRejected is an event when a Slack call is rejected + CallRejected = EventsAPIType("call_rejected") + // ChannelArchive is sent when a channel is archived. + ChannelArchive = EventsAPIType("channel_archive") // ChannelCreated is sent when a new channel is created. ChannelCreated = EventsAPIType("channel_created") // ChannelDeleted is sent when a channel is deleted. ChannelDeleted = EventsAPIType("channel_deleted") - // ChannelArchive is sent when a channel is archived. - ChannelArchive = EventsAPIType("channel_archive") - // ChannelUnarchive is sent when a channel is unarchived. - ChannelUnarchive = EventsAPIType("channel_unarchive") + // ChannelHistoryChanged The history of a channel changed + ChannelHistoryChanged = EventsAPIType("channel_history_changed") + // ChannelIDChanged is sent when a channel identifier is changed. + ChannelIDChanged = EventsAPIType("channel_id_changed") // ChannelLeft is sent when a channel is left. ChannelLeft = EventsAPIType("channel_left") // ChannelRename is sent when a channel is rename. ChannelRename = EventsAPIType("channel_rename") - // ChannelIDChanged is sent when a channel identifier is changed. - ChannelIDChanged = EventsAPIType("channel_id_changed") - // GroupDeleted is sent when a group is deleted. - GroupDeleted = EventsAPIType("group_deleted") - // GroupArchive is sent when a group is archived. - GroupArchive = EventsAPIType("group_archive") - // GroupUnarchive is sent when a group is unarchived. - GroupUnarchive = EventsAPIType("group_unarchive") - // GroupLeft is sent when a group is left. - GroupLeft = EventsAPIType("group_left") - // GroupRename is sent when a group is renamed. - GroupRename = EventsAPIType("group_rename") + // ChannelShared is an event when a channel is shared with another workspace + ChannelShared = EventsAPIType("channel_shared") + // ChannelUnarchive is sent when a channel is unarchived. + ChannelUnarchive = EventsAPIType("channel_unarchive") + // ChannelUnshared is sent when a channel is unshared. + ChannelUnshared = EventsAPIType("channel_unshared") + // CommandsChanged A command was changed + CommandsChanged = EventsAPIType("commands_changed") + // DndUpdated Do Not Disturb settings were updated + DndUpdated = EventsAPIType("dnd_updated") + // DndUpdatedUser Do Not Disturb settings for a user were updated + DndUpdatedUser = EventsAPIType("dnd_updated_user") + // EmailDomainChanged The email domain changed + EmailDomainChanged = EventsAPIType("email_domain_changed") + // EmojiChanged A custom emoji has been added or changed + EmojiChanged = EventsAPIType("emoji_changed") // FileChange is sent when a file is changed. FileChange = EventsAPIType("file_change") + // FileCreated is an event when a file is created in a workspace + FileCreated = EventsAPIType("file_created") // FileDeleted is sent when a file is deleted. FileDeleted = EventsAPIType("file_deleted") + // FilePublic is an event when a file is made public in a workspace + FilePublic = EventsAPIType("file_public") // FileShared is sent when a file is shared. FileShared = EventsAPIType("file_shared") // FileUnshared is sent when a file is unshared. FileUnshared = EventsAPIType("file_unshared") + // FunctionExecuted is an event when a Slack function is executed + FunctionExecuted = EventsAPIType("function_executed") // GridMigrationFinished An enterprise grid migration has finished on this workspace. GridMigrationFinished = EventsAPIType("grid_migration_finished") // GridMigrationStarted An enterprise grid migration has started on this workspace. GridMigrationStarted = EventsAPIType("grid_migration_started") + // GroupArchive is sent when a group is archived. + GroupArchive = EventsAPIType("group_archive") + // GroupClose A group was closed + GroupClose = EventsAPIType("group_close") + // GroupDeleted is sent when a group is deleted. + GroupDeleted = EventsAPIType("group_deleted") + // GroupHistoryChanged The history of a group changed + GroupHistoryChanged = EventsAPIType("group_history_changed") + // GroupLeft is sent when a group is left. + GroupLeft = EventsAPIType("group_left") + // GroupOpen A group was opened + GroupOpen = EventsAPIType("group_open") + // GroupRename is sent when a group is renamed. + GroupRename = EventsAPIType("group_rename") + // GroupUnarchive is sent when a group is unarchived. + GroupUnarchive = EventsAPIType("group_unarchive") + // ImClose An instant message channel was closed + ImClose = EventsAPIType("im_close") + // ImCreated An instant message channel was created + ImCreated = EventsAPIType("im_created") + // ImHistoryChanged The history of an instant message channel changed + ImHistoryChanged = EventsAPIType("im_history_changed") + // ImOpen An instant message channel was opened + ImOpen = EventsAPIType("im_open") + // InviteRequested is an event when a user requests an invite to a workspace + InviteRequested = EventsAPIType("invite_requested") // LinkShared A message was posted containing one or more links relevant to your application LinkShared = EventsAPIType("link_shared") - // Message A message was posted to a channel, private channel (group), im, or mim - Message = EventsAPIType("message") // MemberJoinedChannel is sent if a member joined a channel. MemberJoinedChannel = EventsAPIType("member_joined_channel") // MemberLeftChannel is sent if a member left a channel. MemberLeftChannel = EventsAPIType("member_left_channel") + // Message A message was posted to a channel, private channel (group), im, or mim + Message = EventsAPIType("message") + // MessageMetadataDeleted A message with metadata was deleted + MessageMetadataDeleted = EventsAPIType("message_metadata_deleted") + // MessageMetadataPosted A message with metadata was posted + MessageMetadataPosted = EventsAPIType("message_metadata_posted") + // MessageMetadataUpdated A message with metadata was updated + MessageMetadataUpdated = EventsAPIType("message_metadata_updated") // PinAdded An item was pinned to a channel PinAdded = EventsAPIType("pin_added") // PinRemoved An item was unpinned from a channel @@ -1169,58 +1342,20 @@ const ( ReactionAdded = EventsAPIType("reaction_added") // ReactionRemoved An reaction was removed from a message ReactionRemoved = EventsAPIType("reaction_removed") - // TeamJoin A new user joined the workspace - TeamJoin = EventsAPIType("team_join") - // Slack connect app or bot invite received - SharedChannelInviteReceived = EventsAPIType("shared_channel_invite_received") - // Slack connect channel invite approved + // SharedChannelInviteAccepted Slack connect channel invite accepted by an end user + SharedChannelInviteAccepted = EventsAPIType("shared_channel_invite_accepted") + // SharedChannelInviteApproved Slack connect channel invite approved SharedChannelInviteApproved = EventsAPIType("shared_channel_invite_approved") - // Slack connect channel invite declined + // SharedChannelInviteDeclined Slack connect channel invite declined SharedChannelInviteDeclined = EventsAPIType("shared_channel_invite_declined") - // Slack connect channel invite accepted by an end user - SharedChannelInviteAccepted = EventsAPIType("shared_channel_invite_accepted") - // TokensRevoked APP's API tokes are revoked - TokensRevoked = EventsAPIType("tokens_revoked") - // EmojiChanged A custom emoji has been added or changed - EmojiChanged = EventsAPIType("emoji_changed") - // WorkflowStepExecute Happens, if a workflow step of your app is invoked - WorkflowStepExecute = EventsAPIType("workflow_step_execute") - // MessageMetadataPosted A message with metadata was posted - MessageMetadataPosted = EventsAPIType("message_metadata_posted") - // MessageMetadataUpdated A message with metadata was updated - MessageMetadataUpdated = EventsAPIType("message_metadata_updated") - // MessageMetadataDeleted A message with metadata was deleted - MessageMetadataDeleted = EventsAPIType("message_metadata_deleted") - // TeamAccessGranted is sent if access to teams was granted for your org-wide app. - TeamAccessGranted = EventsAPIType("team_access_granted") - // TeamAccessRevoked is sent if access to teams was revoked for your org-wide app. - TeamAccessRevoked = EventsAPIType("team_access_revoked") - // UserProfileChanged is sent if a user's profile information has changed. - UserProfileChanged = EventsAPIType("user_profile_changed") - // ChannelHistoryChanged The history of a channel changed - ChannelHistoryChanged = EventsAPIType("channel_history_changed") - // CommandsChanged A command was changed - CommandsChanged = EventsAPIType("commands_changed") - // DndUpdated Do Not Disturb settings were updated - DndUpdated = EventsAPIType("dnd_updated") - // DndUpdatedUser Do Not Disturb settings for a user were updated - DndUpdatedUser = EventsAPIType("dnd_updated_user") - // EmailDomainChanged The email domain changed - EmailDomainChanged = EventsAPIType("email_domain_changed") - // GroupClose A group was closed - GroupClose = EventsAPIType("group_close") - // GroupHistoryChanged The history of a group changed - GroupHistoryChanged = EventsAPIType("group_history_changed") - // GroupOpen A group was opened - GroupOpen = EventsAPIType("group_open") - // ImClose An instant message channel was closed - ImClose = EventsAPIType("im_close") - // ImCreated An instant message channel was created - ImCreated = EventsAPIType("im_created") - // ImHistoryChanged The history of an instant message channel changed - ImHistoryChanged = EventsAPIType("im_history_changed") - // ImOpen An instant message channel was opened - ImOpen = EventsAPIType("im_open") + // SharedChannelInviteReceived Slack connect app or bot invite received + SharedChannelInviteReceived = EventsAPIType("shared_channel_invite_received") + // SharedChannelInviteRequested is an event when an invitation to share a channel is requested + SharedChannelInviteRequested = EventsAPIType("shared_channel_invite_requested") + // StarAdded is an event when a star is added to a message or file + StarAdded = EventsAPIType("star_added") + // StarRemoved is an event when a star is removed from a message or file + StarRemoved = EventsAPIType("star_removed") // SubteamCreated A subteam was created SubteamCreated = EventsAPIType("subteam_created") // SubteamMembersChanged The members of a subteam changed @@ -1231,123 +1366,115 @@ const ( SubteamSelfRemoved = EventsAPIType("subteam_self_removed") // SubteamUpdated A subteam was updated SubteamUpdated = EventsAPIType("subteam_updated") + // TeamAccessGranted is sent if access to teams was granted for your org-wide app. + TeamAccessGranted = EventsAPIType("team_access_granted") + // TeamAccessRevoked is sent if access to teams was revoked for your org-wide app. + TeamAccessRevoked = EventsAPIType("team_access_revoked") // TeamDomainChange The team's domain changed TeamDomainChange = EventsAPIType("team_domain_change") + // TeamJoin A new user joined the workspace + TeamJoin = EventsAPIType("team_join") // TeamRename The team was renamed TeamRename = EventsAPIType("team_rename") + // TokensRevoked APP's API tokes are revoked + TokensRevoked = EventsAPIType("tokens_revoked") // UserChange A user object has changed UserChange = EventsAPIType("user_change") - // AppDeleted is an event when an app is deleted from a workspace - AppDeleted = EventsAPIType("app_deleted") - // AppInstalled is an event when an app is installed to a workspace - AppInstalled = EventsAPIType("app_installed") - // AppRequested is an event when a user requests to install an app to a workspace - AppRequested = EventsAPIType("app_requested") - // AppUninstalledTeam is an event when an app is uninstalled from a team - AppUninstalledTeam = EventsAPIType("app_uninstalled_team") - // CallRejected is an event when a Slack call is rejected - CallRejected = EventsAPIType("call_rejected") - // ChannelShared is an event when a channel is shared with another workspace - ChannelShared = EventsAPIType("channel_shared") - // FileCreated is an event when a file is created in a workspace - FileCreated = EventsAPIType("file_created") - // FilePublic is an event when a file is made public in a workspace - FilePublic = EventsAPIType("file_public") - // FunctionExecuted is an event when a Slack function is executed - FunctionExecuted = EventsAPIType("function_executed") - // InviteRequested is an event when a user requests an invite to a workspace - InviteRequested = EventsAPIType("invite_requested") - // SharedChannelInviteRequested is an event when an invitation to share a channel is requested - SharedChannelInviteRequested = EventsAPIType("shared_channel_invite_requested") - // StarAdded is an event when a star is added to a message or file - StarAdded = EventsAPIType("star_added") - // StarRemoved is an event when a star is removed from a message or file - StarRemoved = EventsAPIType("star_removed") // UserHuddleChanged is an event when a user's huddle status changes UserHuddleChanged = EventsAPIType("user_huddle_changed") + // UserProfileChanged is sent if a user's profile information has changed. + UserProfileChanged = EventsAPIType("user_profile_changed") // UserStatusChanged is an event when a user's status changes UserStatusChanged = EventsAPIType("user_status_changed") + // WorkflowStepExecute Happens, if a workflow step of your app is invoked + WorkflowStepExecute = EventsAPIType("workflow_step_execute") + // EntityDetailsRequested is sent when entity details are requested + EntityDetailsRequested = EventsAPIType("entity_details_requested") ) // EventsAPIInnerEventMapping maps INNER Event API events to their corresponding struct // implementations. The structs should be instances of the unmarshalling // target for the matching event type. var EventsAPIInnerEventMapping = map[EventsAPIType]interface{}{ - AppMention: AppMentionEvent{}, - AppHomeOpened: AppHomeOpenedEvent{}, - AppUninstalled: AppUninstalledEvent{}, - ChannelCreated: ChannelCreatedEvent{}, - ChannelDeleted: ChannelDeletedEvent{}, - ChannelArchive: ChannelArchiveEvent{}, - ChannelUnarchive: ChannelUnarchiveEvent{}, - ChannelLeft: ChannelLeftEvent{}, - ChannelRename: ChannelRenameEvent{}, - ChannelIDChanged: ChannelIDChangedEvent{}, - FileChange: FileChangeEvent{}, - FileDeleted: FileDeletedEvent{}, - FileShared: FileSharedEvent{}, - FileUnshared: FileUnsharedEvent{}, - GroupDeleted: GroupDeletedEvent{}, - GroupArchive: GroupArchiveEvent{}, - GroupUnarchive: GroupUnarchiveEvent{}, - GroupLeft: GroupLeftEvent{}, - GroupRename: GroupRenameEvent{}, - GridMigrationFinished: GridMigrationFinishedEvent{}, - GridMigrationStarted: GridMigrationStartedEvent{}, - LinkShared: LinkSharedEvent{}, - Message: MessageEvent{}, - MemberJoinedChannel: MemberJoinedChannelEvent{}, - MemberLeftChannel: MemberLeftChannelEvent{}, - PinAdded: PinAddedEvent{}, - PinRemoved: PinRemovedEvent{}, - ReactionAdded: ReactionAddedEvent{}, - ReactionRemoved: ReactionRemovedEvent{}, - SharedChannelInviteApproved: SharedChannelInviteApprovedEvent{}, - SharedChannelInviteAccepted: SharedChannelInviteAcceptedEvent{}, - SharedChannelInviteDeclined: SharedChannelInviteDeclinedEvent{}, - SharedChannelInviteReceived: SharedChannelInviteReceivedEvent{}, - TeamJoin: TeamJoinEvent{}, - TokensRevoked: TokensRevokedEvent{}, - EmojiChanged: EmojiChangedEvent{}, - WorkflowStepExecute: WorkflowStepExecuteEvent{}, - MessageMetadataPosted: MessageMetadataPostedEvent{}, - MessageMetadataUpdated: MessageMetadataUpdatedEvent{}, - MessageMetadataDeleted: MessageMetadataDeletedEvent{}, - TeamAccessGranted: TeamAccessGrantedEvent{}, - TeamAccessRevoked: TeamAccessRevokedEvent{}, - UserProfileChanged: UserProfileChangedEvent{}, - ChannelHistoryChanged: ChannelHistoryChangedEvent{}, - DndUpdated: DndUpdatedEvent{}, - DndUpdatedUser: DndUpdatedUserEvent{}, - EmailDomainChanged: EmailDomainChangedEvent{}, - GroupClose: GroupCloseEvent{}, - GroupHistoryChanged: GroupHistoryChangedEvent{}, - GroupOpen: GroupOpenEvent{}, - ImClose: ImCloseEvent{}, - ImCreated: ImCreatedEvent{}, - ImHistoryChanged: ImHistoryChangedEvent{}, - ImOpen: ImOpenEvent{}, - SubteamCreated: SubteamCreatedEvent{}, - SubteamMembersChanged: SubteamMembersChangedEvent{}, - SubteamSelfAdded: SubteamSelfAddedEvent{}, - SubteamSelfRemoved: SubteamSelfRemovedEvent{}, - SubteamUpdated: SubteamUpdatedEvent{}, - TeamDomainChange: TeamDomainChangeEvent{}, - TeamRename: TeamRenameEvent{}, - UserChange: UserChangeEvent{}, - AppDeleted: AppDeletedEvent{}, - AppInstalled: AppInstalledEvent{}, - AppRequested: AppRequestedEvent{}, - AppUninstalledTeam: AppUninstalledTeamEvent{}, - CallRejected: CallRejectedEvent{}, - ChannelShared: ChannelSharedEvent{}, - FileCreated: FileCreatedEvent{}, - FilePublic: FilePublicEvent{}, - FunctionExecuted: FunctionExecutedEvent{}, - InviteRequested: InviteRequestedEvent{}, - SharedChannelInviteRequested: SharedChannelInviteRequestedEvent{}, - StarAdded: StarAddedEvent{}, - StarRemoved: StarRemovedEvent{}, - UserHuddleChanged: UserHuddleChangedEvent{}, - UserStatusChanged: UserStatusChangedEvent{}, + AppDeleted: AppDeletedEvent{}, + AppHomeOpened: AppHomeOpenedEvent{}, + AppInstalled: AppInstalledEvent{}, + AppMention: AppMentionEvent{}, + AppRequested: AppRequestedEvent{}, + AppUninstalled: AppUninstalledEvent{}, + AppUninstalledTeam: AppUninstalledTeamEvent{}, + AssistantThreadContextChanged: AssistantThreadContextChangedEvent{}, + AssistantThreadStarted: AssistantThreadStartedEvent{}, + CallRejected: CallRejectedEvent{}, + ChannelArchive: ChannelArchiveEvent{}, + ChannelCreated: ChannelCreatedEvent{}, + ChannelDeleted: ChannelDeletedEvent{}, + ChannelHistoryChanged: ChannelHistoryChangedEvent{}, + ChannelIDChanged: ChannelIDChangedEvent{}, + ChannelLeft: ChannelLeftEvent{}, + ChannelRename: ChannelRenameEvent{}, + ChannelShared: ChannelSharedEvent{}, + ChannelUnarchive: ChannelUnarchiveEvent{}, + ChannelUnshared: ChannelUnsharedEvent{}, + CommandsChanged: CommandsChangedEvent{}, + DndUpdated: DndUpdatedEvent{}, + DndUpdatedUser: DndUpdatedUserEvent{}, + EmailDomainChanged: EmailDomainChangedEvent{}, + EmojiChanged: EmojiChangedEvent{}, + FileChange: FileChangeEvent{}, + FileCreated: FileCreatedEvent{}, + FileDeleted: FileDeletedEvent{}, + FilePublic: FilePublicEvent{}, + FileShared: FileSharedEvent{}, + FileUnshared: FileUnsharedEvent{}, + FunctionExecuted: FunctionExecutedEvent{}, + GridMigrationFinished: GridMigrationFinishedEvent{}, + GridMigrationStarted: GridMigrationStartedEvent{}, + GroupArchive: GroupArchiveEvent{}, + GroupClose: GroupCloseEvent{}, + GroupDeleted: GroupDeletedEvent{}, + GroupHistoryChanged: GroupHistoryChangedEvent{}, + GroupLeft: GroupLeftEvent{}, + GroupOpen: GroupOpenEvent{}, + GroupRename: GroupRenameEvent{}, + GroupUnarchive: GroupUnarchiveEvent{}, + ImClose: ImCloseEvent{}, + ImCreated: ImCreatedEvent{}, + ImHistoryChanged: ImHistoryChangedEvent{}, + ImOpen: ImOpenEvent{}, + InviteRequested: InviteRequestedEvent{}, + LinkShared: LinkSharedEvent{}, + MemberJoinedChannel: MemberJoinedChannelEvent{}, + MemberLeftChannel: MemberLeftChannelEvent{}, + Message: MessageEvent{}, + MessageMetadataDeleted: MessageMetadataDeletedEvent{}, + MessageMetadataPosted: MessageMetadataPostedEvent{}, + MessageMetadataUpdated: MessageMetadataUpdatedEvent{}, + PinAdded: PinAddedEvent{}, + PinRemoved: PinRemovedEvent{}, + ReactionAdded: ReactionAddedEvent{}, + ReactionRemoved: ReactionRemovedEvent{}, + SharedChannelInviteAccepted: SharedChannelInviteAcceptedEvent{}, + SharedChannelInviteApproved: SharedChannelInviteApprovedEvent{}, + SharedChannelInviteDeclined: SharedChannelInviteDeclinedEvent{}, + SharedChannelInviteReceived: SharedChannelInviteReceivedEvent{}, + SharedChannelInviteRequested: SharedChannelInviteRequestedEvent{}, + StarAdded: StarAddedEvent{}, + StarRemoved: StarRemovedEvent{}, + SubteamCreated: SubteamCreatedEvent{}, + SubteamMembersChanged: SubteamMembersChangedEvent{}, + SubteamSelfAdded: SubteamSelfAddedEvent{}, + SubteamSelfRemoved: SubteamSelfRemovedEvent{}, + SubteamUpdated: SubteamUpdatedEvent{}, + TeamAccessGranted: TeamAccessGrantedEvent{}, + TeamAccessRevoked: TeamAccessRevokedEvent{}, + TeamDomainChange: TeamDomainChangeEvent{}, + TeamJoin: TeamJoinEvent{}, + TeamRename: TeamRenameEvent{}, + TokensRevoked: TokensRevokedEvent{}, + UserChange: UserChangeEvent{}, + UserHuddleChanged: UserHuddleChangedEvent{}, + UserProfileChanged: UserProfileChangedEvent{}, + UserStatusChanged: UserStatusChangedEvent{}, + EntityDetailsRequested: EntityDetailsRequestedEvent{}, } diff --git a/vendor/github.com/slack-go/slack/slackevents/parsers.go b/vendor/github.com/slack-go/slack/slackevents/parsers.go index 9e8c22b7f..80308eae7 100644 --- a/vendor/github.com/slack-go/slack/slackevents/parsers.go +++ b/vendor/github.com/slack-go/slack/slackevents/parsers.go @@ -10,23 +10,16 @@ import ( "github.com/slack-go/slack" ) -// eventsMap checks both slack.EventsMapping and -// and slackevents.EventsAPIInnerEventMapping. If the event -// exists, returns the unmarshalled struct instance of -// target for the matching event type. -// TODO: Consider moving all events into its own package? +// eventsMap checks both slackevents.EventsAPIInnerEventMapping and slack.EventMapping +// (RTM). EventsAPI mapping is checked first because both define a "message" type, and +// EventsAPI's MessageEvent is the correct choice for Events API payloads. func eventsMap(t string) (interface{}, bool) { - // Must parse EventsAPI FIRST as both RTM and EventsAPI - // have a type: "Message" event. - // TODO: Handle these cases more explicitly. + // EventsAPI mapping takes precedence over RTM mapping. v, exists := EventsAPIInnerEventMapping[EventsAPIType(t)] if exists { return v, exists } v, exists = slack.EventMapping[t] - if exists { - return v, exists - } return v, exists } @@ -262,6 +255,22 @@ func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error }, nil } +// Deprecated: ParseActionEvent cannot parse block_actions payloads and will return an +// unmarshalling error for them. Use [slack.InteractionCallback] with [json.Unmarshal] +// instead, or [slack.InteractionCallbackParse] to parse directly from an HTTP request. +// InteractionCallback handles all interaction types (block_actions, interactive_message, +// view_submission, etc.). +// +// Migration example: +// +// // Before (broken for block_actions): +// action, err := slackevents.ParseActionEvent(payload, slackevents.OptionNoVerifyToken()) +// +// // After (handles all interaction types): +// var ic slack.InteractionCallback +// err := json.Unmarshal([]byte(payload), &ic) +// // Use ic.ActionCallback.BlockActions for block actions +// // Use ic.ActionCallback.AttachmentActions for legacy attachment actions func ParseActionEvent(payloadString string, opts ...Option) (MessageAction, error) { byteString := []byte(payloadString) action := MessageAction{} diff --git a/vendor/github.com/slack-go/slack/socket_mode.go b/vendor/github.com/slack-go/slack/socket_mode.go index 69e40d99d..9498cdf73 100644 --- a/vendor/github.com/slack-go/slack/socket_mode.go +++ b/vendor/github.com/slack-go/slack/socket_mode.go @@ -2,6 +2,7 @@ package slack import ( "context" + "net/url" ) // SocketModeConnection contains various details about the SocketMode connection. @@ -21,7 +22,7 @@ type openResponseFull struct { // To have a fully managed Socket Mode connection, use `socketmode.New()`, and call `Run()` on it. func (api *Client) StartSocketModeContext(ctx context.Context) (info *SocketModeConnection, websocketURL string, err error) { response := &openResponseFull{} - err = postJSON(ctx, api.httpclient, api.endpoint+"apps.connections.open", api.appLevelToken, nil, response, api) + err = api.postJSONMethod(ctx, "apps.connections.open", api.appLevelToken, nil, response) if err != nil { return nil, "", err } @@ -30,5 +31,15 @@ func (api *Client) StartSocketModeContext(ctx context.Context) (info *SocketMode api.Debugln("Using URL:", response.SocketModeConnection.URL) } + // According to the API documentation at https://api.slack.com/apis/socket-mode, we + // can add a query parameter `debug_reconnects=true` to the URL to make the connection + // time significantly shorter (360 seconds). + if api.debug { + u, _ := url.Parse(response.SocketModeConnection.URL) + q := u.Query() + q.Set("debug_reconnects", "true") + u.RawQuery = q.Encode() + response.SocketModeConnection.URL = u.String() + } return &response.SocketModeConnection, response.SocketModeConnection.URL, response.Err() } diff --git a/vendor/github.com/slack-go/slack/stars.go b/vendor/github.com/slack-go/slack/stars.go index 51926854e..0adb28c53 100644 --- a/vendor/github.com/slack-go/slack/stars.go +++ b/vendor/github.com/slack-go/slack/stars.go @@ -8,31 +8,28 @@ import ( ) const ( - DEFAULT_STARS_USER = "" - DEFAULT_STARS_COUNT = 100 - DEFAULT_STARS_PAGE = 1 + DEFAULT_STARS_USER = "" ) type StarsParameters struct { - User string - Count int - Page int + User string + Cursor string + Limit int + TeamID string } type StarredItem Item type listResponseFull struct { - Items []Item `json:"items"` - Paging `json:"paging"` + Items []Item `json:"items"` SlackResponse + ResponseMetadata `json:"response_metadata"` } // NewStarsParameters initialises StarsParameters with default values func NewStarsParameters() StarsParameters { return StarsParameters{ - User: DEFAULT_STARS_USER, - Count: DEFAULT_STARS_COUNT, - Page: DEFAULT_STARS_PAGE, + User: DEFAULT_STARS_USER, } } @@ -100,37 +97,40 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I // ListStars returns information about the stars a user added. // For more information see the ListStarsContext documentation. -func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) { +func (api *Client) ListStars(params StarsParameters) ([]Item, string, error) { return api.ListStarsContext(context.Background(), params) } // ListStarsContext returns information about the stars a user added with a custom context. // Slack API docs: https://api.slack.com/methods/stars.list -func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) { +func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, string, error) { values := url.Values{ "token": {api.token}, } if params.User != DEFAULT_STARS_USER { values.Add("user", params.User) } - if params.Count != DEFAULT_STARS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) } - if params.Page != DEFAULT_STARS_PAGE { - values.Add("page", strconv.Itoa(params.Page)) + if params.TeamID != "" { + values.Add("team_id", params.TeamID) } response := &listResponseFull{} err := api.postMethod(ctx, "stars.list", values, response) if err != nil { - return nil, nil, err + return nil, "", err } if err := response.Err(); err != nil { - return nil, nil, err + return nil, "", err } - return response.Items, &response.Paging, nil + return response.Items, response.ResponseMetadata.Cursor, nil } // GetStarred returns a list of StarredItem items. @@ -139,31 +139,31 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) // be looking at according to what is in the Type: // // for _, item := range items { -// switch c.Type { -// case "file_comment": -// log.Println(c.Comment) -// case "file": -// ... +// switch c.Type { +// case "file_comment": +// log.Println(c.Comment) +// case "file": +// ... // } // // This function still exists to maintain backwards compatibility. // I exposed it as returning []StarredItem, so it shall stay as StarredItem. -func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) { +func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, string, error) { return api.GetStarredContext(context.Background(), params) } // GetStarredContext returns a list of StarredItem items with a custom context // For more details see GetStarred -func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, *Paging, error) { - items, paging, err := api.ListStarsContext(ctx, params) +func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, string, error) { + items, nextCursor, err := api.ListStarsContext(ctx, params) if err != nil { - return nil, nil, err + return nil, "", err } starredItems := make([]StarredItem, len(items)) for i, item := range items { starredItems[i] = StarredItem(item) } - return starredItems, paging, nil + return starredItems, nextCursor, nil } type listResponsePaginated struct { diff --git a/vendor/github.com/slack-go/slack/team.go b/vendor/github.com/slack-go/slack/team.go index 4e890c2be..55364a9b4 100644 --- a/vendor/github.com/slack-go/slack/team.go +++ b/vendor/github.com/slack-go/slack/team.go @@ -6,11 +6,6 @@ import ( "strconv" ) -const ( - DEFAULT_LOGINS_COUNT = 100 - DEFAULT_LOGINS_PAGE = 1 -) - type TeamResponse struct { Team TeamInfo `json:"team"` SlackResponse @@ -46,8 +41,8 @@ type TeamProfileField struct { type LoginResponse struct { Logins []Login `json:"logins"` - Paging `json:"paging"` SlackResponse + ResponseMetadata `json:"response_metadata"` } type Login struct { @@ -75,16 +70,14 @@ type BillingActive struct { // AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request type AccessLogParameters struct { TeamID string - Count int - Page int + Cursor string + Limit int + Before int } // NewAccessLogParameters provides an instance of AccessLogParameters with all the sane default values set func NewAccessLogParameters() AccessLogParameters { - return AccessLogParameters{ - Count: DEFAULT_LOGINS_COUNT, - Page: DEFAULT_LOGINS_PAGE, - } + return AccessLogParameters{} } func (api *Client) teamRequest(ctx context.Context, path string, values url.Values) (*TeamResponse, error) { @@ -116,7 +109,7 @@ func (api *Client) accessLogsRequest(ctx context.Context, path string, values ur return response, response.Err() } -func (api *Client) teamProfileRequest(ctx context.Context, client httpClient, path string, values url.Values) (*TeamProfileResponse, error) { +func (api *Client) teamProfileRequest(ctx context.Context, path string, values url.Values) (*TeamProfileResponse, error) { response := &TeamProfileResponse{} err := api.postMethod(ctx, path, values, response) if err != nil { @@ -184,7 +177,7 @@ func (api *Client) GetTeamProfileContext(ctx context.Context, teamID ...string) values["team_id"] = teamID } - response, err := api.teamProfileRequest(ctx, api.httpclient, "team.profile.get", values) + response, err := api.teamProfileRequest(ctx, "team.profile.get", values) if err != nil { return nil, err } @@ -193,31 +186,34 @@ func (api *Client) GetTeamProfileContext(ctx context.Context, teamID ...string) // GetAccessLogs retrieves a page of logins according to the parameters given. // For more information see the GetAccessLogsContext documentation. -func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) { +func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, string, error) { return api.GetAccessLogsContext(context.Background(), params) } // GetAccessLogsContext retrieves a page of logins according to the parameters given with a custom context. // Slack API docs: https://api.slack.com/methods/team.accessLogs -func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) { +func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, string, error) { values := url.Values{ "token": {api.token}, } if params.TeamID != "" { values.Add("team_id", params.TeamID) } - if params.Count != DEFAULT_LOGINS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) } - if params.Page != DEFAULT_LOGINS_PAGE { - values.Add("page", strconv.Itoa(params.Page)) + if params.Before != 0 { + values.Add("before", strconv.Itoa(params.Before)) } response, err := api.accessLogsRequest(ctx, "team.accessLogs", values) if err != nil { - return nil, nil, err + return nil, "", err } - return response.Logins, &response.Paging, nil + return response.Logins, response.ResponseMetadata.Cursor, nil } type GetBillableInfoParams struct { diff --git a/vendor/github.com/slack-go/slack/usergroups.go b/vendor/github.com/slack-go/slack/usergroups.go index 0c2ec4c9a..b5c545457 100644 --- a/vendor/github.com/slack-go/slack/usergroups.go +++ b/vendor/github.com/slack-go/slack/usergroups.go @@ -3,6 +3,7 @@ package slack import ( "context" "net/url" + "strconv" "strings" ) @@ -50,20 +51,57 @@ func (api *Client) userGroupRequest(ctx context.Context, path string, values url return response, response.Err() } +// createUserGroupParams contains arguments for CreateUserGroup method call +type createUserGroupParams struct { + enableSection bool + includeCount bool +} + +// CreateUserGroupOption options for the CreateUserGroup method call. +type CreateUserGroupOption func(*createUserGroupParams) + +// CreateUserGroupOptionEnableSection enable the section for the user group (default: false) +func CreateUserGroupOptionEnableSection(enableSection bool) CreateUserGroupOption { + return func(params *createUserGroupParams) { + params.enableSection = enableSection + } +} + +// CreateUserGroupOptionIncludeCount include the number of users in each User Group +func CreateUserGroupOptionIncludeCount(includeCount bool) CreateUserGroupOption { + return func(params *createUserGroupParams) { + params.includeCount = includeCount + } +} + // CreateUserGroup creates a new user group. // For more information see the CreateUserGroupContext documentation. -func (api *Client) CreateUserGroup(userGroup UserGroup) (UserGroup, error) { - return api.CreateUserGroupContext(context.Background(), userGroup) +func (api *Client) CreateUserGroup(userGroup UserGroup, options ...CreateUserGroupOption) (UserGroup, error) { + return api.CreateUserGroupContext(context.Background(), userGroup, options...) } // CreateUserGroupContext creates a new user group with a custom context. // Slack API docs: https://api.slack.com/methods/usergroups.create -func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) { +func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup, options ...CreateUserGroupOption) (UserGroup, error) { + params := createUserGroupParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "name": {userGroup.Name}, } + if params.enableSection { + values["enable_section"] = []string{strconv.FormatBool(params.enableSection)} + } + + if params.includeCount { + values["include_count"] = []string{strconv.FormatBool(params.includeCount)} + } + if userGroup.TeamID != "" { values["team_id"] = []string{userGroup.TeamID} } @@ -87,20 +125,57 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro return response.UserGroup, nil } +// DisableUserGroupParams contains arguments for DisableUserGroup method calls. +type DisableUserGroupParams struct { + IncludeCount bool + TeamID string +} + +// DisableUserGroupOption options for the DisableUserGroup method calls. +type DisableUserGroupOption func(*DisableUserGroupParams) + +// DisableUserGroupOptionIncludeCount include the count of User Groups (default: false) +func DisableUserGroupOptionIncludeCount(b bool) DisableUserGroupOption { + return func(params *DisableUserGroupParams) { + params.IncludeCount = b + } +} + +// DisableUserGroupOptionTeamID include team Id +func DisableUserGroupOptionTeamID(teamID string) DisableUserGroupOption { + return func(params *DisableUserGroupParams) { + params.TeamID = teamID + } +} + // DisableUserGroup disables an existing user group. // For more information see the DisableUserGroupContext documentation. -func (api *Client) DisableUserGroup(userGroup string) (UserGroup, error) { - return api.DisableUserGroupContext(context.Background(), userGroup) +func (api *Client) DisableUserGroup(userGroup string, options ...DisableUserGroupOption) (UserGroup, error) { + return api.DisableUserGroupContext(context.Background(), userGroup, options...) } // DisableUserGroupContext disables an existing user group with a custom context. // Slack API docs: https://api.slack.com/methods/usergroups.disable -func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) { +func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string, options ...DisableUserGroupOption) (UserGroup, error) { + params := DisableUserGroupParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "usergroup": {userGroup}, } + if params.IncludeCount { + values.Add("include_count", "true") + } + + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + response, err := api.userGroupRequest(ctx, "usergroups.disable", values) if err != nil { return UserGroup{}, err @@ -108,20 +183,57 @@ func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string return response.UserGroup, nil } +// EnableUserGroupParams contains arguments for EnableUserGroup method calls. +type EnableUserGroupParams struct { + IncludeCount bool + TeamID string +} + +// EnableUserGroupOption options for the EnableUserGroup method calls. +type EnableUserGroupOption func(*EnableUserGroupParams) + +// EnableUserGroupOptionIncludeCount include the count of User Groups (default: false) +func EnableUserGroupOptionIncludeCount(b bool) EnableUserGroupOption { + return func(params *EnableUserGroupParams) { + params.IncludeCount = b + } +} + +// EnableUserGroupOptionTeamID include team Id +func EnableUserGroupOptionTeamID(teamID string) EnableUserGroupOption { + return func(params *EnableUserGroupParams) { + params.TeamID = teamID + } +} + // EnableUserGroup enables an existing user group. // For more information see the EnableUserGroupContext documentation. -func (api *Client) EnableUserGroup(userGroup string) (UserGroup, error) { - return api.EnableUserGroupContext(context.Background(), userGroup) +func (api *Client) EnableUserGroup(userGroup string, options ...EnableUserGroupOption) (UserGroup, error) { + return api.EnableUserGroupContext(context.Background(), userGroup, options...) } // EnableUserGroupContext enables an existing user group with a custom context. // Slack API docs: https://api.slack.com/methods/usergroups.enable -func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) { +func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string, options ...EnableUserGroupOption) (UserGroup, error) { + params := EnableUserGroupParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "usergroup": {userGroup}, } + if params.IncludeCount { + values.Add("include_count", "true") + } + + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + response, err := api.userGroupRequest(ctx, "usergroups.enable", values) if err != nil { return UserGroup{}, err @@ -132,7 +244,12 @@ func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) // GetUserGroupsOption options for the GetUserGroups method call. type GetUserGroupsOption func(*GetUserGroupsParams) +// Deprecated: GetUserGroupsOptionWithTeamID is deprecated, use GetUserGroupsOptionTeamID instead func GetUserGroupsOptionWithTeamID(teamID string) GetUserGroupsOption { + return GetUserGroupsOptionTeamID(teamID) +} + +func GetUserGroupsOptionTeamID(teamID string) GetUserGroupsOption { return func(params *GetUserGroupsParams) { params.TeamID = teamID } @@ -236,12 +353,28 @@ func UpdateUserGroupsOptionChannels(channels []string) UpdateUserGroupsOption { } } +// UpdateUserGroupsOptionEnableSection enable the section for the user group (default: false) +func UpdateUserGroupsOptionEnableSection(enableSection bool) UpdateUserGroupsOption { + return func(params *UpdateUserGroupsParams) { + params.EnableSection = enableSection + } +} + +// UpdateUserGroupsOptionTeamID specify the team id for the User Group. (default: nil, so it's no-op) +func UpdateUserGroupsOptionTeamID(teamID string) UpdateUserGroupsOption { + return func(params *UpdateUserGroupsParams) { + params.TeamID = teamID + } +} + // UpdateUserGroupsParams contains arguments for UpdateUserGroup method call type UpdateUserGroupsParams struct { - Name string - Handle string - Description *string - Channels *[]string + Name string + Handle string + Description *string + Channels *[]string + EnableSection bool + TeamID string } // UpdateUserGroup will update an existing user group. @@ -280,6 +413,14 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroupID strin values["channels"] = []string{strings.Join(*params.Channels, ",")} } + if params.EnableSection { + values["enable_section"] = []string{strconv.FormatBool(params.EnableSection)} + } + + if params.TeamID != "" { + values["team_id"] = []string{params.TeamID} + } + response, err := api.userGroupRequest(ctx, "usergroups.update", values) if err != nil { return UserGroup{}, err @@ -287,20 +428,57 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroupID strin return response.UserGroup, nil } +// GetUserGroupMembersOption options for the GetUserGroupMembers method call. +type GetUserGroupMembersOption func(*GetUserGroupMembersParams) + +// GetUserGroupMembersParams contains arguments for GetUserGroupMembers method call +type GetUserGroupMembersParams struct { + IncludeDisabled bool + TeamID string +} + +// GetUserGroupMembersOptionIncludeDisabled include disabled User Groups (default: false) +func GetUserGroupMembersOptionIncludeDisabled(b bool) GetUserGroupMembersOption { + return func(params *GetUserGroupMembersParams) { + params.IncludeDisabled = b + } +} + +// GetUserGroupMembersOptionTeamID include team Id +func GetUserGroupMembersOptionTeamID(teamID string) GetUserGroupMembersOption { + return func(params *GetUserGroupMembersParams) { + params.TeamID = teamID + } +} + // GetUserGroupMembers will retrieve the current list of users in a group. // For more information see the GetUserGroupMembersContext documentation. -func (api *Client) GetUserGroupMembers(userGroup string) ([]string, error) { - return api.GetUserGroupMembersContext(context.Background(), userGroup) +func (api *Client) GetUserGroupMembers(userGroup string, options ...GetUserGroupMembersOption) ([]string, error) { + return api.GetUserGroupMembersContext(context.Background(), userGroup, options...) } // GetUserGroupMembersContext will retrieve the current list of users in a group with a custom context. // Slack API docs: https://api.slack.com/methods/usergroups.users.list -func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup string) ([]string, error) { +func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup string, options ...GetUserGroupMembersOption) ([]string, error) { + params := GetUserGroupMembersParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "usergroup": {userGroup}, } + if params.IncludeDisabled { + values.Add("include_disabled", "true") + } + + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + response, err := api.userGroupRequest(ctx, "usergroups.users.list", values) if err != nil { return []string{}, err @@ -308,24 +486,99 @@ func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup str return response.Users, nil } +// UpdateUserGroupMembersOption options for the UpdateUserGroupMembers method call. +type UpdateUserGroupMembersOption func(*UpdateUserGroupMembersParams) + +// UpdateUserGroupMembersParams contains arguments for UpdateUserGroupMembers method call +type UpdateUserGroupMembersParams struct { + AdditionalChannels []string + IncludeCount bool + IsShared bool + TeamID string +} + +// UpdateUserGroupMembersOptionAdditionalChannels include additional channels +func UpdateUserGroupMembersOptionAdditionalChannels(channels []string) UpdateUserGroupMembersOption { + return func(params *UpdateUserGroupMembersParams) { + params.AdditionalChannels = channels + } +} + +// UpdateUserGroupMembersOptionIsShared include the count of User Groups (default: false) +func UpdateUserGroupMembersOptionIsShared(b bool) UpdateUserGroupMembersOption { + return func(params *UpdateUserGroupMembersParams) { + params.IsShared = b + } +} + +// UpdateUserGroupMembersOptionIncludeCount include the count of User Groups (default: false) +func UpdateUserGroupMembersOptionIncludeCount(b bool) UpdateUserGroupMembersOption { + return func(params *UpdateUserGroupMembersParams) { + params.IncludeCount = b + } +} + +// UpdateUserGroupMembersOptionTeamID include team Id +func UpdateUserGroupMembersOptionTeamID(teamID string) UpdateUserGroupMembersOption { + return func(params *UpdateUserGroupMembersParams) { + params.TeamID = teamID + } +} + // UpdateUserGroupMembers will update the members of an existing user group. // For more information see the UpdateUserGroupMembersContext documentation. -func (api *Client) UpdateUserGroupMembers(userGroup string, members string) (UserGroup, error) { - return api.UpdateUserGroupMembersContext(context.Background(), userGroup, members) +func (api *Client) UpdateUserGroupMembers(userGroup string, members string, options ...UpdateUserGroupMembersOption) (UserGroup, error) { + return api.UpdateUserGroupMembersContext(context.Background(), userGroup, members, options...) } // UpdateUserGroupMembersContext will update the members of an existing user group with a custom context. // Slack API docs: https://api.slack.com/methods/usergroups.update -func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup string, members string) (UserGroup, error) { +func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup string, members string, options ...UpdateUserGroupMembersOption) (UserGroup, error) { + params := UpdateUserGroupMembersParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "usergroup": {userGroup}, "users": {members}, } + if params.IncludeCount { + values.Add("include_count", "true") + } + + if params.IsShared { + values.Add("is_shared", "true") + } + + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + + if len(params.AdditionalChannels) > 0 { + values["additional_channels"] = []string{strings.Join(params.AdditionalChannels, ",")} + } + response, err := api.userGroupRequest(ctx, "usergroups.users.update", values) if err != nil { return UserGroup{}, err } return response.UserGroup, nil } + +// UpdateUserGroupMembersList updates the members of an existing user group, +// accepting a slice of user IDs. This is a convenience wrapper around +// UpdateUserGroupMembers for use with APIs that return []string (e.g. +// GetUserGroupMembers). +func (api *Client) UpdateUserGroupMembersList(userGroup string, members []string, options ...UpdateUserGroupMembersOption) (UserGroup, error) { + return api.UpdateUserGroupMembersContext(context.Background(), userGroup, strings.Join(members, ","), options...) +} + +// UpdateUserGroupMembersListContext updates the members of an existing user +// group with a custom context, accepting a slice of user IDs. +func (api *Client) UpdateUserGroupMembersListContext(ctx context.Context, userGroup string, members []string, options ...UpdateUserGroupMembersOption) (UserGroup, error) { + return api.UpdateUserGroupMembersContext(ctx, userGroup, strings.Join(members, ","), options...) +} diff --git a/vendor/github.com/slack-go/slack/users.go b/vendor/github.com/slack-go/slack/users.go index b51b1721f..541baae78 100644 --- a/vendor/github.com/slack-go/slack/users.go +++ b/vendor/github.com/slack-go/slack/users.go @@ -17,32 +17,41 @@ const ( // UserProfile contains all the information details of a given user type UserProfile struct { - FirstName string `json:"first_name,omitempty"` - LastName string `json:"last_name,omitempty"` - RealName string `json:"real_name"` - RealNameNormalized string `json:"real_name_normalized"` - DisplayName string `json:"display_name"` - DisplayNameNormalized string `json:"display_name_normalized"` - AvatarHash string `json:"avatar_hash"` - Email string `json:"email,omitempty"` - Skype string `json:"skyp,omitempty"` - Phone string `json:"phone,omitempty"` - Image24 string `json:"image_24"` - Image32 string `json:"image_32"` - Image48 string `json:"image_48"` - Image72 string `json:"image_72"` - Image192 string `json:"image_192"` - Image512 string `json:"image_512"` - ImageOriginal string `json:"image_original,omitempty"` - Title string `json:"title,omitempty"` - BotID string `json:"bot_id,omitempty"` - ApiAppID string `json:"api_app_id,omitempty"` - StatusText string `json:"status_text,omitempty"` - StatusEmoji string `json:"status_emoji,omitempty"` - StatusEmojiDisplayInfo []UserProfileStatusEmojiDisplayInfo `json:"status_emoji_display_info,omitempty"` - StatusExpiration int `json:"status_expiration,omitempty"` - Team string `json:"team"` - Fields UserProfileCustomFields `json:"fields,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + RealName string `json:"real_name"` + RealNameNormalized string `json:"real_name_normalized"` + DisplayName string `json:"display_name"` + DisplayNameNormalized string `json:"display_name_normalized"` + Pronouns string `json:"pronouns,omitempty"` + AvatarHash string `json:"avatar_hash"` + Email string `json:"email,omitempty"` + Skype string `json:"skype,omitempty"` + Phone string `json:"phone,omitempty"` + Image24 string `json:"image_24"` + Image32 string `json:"image_32"` + Image48 string `json:"image_48"` + Image72 string `json:"image_72"` + Image192 string `json:"image_192"` + Image512 string `json:"image_512"` + Image1024 string `json:"image_1024,omitempty"` + ImageOriginal string `json:"image_original,omitempty"` + IsCustomImage bool `json:"is_custom_image,omitempty"` + Title string `json:"title,omitempty"` + BotID string `json:"bot_id,omitempty"` + ApiAppID string `json:"api_app_id,omitempty"` + AlwaysActive bool `json:"always_active,omitempty"` + StatusText string `json:"status_text,omitempty"` + StatusEmoji string `json:"status_emoji,omitempty"` + StatusEmojiDisplayInfo []UserProfileStatusEmojiDisplayInfo `json:"status_emoji_display_info,omitempty"` + StatusExpiration int `json:"status_expiration,omitempty"` + StatusTextCanonical string `json:"status_text_canonical,omitempty"` + HuddleState string `json:"huddle_state,omitempty"` + HuddleStateExpirationTS int `json:"huddle_state_expiration_ts,omitempty"` + StartDate string `json:"start_date,omitempty"` + GuestInvitedBy string `json:"guest_invited_by,omitempty"` + Team string `json:"team"` + Fields UserProfileCustomFields `json:"fields,omitempty"` } type UserProfileStatusEmojiDisplayInfo struct { @@ -72,7 +81,7 @@ func (fields *UserProfileCustomFields) UnmarshalJSON(b []byte) error { // MarshalJSON is the implementation of the json.Marshaler interface. func (fields UserProfileCustomFields) MarshalJSON() ([]byte, error) { if len(fields.fields) == 0 { - return []byte("[]"), nil + return []byte("{}"), nil } return json.Marshal(fields.fields) } @@ -111,32 +120,37 @@ type UserProfileCustomField struct { // User contains all the information of a user type User struct { - ID string `json:"id"` - TeamID string `json:"team_id"` - Name string `json:"name"` - Deleted bool `json:"deleted"` - Color string `json:"color"` - RealName string `json:"real_name"` - TZ string `json:"tz,omitempty"` - TZLabel string `json:"tz_label"` - TZOffset int `json:"tz_offset"` - Profile UserProfile `json:"profile"` - IsBot bool `json:"is_bot"` - IsAdmin bool `json:"is_admin"` - IsOwner bool `json:"is_owner"` - IsPrimaryOwner bool `json:"is_primary_owner"` - IsRestricted bool `json:"is_restricted"` - IsUltraRestricted bool `json:"is_ultra_restricted"` - IsStranger bool `json:"is_stranger"` - IsAppUser bool `json:"is_app_user"` - IsInvitedUser bool `json:"is_invited_user"` - Has2FA bool `json:"has_2fa"` - TwoFactorType *string `json:"two_factor_type"` - HasFiles bool `json:"has_files"` - Presence string `json:"presence"` - Locale string `json:"locale"` - Updated JSONTime `json:"updated"` - Enterprise EnterpriseUser `json:"enterprise_user,omitempty"` + ID string `json:"id"` + TeamID string `json:"team_id"` + Name string `json:"name"` + Username string `json:"username,omitempty"` + Deleted bool `json:"deleted"` + Color string `json:"color"` + RealName string `json:"real_name"` + TZ string `json:"tz,omitempty"` + TZLabel string `json:"tz_label"` + TZOffset int `json:"tz_offset"` + Profile UserProfile `json:"profile"` + IsBot bool `json:"is_bot"` + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + IsPrimaryOwner bool `json:"is_primary_owner"` + IsRestricted bool `json:"is_restricted"` + IsUltraRestricted bool `json:"is_ultra_restricted"` + IsStranger bool `json:"is_stranger"` + IsAppUser bool `json:"is_app_user"` + IsConnectorBot bool `json:"is_connector_bot"` + IsWorkflowBot bool `json:"is_workflow_bot"` + IsInvitedUser bool `json:"is_invited_user"` + IsEmailConfirmed bool `json:"is_email_confirmed"` + Has2FA *bool `json:"has_2fa,omitempty"` + TwoFactorType *string `json:"two_factor_type"` + HasFiles bool `json:"has_files"` + Presence string `json:"presence"` + Locale string `json:"locale"` + Updated JSONTime `json:"updated"` + WhoCanShareContactCard string `json:"who_can_share_contact_card,omitempty"` + Enterprise EnterpriseUser `json:"enterprise_user,omitempty"` } // UserPresence contains details about a user online status @@ -168,13 +182,14 @@ type UserIdentity struct { } // EnterpriseUser is present when a user is part of Slack Enterprise Grid -// https://api.slack.com/types/user#enterprise_grid_user_objects +// https://docs.slack.dev/reference/objects/user-object/#fields type EnterpriseUser struct { ID string `json:"id"` EnterpriseID string `json:"enterprise_id"` EnterpriseName string `json:"enterprise_name"` IsAdmin bool `json:"is_admin"` IsOwner bool `json:"is_owner"` + IsPrimaryOwner bool `json:"is_primary_owner"` Teams []string `json:"teams"` } @@ -315,6 +330,13 @@ func GetUsersOptionTeamID(teamId string) GetUsersOption { } } +// GetUsersOptionCursor set the cursor to the next page of results +func GetUsersOptionCursor(cursor string) GetUsersOption { + return func(p *UserPagination) { + p.Cursor = cursor + } +} + func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) { up = UserPagination{ c: c, @@ -330,12 +352,13 @@ func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) // UserPagination allows for paginating over the users type UserPagination struct { - Users []User - limit int - presence bool - teamId string - previousResp *ResponseMetadata - c *Client + Users []User + Cursor string + limit int + presence bool + teamId string + complete bool + c *Client } // Done checks if the pagination has completed @@ -357,17 +380,15 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) resp *userResponseFull ) - if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") { + if t.c == nil || t.complete { return t, errPaginationComplete } - t.previousResp = t.previousResp.initialize() - values := url.Values{ "limit": {strconv.Itoa(t.limit)}, "presence": {strconv.FormatBool(t.presence)}, "token": {t.c.token}, - "cursor": {t.previousResp.Cursor}, + "cursor": {t.Cursor}, "team_id": {t.teamId}, "include_locale": {strconv.FormatBool(true)}, } @@ -378,7 +399,8 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) t.c.Debugf("GetUsersContext: got %d users; metadata %v", len(resp.Members), resp.Metadata) t.Users = resp.Members - t.previousResp = &resp.Metadata + t.Cursor = resp.Metadata.Cursor + t.complete = t.Cursor == "" return t, nil } @@ -584,6 +606,54 @@ func (api *Client) SetUserRealNameContextWithUser(ctx context.Context, user, rea return response.Err() } +// SetUserProfile sets the profile for the provided user. +// For more information see the SetUserProfileContext documentation. +func (api *Client) SetUserProfile(user string, profile *UserProfile) error { + return api.SetUserProfileContext(context.Background(), user, profile) +} + +// SetUserProfileContext sets the profile for the provided user with a custom context. +// +// The profile parameter is serialized as-is. Fields present in the JSON (including +// zero-value fields without an omitempty tag, such as RealName and DisplayName) will +// be updated by Slack. To avoid unintended changes, retrieve the current profile with +// GetUserProfile, modify the desired fields, and pass the result. +// +// For setting individual fields, prefer the targeted methods: SetUserRealName, +// SetUserCustomStatus, SetUserCustomFields. +// +// If a workspace admin has mapped custom profile fields to standard fields (e.g. +// title), the custom field takes precedence. Update the custom field via +// SetUserCustomFields instead. +// +// The user parameter is required when setting another user's profile (admin only, +// paid plans). Pass an empty string to modify the authenticated user's own profile. +// +// Slack API docs: https://docs.slack.dev/reference/methods/users.profile.set/ +func (api *Client) SetUserProfileContext(ctx context.Context, user string, profile *UserProfile) error { + profileJSON, err := json.Marshal(profile) + if err != nil { + return err + } + + values := url.Values{ + "token": {api.token}, + "profile": {string(profileJSON)}, + } + + // optional field. It should not be set if empty + if user != "" { + values["user"] = []string{user} + } + + response := &userResponseFull{} + if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil { + return err + } + + return response.Err() +} + // SetUserCustomFields sets Custom Profile fields on the provided users account. // For more information see the SetUserCustomFieldsContext documentation. func (api *Client) SetUserCustomFields(userID string, customFields map[string]UserProfileCustomField) error { @@ -627,7 +697,7 @@ func (api *Client) SetUserCustomFieldsContext(ctx context.Context, userID string } response := &userResponseFull{} - if err := postForm(ctx, api.httpclient, APIURL+"users.profile.set", values, response, api); err != nil { + if _, err := postForm(ctx, api.httpclient, APIURL+"users.profile.set", values, response, api); err != nil { return err } @@ -660,16 +730,16 @@ func (api *Client) SetUserCustomStatusWithUser(user, statusText, statusEmoji str // // Slack API docs: https://api.slack.com/methods/users.profile.set func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user, statusText, statusEmoji string, statusExpiration int64) error { - // XXX(theckman): this anonymous struct is for making requests to the Slack - // API for setting and unsetting a User's Custom Status/Emoji. To change - // these values we must provide a JSON document as the profile POST field. + // This anonymous struct is for making requests to the Slack API for setting and + // unsetting a User's Custom Status/Emoji. To change these values we must provide a + // JSON document as the profile POST field. // - // We use an anonymous struct over UserProfile because to unset the values - // on the User's profile we cannot use the `json:"omitempty"` tag. This is - // because an empty string ("") is what's used to unset the values. Check - // out the API docs for more details: + // We use an anonymous struct over UserProfile because to unset the values on the + // User's profile we cannot use the `json:"omitempty"` tag. This is because an empty + // string ("") is what's used to unset the values. Check out the API docs for more + // details: // - // - https://api.slack.com/docs/presence-and-status#custom_status + // - https://docs.slack.dev/apis/web-api/user-presence-and-status/#custom-status profile, err := json.Marshal( &struct { StatusText string `json:"status_text"` diff --git a/vendor/github.com/slack-go/slack/views.go b/vendor/github.com/slack-go/slack/views.go index 0822d2cb2..c16503c0e 100644 --- a/vendor/github.com/slack-go/slack/views.go +++ b/vendor/github.com/slack-go/slack/views.go @@ -70,12 +70,30 @@ type ViewSubmissionResponse struct { Errors map[string]string `json:"errors,omitempty"` } +// NewClearViewSubmissionResponse closes all open modals in the current stack. +// +// For HTTP-based apps, marshal this to JSON and write it as the HTTP response +// body. The response is not sent until the handler returns, so start any slow +// work in a goroutine and return promptly. +// +// For Socket Mode apps, pass this as the payload argument to Ack(). +// +// See https://docs.slack.dev/surfaces/modals#closing_views func NewClearViewSubmissionResponse() *ViewSubmissionResponse { return &ViewSubmissionResponse{ ResponseAction: RAClear, } } +// NewUpdateViewSubmissionResponse replaces the current modal with a new view. +// +// For HTTP-based apps, marshal this to JSON and write it as the HTTP response +// body. The response is not sent until the handler returns, so start any slow +// work in a goroutine and return promptly. +// +// For Socket Mode apps, pass this as the payload argument to Ack(). +// +// See https://docs.slack.dev/surfaces/modals#updating_views func NewUpdateViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResponse { return &ViewSubmissionResponse{ ResponseAction: RAUpdate, @@ -83,6 +101,15 @@ func NewUpdateViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResp } } +// NewPushViewSubmissionResponse pushes a new view onto the modal stack. +// +// For HTTP-based apps, marshal this to JSON and write it as the HTTP response +// body. The response is not sent until the handler returns, so start any slow +// work in a goroutine and return promptly. +// +// For Socket Mode apps, pass this as the payload argument to Ack(). +// +// See https://docs.slack.dev/surfaces/modals#pushing_views func NewPushViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResponse { return &ViewSubmissionResponse{ ResponseAction: RAPush, @@ -90,6 +117,19 @@ func NewPushViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionRespon } } +// NewErrorsViewSubmissionResponse displays validation errors on form fields. +// +// The errors map keys must be the BlockID of an InputBlock in the view. Keys +// that reference other block types (e.g. SectionBlock) are silently ignored +// by Slack, which shows a generic "trouble connecting" error instead. +// +// For HTTP-based apps, marshal this to JSON and write it as the HTTP response +// body. The response is not sent until the handler returns, so start any slow +// work in a goroutine and return promptly. +// +// For Socket Mode apps, pass this as the payload argument to Ack(). +// +// See https://docs.slack.dev/surfaces/modals/#displaying_errors func NewErrorsViewSubmissionResponse(errors map[string]string) *ViewSubmissionResponse { return &ViewSubmissionResponse{ ResponseAction: RAErrors, @@ -110,6 +150,12 @@ type ModalViewRequest struct { ExternalID string `json:"external_id,omitempty"` } +type PublishViewContextRequest struct { + UserID string `json:"user_id"` + View HomeTabViewRequest `json:"view"` + Hash *string `json:"hash,omitempty"` +} + func (v *ModalViewRequest) ViewType() ViewType { return v.Type } @@ -131,12 +177,6 @@ type openViewRequest struct { View ModalViewRequest `json:"view"` } -type publishViewRequest struct { - UserID string `json:"user_id"` - View HomeTabViewRequest `json:"view"` - Hash string `json:"hash,omitempty"` -} - type pushViewRequest struct { TriggerID string `json:"trigger_id"` View ModalViewRequest `json:"view"` @@ -167,6 +207,9 @@ func ValidateUniqueBlockID(view ModalViewRequest) bool { for _, b := range view.Blocks.BlockSet { if inputBlock, ok := b.(*InputBlock); ok { + if inputBlock.BlockID == "" { + continue + } if _, ok := uniqueBlockID[inputBlock.BlockID]; ok { return false } @@ -178,7 +221,7 @@ func ValidateUniqueBlockID(view ModalViewRequest) bool { } // OpenViewContext opens a view for a user with a custom context. -// Slack API docs: https://api.slack.com/methods/views.open +// Slack API docs: https://docs.slack.dev/reference/methods/views.open func (api *Client) OpenViewContext( ctx context.Context, triggerID string, @@ -200,9 +243,8 @@ func (api *Client) OpenViewContext( if err != nil { return nil, err } - endpoint := api.endpoint + "views.open" resp := &ViewResponse{} - err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api) + err = api.postJSONMethod(ctx, "views.open", api.token, encoded, resp) if err != nil { return nil, err } @@ -212,32 +254,28 @@ func (api *Client) OpenViewContext( // PublishView publishes a static view for a user. // For more information see the PublishViewContext documentation. func (api *Client) PublishView(userID string, view HomeTabViewRequest, hash string) (*ViewResponse, error) { - return api.PublishViewContext(context.Background(), userID, view, hash) + var hashPtr *string + if hash != "" { + hashPtr = &hash + } + return api.PublishViewContext(context.Background(), PublishViewContextRequest{UserID: userID, View: view, Hash: hashPtr}) } // PublishViewContext publishes a static view for a user with a custom context. -// Slack API docs: https://api.slack.com/methods/views.publish +// Slack API docs: https://docs.slack.dev/reference/methods/views.publish func (api *Client) PublishViewContext( ctx context.Context, - userID string, - view HomeTabViewRequest, - hash string, + req PublishViewContextRequest, ) (*ViewResponse, error) { - if userID == "" { + if req.UserID == "" { return nil, ErrParametersMissing } - req := publishViewRequest{ - UserID: userID, - View: view, - Hash: hash, - } encoded, err := json.Marshal(req) if err != nil { return nil, err } - endpoint := api.endpoint + "views.publish" resp := &ViewResponse{} - err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api) + err = api.postJSONMethod(ctx, "views.publish", api.token, encoded, resp) if err != nil { return nil, err } @@ -251,7 +289,7 @@ func (api *Client) PushView(triggerID string, view ModalViewRequest) (*ViewRespo } // PushViewContext pushes a view onto the stack of a root view with a custom context. -// Slack API docs: https://api.slack.com/methods/views.push +// Slack API docs: https://docs.slack.dev/reference/methods/views.push func (api *Client) PushViewContext( ctx context.Context, triggerID string, @@ -268,9 +306,8 @@ func (api *Client) PushViewContext( if err != nil { return nil, err } - endpoint := api.endpoint + "views.push" resp := &ViewResponse{} - err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api) + err = api.postJSONMethod(ctx, "views.push", api.token, encoded, resp) if err != nil { return nil, err } @@ -284,7 +321,7 @@ func (api *Client) UpdateView(view ModalViewRequest, externalID, hash, viewID st } // UpdateViewContext updates an existing view with a custom context. -// Slack API docs: https://api.slack.com/methods/views.update +// Slack API docs: https://docs.slack.dev/reference/methods/views.update func (api *Client) UpdateViewContext( ctx context.Context, view ModalViewRequest, @@ -304,9 +341,8 @@ func (api *Client) UpdateViewContext( if err != nil { return nil, err } - endpoint := api.endpoint + "views.update" resp := &ViewResponse{} - err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api) + err = api.postJSONMethod(ctx, "views.update", api.token, encoded, resp) if err != nil { return nil, err } diff --git a/vendor/github.com/slack-go/slack/webhooks.go b/vendor/github.com/slack-go/slack/webhooks.go index 5a854f38b..729bce401 100644 --- a/vendor/github.com/slack-go/slack/webhooks.go +++ b/vendor/github.com/slack-go/slack/webhooks.go @@ -23,8 +23,8 @@ type WebhookMessage struct { ReplaceOriginal bool `json:"replace_original"` DeleteOriginal bool `json:"delete_original"` ReplyBroadcast bool `json:"reply_broadcast,omitempty"` - UnfurlLinks bool `json:"unfurl_links,omitempty"` - UnfurlMedia bool `json:"unfurl_media,omitempty"` + UnfurlLinks *bool `json:"unfurl_links,omitempty"` + UnfurlMedia *bool `json:"unfurl_media,omitempty"` } func PostWebhook(url string, msg *WebhookMessage) error { diff --git a/vendor/github.com/slack-go/slack/websocket_groups.go b/vendor/github.com/slack-go/slack/websocket_groups.go index eb88985c4..c35d5f357 100644 --- a/vendor/github.com/slack-go/slack/websocket_groups.go +++ b/vendor/github.com/slack-go/slack/websocket_groups.go @@ -7,9 +7,6 @@ type GroupCreatedEvent struct { Channel ChannelCreatedInfo `json:"channel"` } -// XXX: Should we really do this? event.Group is probably nicer than event.Channel -// even though the api returns "channel" - // GroupMarkedEvent represents the Group marked event type GroupMarkedEvent ChannelInfoEvent diff --git a/vendor/github.com/slack-go/slack/websocket_managed_conn.go b/vendor/github.com/slack-go/slack/websocket_managed_conn.go index f107b2a47..da861fa87 100644 --- a/vendor/github.com/slack-go/slack/websocket_managed_conn.go +++ b/vendor/github.com/slack-go/slack/websocket_managed_conn.go @@ -582,7 +582,10 @@ var EventMapping = map[string]interface{}{ "manual_presence_change": ManualPresenceChangeEvent{}, - "user_change": UserChangeEvent{}, + "user_change": UserChangeEvent{}, + "user_status_changed": UserStatusChangedEvent{}, + "user_huddle_changed": UserHuddleChangedEvent{}, + "user_profile_changed": UserProfileChangedEvent{}, "emoji_changed": EmojiChangedEvent{}, @@ -595,6 +598,10 @@ var EventMapping = map[string]interface{}{ "accounts_changed": AccountsChangedEvent{}, + "apps_uninstalled": AppsUninstalledEvent{}, + "activity": ActivityEvent{}, + "badge_counts_updated": BadgeCountsUpdatedEvent{}, + "reconnect_url": ReconnectUrlEvent{}, "member_joined_channel": MemberJoinedChannelEvent{}, @@ -608,4 +615,10 @@ var EventMapping = map[string]interface{}{ "desktop_notification": DesktopNotificationEvent{}, "mobile_in_app_notification": MobileInAppNotificationEvent{}, + + "channel_updated": ChannelUpdatedEvent{}, + + "sh_room_join": SHRoomJoinEvent{}, + "sh_room_leave": SHRoomLeaveEvent{}, + "sh_room_update": SHRoomUpdateEvent{}, } diff --git a/vendor/github.com/slack-go/slack/websocket_misc.go b/vendor/github.com/slack-go/slack/websocket_misc.go index 65a8bb65d..fb301f5eb 100644 --- a/vendor/github.com/slack-go/slack/websocket_misc.go +++ b/vendor/github.com/slack-go/slack/websocket_misc.go @@ -71,8 +71,34 @@ type ManualPresenceChangeEvent struct { // UserChangeEvent represents the user change event type UserChangeEvent struct { - Type string `json:"type"` - User User `json:"user"` + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +// UserStatusChangedEvent represents the user status changed event +type UserStatusChangedEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +// UserHuddleChangedEvent represents the user huddle changed event +type UserHuddleChangedEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +// UserProfileChangedEvent represents the user profile changed event +type UserProfileChangedEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` } // EmojiChangedEvent represents the emoji changed event @@ -139,3 +165,115 @@ type MemberLeftChannelEvent struct { ChannelType string `json:"channel_type"` Team string `json:"team"` } + +// ChannelUpdatedEvent is fired when a channel's properties are updated (tabs, meeting +// notes, etc.). +type ChannelUpdatedEvent struct { + Type string `json:"type"` + Updates map[string]any `json:"updates"` + Channel string `json:"channel"` + Channels []string `json:"channels"` + EventTS string `json:"event_ts"` + TS string `json:"ts"` +} + +// SHRoomRecording holds recording metadata for a Slack Call/Huddle room. +type SHRoomRecording struct { + CanRecordSummary string `json:"can_record_summary,omitempty"` +} + +// SHRoom represents a Slack Huddle/Call room. +type SHRoom struct { + ID string `json:"id"` + Name *string `json:"name"` // nullable in Slack's response + MediaServer string `json:"media_server"` + CreatedBy string `json:"created_by"` + DateStart int64 `json:"date_start"` + DateEnd int64 `json:"date_end"` + Participants []string `json:"participants"` + ParticipantHistory []string `json:"participant_history"` + ParticipantsEvents map[string]map[string]any `json:"participants_events,omitempty"` + ParticipantsCameraOn []string `json:"participants_camera_on"` + ParticipantsCameraOff []string `json:"participants_camera_off"` + ParticipantsScreenshareOn []string `json:"participants_screenshare_on"` + ParticipantsScreenshareOff []string `json:"participants_screenshare_off"` + CanvasThreadTS string `json:"canvas_thread_ts,omitempty"` + ThreadRootTS string `json:"thread_root_ts,omitempty"` + Channels []string `json:"channels"` + IsDMCall bool `json:"is_dm_call"` + WasRejected bool `json:"was_rejected"` + WasMissed bool `json:"was_missed"` + WasAccepted bool `json:"was_accepted"` + HasEnded bool `json:"has_ended"` + BackgroundID string `json:"background_id,omitempty"` + CanvasBackground string `json:"canvas_background,omitempty"` + IsPrewarmed bool `json:"is_prewarmed,omitempty"` + IsScheduled bool `json:"is_scheduled,omitempty"` + Recording *SHRoomRecording `json:"recording,omitempty"` + Locale string `json:"locale,omitempty"` + AttachedFileIDs []string `json:"attached_file_ids,omitempty"` + MediaBackendType string `json:"media_backend_type"` + DisplayID string `json:"display_id,omitempty"` + ExternalUniqueID string `json:"external_unique_id"` + AppID string `json:"app_id"` + CallFamily string `json:"call_family,omitempty"` + HuddleLink string `json:"huddle_link,omitempty"` +} + +// SHRoomHuddle holds the huddle-specific metadata on sh_room events. +type SHRoomHuddle struct { + ChannelID string `json:"channel_id"` +} + +// SHRoomJoinEvent is fired when a user joins a Slack Call/Huddle room. +type SHRoomJoinEvent struct { + Type string `json:"type"` + Room SHRoom `json:"room"` + User string `json:"user"` + Huddle *SHRoomHuddle `json:"huddle,omitempty"` + EventTS string `json:"event_ts"` + TS string `json:"ts"` +} + +// SHRoomLeaveEvent is fired when a user leaves a Slack Call/Huddle room. +type SHRoomLeaveEvent struct { + Type string `json:"type"` + Room SHRoom `json:"room"` + User string `json:"user"` + Huddle *SHRoomHuddle `json:"huddle,omitempty"` + EventTS string `json:"event_ts"` + TS string `json:"ts"` +} + +// SHRoomUpdateEvent is fired when a Slack Call/Huddle room is updated. +type SHRoomUpdateEvent struct { + Type string `json:"type"` + Room SHRoom `json:"room"` + User string `json:"user"` + Huddle *SHRoomHuddle `json:"huddle,omitempty"` + EventTS string `json:"event_ts"` + TS string `json:"ts"` +} + +// AppsUninstalledEvent represents the apps_uninstalled event sent via RTM +// when one or more apps are uninstalled from the workspace. +type AppsUninstalledEvent struct { + Type string `json:"type"` +} + +// ActivityEvent represents the activity event sent via RTM. This is an +// internal Slack event that fires during normal workspace usage (e.g. new +// messages, bundle updates). +type ActivityEvent struct { + Type string `json:"type"` + SubType string `json:"subtype"` + Key string `json:"key"` + Entry json.RawMessage `json:"entry"` + EventTimestamp string `json:"event_ts"` +} + +// BadgeCountsUpdatedEvent represents the badge_counts_updated event sent via +// RTM when notification badge counts change. +type BadgeCountsUpdatedEvent struct { + Type string `json:"type"` +} diff --git a/vendor/github.com/slack-go/slack/workflow_step.go b/vendor/github.com/slack-go/slack/workflow_step.go deleted file mode 100644 index 747a5dc00..000000000 --- a/vendor/github.com/slack-go/slack/workflow_step.go +++ /dev/null @@ -1,101 +0,0 @@ -package slack - -import ( - "context" - "encoding/json" -) - -const VTWorkflowStep ViewType = "workflow_step" - -type ( - ConfigurationModalRequest struct { - ModalViewRequest - } - - WorkflowStepCompleteResponse struct { - WorkflowStepEditID string `json:"workflow_step_edit_id"` - Inputs *WorkflowStepInputs `json:"inputs,omitempty"` - Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` - } - - WorkflowStepInputElement struct { - Value string `json:"value"` - SkipVariableReplacement bool `json:"skip_variable_replacement"` - } - - WorkflowStepInputs map[string]WorkflowStepInputElement - - WorkflowStepOutput struct { - Name string `json:"name"` - Type string `json:"type"` - Label string `json:"label"` - } -) - -func NewConfigurationModalRequest(blocks Blocks, privateMetaData string, externalID string) *ConfigurationModalRequest { - return &ConfigurationModalRequest{ - ModalViewRequest{ - Type: VTWorkflowStep, - Title: nil, // slack configuration modal must not have a title! - Blocks: blocks, - PrivateMetadata: privateMetaData, - ExternalID: externalID, - }, - } -} - -// SaveWorkflowStepConfiguration opens a configuration modal for a workflow step. -// For more information see the SaveWorkflowStepConfigurationContext documentation. -func (api *Client) SaveWorkflowStepConfiguration(workflowStepEditID string, inputs *WorkflowStepInputs, outputs *[]WorkflowStepOutput) error { - return api.SaveWorkflowStepConfigurationContext(context.Background(), workflowStepEditID, inputs, outputs) -} - -// SaveWorkflowStepConfigurationContext saves the configuration of a workflow step with a custom context. -// Slack API docs: https://api.slack.com/methods/workflows.updateStep -func (api *Client) SaveWorkflowStepConfigurationContext(ctx context.Context, workflowStepEditID string, inputs *WorkflowStepInputs, outputs *[]WorkflowStepOutput) error { - wscr := WorkflowStepCompleteResponse{ - WorkflowStepEditID: workflowStepEditID, - Inputs: inputs, - Outputs: outputs, - } - - endpoint := api.endpoint + "workflows.updateStep" - jsonData, err := json.Marshal(wscr) - 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 -} - -func GetInitialOptionFromWorkflowStepInput(selection *SelectBlockElement, inputs *WorkflowStepInputs, options []*OptionBlockObject) (*OptionBlockObject, bool) { - if len(*inputs) == 0 { - return &OptionBlockObject{}, false - } - if len(options) == 0 { - return &OptionBlockObject{}, false - } - - if val, ok := (*inputs)[selection.ActionID]; ok { - if val.SkipVariableReplacement { - return &OptionBlockObject{}, false - } - - for _, option := range options { - if option.Value == val.Value { - return option, true - } - } - } - - return &OptionBlockObject{}, false -} diff --git a/vendor/github.com/slack-go/slack/workflow_step_execute.go b/vendor/github.com/slack-go/slack/workflow_step_execute.go deleted file mode 100644 index 32db9ba0b..000000000 --- a/vendor/github.com/slack-go/slack/workflow_step_execute.go +++ /dev/null @@ -1,85 +0,0 @@ -package slack - -import ( - "context" - "encoding/json" -) - -type ( - WorkflowStepCompletedRequest struct { - WorkflowStepExecuteID string `json:"workflow_step_execute_id"` - Outputs map[string]string `json:"outputs"` - } - - WorkflowStepFailedRequest struct { - WorkflowStepExecuteID string `json:"workflow_step_execute_id"` - Error struct { - Message string `json:"message"` - } `json:"error"` - } -) - -type WorkflowStepCompletedRequestOption func(opt *WorkflowStepCompletedRequest) error - -func WorkflowStepCompletedRequestOptionOutput(outputs map[string]string) WorkflowStepCompletedRequestOption { - return func(opt *WorkflowStepCompletedRequest) error { - if len(outputs) > 0 { - opt.Outputs = outputs - } - return nil - } -} - -// WorkflowStepCompleted indicates step is completed -func (api *Client) WorkflowStepCompleted(workflowStepExecuteID string, options ...WorkflowStepCompletedRequestOption) error { - // More information: https://api.slack.com/methods/workflows.stepCompleted - r := &WorkflowStepCompletedRequest{ - WorkflowStepExecuteID: workflowStepExecuteID, - } - for _, option := range options { - option(r) - } - - endpoint := api.endpoint + "workflows.stepCompleted" - jsonData, err := json.Marshal(r) - if err != nil { - return err - } - - response := &SlackResponse{} - if err := postJSON(context.Background(), api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { - return err - } - - if !response.Ok { - return response.Err() - } - - return nil -} - -// WorkflowStepFailed indicates step is failed -func (api *Client) WorkflowStepFailed(workflowStepExecuteID string, errorMessage string) error { - // More information: https://api.slack.com/methods/workflows.stepFailed - r := WorkflowStepFailedRequest{ - WorkflowStepExecuteID: workflowStepExecuteID, - } - r.Error.Message = errorMessage - - endpoint := api.endpoint + "workflows.stepFailed" - jsonData, err := json.Marshal(r) - if err != nil { - return err - } - - response := &SlackResponse{} - if err := postJSON(context.Background(), api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { - return err - } - - if !response.Ok { - return response.Err() - } - - return nil -} diff --git a/vendor/github.com/slack-go/slack/workflows_featured.go b/vendor/github.com/slack-go/slack/workflows_featured.go new file mode 100644 index 000000000..08b339868 --- /dev/null +++ b/vendor/github.com/slack-go/slack/workflows_featured.go @@ -0,0 +1,143 @@ +package slack + +import ( + "context" + "encoding/json" + "fmt" +) + +type ( + FeaturedWorkflowTrigger struct { + ID string `json:"id"` + Title string `json:"title"` + } + + FeaturedWorkflow struct { + ChannelID string `json:"channel_id"` + Triggers []FeaturedWorkflowTrigger `json:"triggers"` + } + + WorkflowsFeaturedAddInput struct { + ChannelID string `json:"channel_id"` + TriggerIDs []string `json:"trigger_ids"` + } + + WorkflowsFeaturedListInput struct { + ChannelIDs []string `json:"channel_ids"` + } + + WorkflowsFeaturedListOutput struct { + FeaturedWorkflows []FeaturedWorkflow `json:"featured_workflows"` + } + + WorkflowsFeaturedRemoveInput struct { + ChannelID string `json:"channel_id"` + TriggerIDs []string `json:"trigger_ids"` + } + + WorkflowsFeaturedSetInput struct { + ChannelID string `json:"channel_id"` + TriggerIDs []string `json:"trigger_ids"` + } +) + +// WorkflowsFeaturedAdd adds featured workflows to a channel. +// +// Slack API Docs:https://api.slack.com/methods/workflows.featured.add +func (api *Client) WorkflowsFeaturedAdd(ctx context.Context, input *WorkflowsFeaturedAddInput) error { + response := struct { + SlackResponse + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal WorkflowsFeaturedAddInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.featured.add", api.token, jsonPayload, &response) + if err != nil { + return err + } + + if err := response.Err(); err != nil { + return err + } + + return nil +} + +// WorkflowsFeaturedList lists featured workflows for the given channels. +// +// Slack API Docs:https://api.slack.com/methods/workflows.featured.list +func (api *Client) WorkflowsFeaturedList(ctx context.Context, input *WorkflowsFeaturedListInput) (*WorkflowsFeaturedListOutput, error) { + response := struct { + SlackResponse + *WorkflowsFeaturedListOutput + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal WorkflowsFeaturedListInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.featured.list", api.token, jsonPayload, &response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response.WorkflowsFeaturedListOutput, nil +} + +// WorkflowsFeaturedRemove removes featured workflows from a channel. +// +// Slack API Docs:https://api.slack.com/methods/workflows.featured.remove +func (api *Client) WorkflowsFeaturedRemove(ctx context.Context, input *WorkflowsFeaturedRemoveInput) error { + response := struct { + SlackResponse + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal WorkflowsFeaturedRemoveInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.featured.remove", api.token, jsonPayload, &response) + if err != nil { + return err + } + + if err := response.Err(); err != nil { + return err + } + + return nil +} + +// WorkflowsFeaturedSet replaces all featured workflows in a channel with the given triggers. +// +// Slack API Docs:https://api.slack.com/methods/workflows.featured.set +func (api *Client) WorkflowsFeaturedSet(ctx context.Context, input *WorkflowsFeaturedSetInput) error { + response := struct { + SlackResponse + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal WorkflowsFeaturedSetInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.featured.set", api.token, jsonPayload, &response) + if err != nil { + return err + } + + if err := response.Err(); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/slack-go/slack/workflows_triggers.go b/vendor/github.com/slack-go/slack/workflows_triggers.go new file mode 100644 index 000000000..348348061 --- /dev/null +++ b/vendor/github.com/slack-go/slack/workflows_triggers.go @@ -0,0 +1,177 @@ +package slack + +import ( + "context" + "encoding/json" + "fmt" +) + +type ( + WorkflowsTriggersPermissionsAddInput struct { + TriggerId string `json:"trigger_id"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsAddOutput struct { + PermissionType string `json:"permission_type"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsListInput struct { + TriggerId string `json:"trigger_id"` + } + + WorkflowsTriggersPermissionsListOutput struct { + PermissionType string `json:"permission_type"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsRemoveInput struct { + TriggerId string `json:"trigger_id"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsRemoveOutput struct { + PermissionType string `json:"permission_type"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsSetInput struct { + PermissionType string `json:"permission_type"` + TriggerId string `json:"trigger_id"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsSetOutput struct { + PermissionType string `json:"permission_type"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } +) + +// WorkflowsTriggersPermissionsAdd allows users to run a trigger that has its permission +// type set to named_entities. +// +// Slack API Docs:https://api.slack.com/methods/workflows.triggers.permissions.add +func (api *Client) WorkflowsTriggersPermissionsAdd(ctx context.Context, input *WorkflowsTriggersPermissionsAddInput) (*WorkflowsTriggersPermissionsAddOutput, error) { + response := struct { + SlackResponse + *WorkflowsTriggersPermissionsAddOutput + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsAddInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.add", api.token, jsonPayload, &response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response.WorkflowsTriggersPermissionsAddOutput, nil +} + +// WorkflowsTriggersPermissionsList returns the permission type of a trigger and if +// applicable, includes the entities that have been granted access. +// +// Slack API Docs:https://api.slack.com/methods/workflows.triggers.permissions.list +func (api *Client) WorkflowsTriggersPermissionsList(ctx context.Context, input *WorkflowsTriggersPermissionsListInput) (*WorkflowsTriggersPermissionsListOutput, error) { + response := struct { + SlackResponse + *WorkflowsTriggersPermissionsListOutput + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsListInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.list", api.token, jsonPayload, &response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response.WorkflowsTriggersPermissionsListOutput, nil +} + +// WorkflowsTriggersPermissionsRemove revoke an entity's access to a trigger that has its +// permission type set to named_entities. +// +// Slack API Docs:https://api.slack.com/methods/workflows.triggers.permissions.remove +func (api *Client) WorkflowsTriggersPermissionsRemove(ctx context.Context, input *WorkflowsTriggersPermissionsRemoveInput) (*WorkflowsTriggersPermissionsRemoveOutput, error) { + response := struct { + SlackResponse + *WorkflowsTriggersPermissionsRemoveOutput + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsRemoveInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.remove", api.token, jsonPayload, &response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response.WorkflowsTriggersPermissionsRemoveOutput, nil +} + +// WorkflowsTriggersPermissionsSet sets the permission type for who can run a trigger. +// +// Slack API Docs:https://api.slack.com/methods/workflows.triggers.permissions.set +func (api *Client) WorkflowsTriggersPermissionsSet(ctx context.Context, input *WorkflowsTriggersPermissionsSetInput) (*WorkflowsTriggersPermissionsSetOutput, error) { + response := struct { + SlackResponse + *WorkflowsTriggersPermissionsSetOutput + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsSetInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.set", api.token, jsonPayload, &response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response.WorkflowsTriggersPermissionsSetOutput, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 78c747566..0f571cb45 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1558,8 +1558,8 @@ github.com/sirupsen/logrus # github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 ## explicit github.com/skratchdot/open-golang/open -# github.com/slack-go/slack v0.15.0 -## explicit; go 1.16 +# github.com/slack-go/slack v0.23.1 +## explicit; go 1.25 github.com/slack-go/slack github.com/slack-go/slack/internal/backoff github.com/slack-go/slack/internal/errorsx From 420a3f43dfa51b5e64ba213403299c3467ce2f79 Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Thu, 30 Jul 2026 13:40:26 +0200 Subject: [PATCH 02/11] =?UTF-8?q?Fix=20GO-2026-5970:=20upgrade=20golang.or?= =?UTF-8?q?g/x/text=20v0.38.0=20=E2=86=92=20v0.39.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 8 +- go.sum | 16 +- vendor/golang.org/x/text/cases/context.go | 2 +- vendor/golang.org/x/text/cases/map.go | 4 +- .../x/text/unicode/norm/forminfo.go | 9 +- vendor/golang.org/x/text/unicode/norm/iter.go | 8 +- .../x/text/unicode/norm/normalize.go | 20 +- .../x/tools/go/packages/packages.go | 14 + .../x/tools/internal/gcimporter/iexport.go | 3 + .../x/tools/internal/gcimporter/iimport.go | 26 +- .../x/tools/internal/imports/fix.go | 1 - .../x/tools/internal/imports/imports.go | 4 + .../x/tools/internal/stdlib/deps.go | 654 +++++++++--------- .../x/tools/internal/stdlib/manifest.go | 287 +++++++- .../x/tools/internal/typesinternal/element.go | 8 +- .../x/tools/internal/typesinternal/types.go | 28 + .../tools/internal/typesinternal/zerovalue.go | 16 +- vendor/modules.txt | 8 +- 18 files changed, 741 insertions(+), 375 deletions(-) diff --git a/go.mod b/go.mod index 879076c69..70aeb8b19 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/spf13/afero v1.15.0 github.com/spf13/pflag v1.0.10 go.podman.io/image/v5 v5.40.0 - golang.org/x/mod v0.36.0 + golang.org/x/mod v0.37.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.34.2 k8s.io/apimachinery v0.34.2 @@ -143,7 +143,7 @@ require ( go.uber.org/automaxprocs v1.6.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect + golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect k8s.io/apiextensions-apiserver v0.34.1 // indirect sigs.k8s.io/cluster-api v1.11.2 // indirect @@ -290,7 +290,7 @@ require ( go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/mock v0.6.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/tools v0.45.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260618152121-87f3d3e198d3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect gopkg.in/warnings.v0 v0.1.2 // indirect @@ -379,7 +379,7 @@ require ( golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/text v0.39.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum index cfc9b8ddc..c1c23d1fe 100644 --- a/go.sum +++ b/go.sum @@ -1293,8 +1293,8 @@ golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1494,8 +1494,8 @@ golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= -golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE= -golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1547,8 +1547,8 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1631,8 +1631,8 @@ golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/vendor/golang.org/x/text/cases/context.go b/vendor/golang.org/x/text/cases/context.go index e9aa9e193..a28f45d7b 100644 --- a/vendor/golang.org/x/text/cases/context.go +++ b/vendor/golang.org/x/text/cases/context.go @@ -249,7 +249,7 @@ func upper(c *context) bool { return c.copy() } -// isUpper writes the isUppercase version of the current rune to dst. +// isUpper reports whether the current rune is in upper case. func isUpper(c *context) bool { ct := c.caseType() if c.info&hasMappingMask == 0 || ct == cUpper { diff --git a/vendor/golang.org/x/text/cases/map.go b/vendor/golang.org/x/text/cases/map.go index 0f7c6a14b..51a683092 100644 --- a/vendor/golang.org/x/text/cases/map.go +++ b/vendor/golang.org/x/text/cases/map.go @@ -774,7 +774,7 @@ func nlTitle(c *context) bool { // From CLDR: // # Special titlecasing for Dutch initial "ij". // ::Any-Title(); - // # Fix up Ij at the beginning of a "word" (per Any-Title, notUAX #29) + // # Fix up Ij at the beginning of a "word" (per Any-Title, not UAX #29) // [:^WB=ALetter:] [:WB=Extend:]* [[:WB=MidLetter:][:WB=MidNumLet:]]? { Ij } → IJ ; if c.src[c.pSrc] != 'I' && c.src[c.pSrc] != 'i' { return title(c) @@ -794,7 +794,7 @@ func nlTitleSpan(c *context) bool { // From CLDR: // # Special titlecasing for Dutch initial "ij". // ::Any-Title(); - // # Fix up Ij at the beginning of a "word" (per Any-Title, notUAX #29) + // # Fix up Ij at the beginning of a "word" (per Any-Title, not UAX #29) // [:^WB=ALetter:] [:WB=Extend:]* [[:WB=MidLetter:][:WB=MidNumLet:]]? { Ij } → IJ ; if c.src[c.pSrc] != 'I' { return isTitle(c) diff --git a/vendor/golang.org/x/text/unicode/norm/forminfo.go b/vendor/golang.org/x/text/unicode/norm/forminfo.go index f3a234e5f..b3cf5d9bd 100644 --- a/vendor/golang.org/x/text/unicode/norm/forminfo.go +++ b/vendor/golang.org/x/text/unicode/norm/forminfo.go @@ -121,8 +121,12 @@ func (p Properties) BoundaryAfter() bool { // // When all 6 bits are zero, the character is inert, meaning it is never // influenced by normalization. +// +// We set flags to 0x80 (high bit 7 unused in quick check data) to indicate an invalid rune. type qcInfo uint8 +func (p Properties) isInvalid() bool { return p.flags == 0x80 } + func (p Properties) isYesC() bool { return p.flags&0x10 == 0 } func (p Properties) isYesD() bool { return p.flags&0x4 == 0 } @@ -247,6 +251,9 @@ func (f Form) PropertiesString(s string) Properties { // to a Properties. See the comment at the top of the file // for more information on the format. func compInfo(v uint16, sz int) Properties { + if sz == 0 { + return Properties{flags: 0x80, size: 1} + } if v == 0 { return Properties{size: uint8(sz)} } else if v >= 0x8000 { @@ -254,7 +261,7 @@ func compInfo(v uint16, sz int) Properties { size: uint8(sz), ccc: uint8(v), tccc: uint8(v), - flags: qcInfo(v >> 8), + flags: qcInfo(v>>8) & 0x3f, } if p.ccc > 0 || p.combinesBackward() { p.nLead = uint8(p.flags & 0x3) diff --git a/vendor/golang.org/x/text/unicode/norm/iter.go b/vendor/golang.org/x/text/unicode/norm/iter.go index 417c6b268..3cc059224 100644 --- a/vendor/golang.org/x/text/unicode/norm/iter.go +++ b/vendor/golang.org/x/text/unicode/norm/iter.go @@ -376,16 +376,12 @@ func nextComposed(i *Iter) []byte { goto doNorm } prevCC = i.info.tccc - sz := int(i.info.size) - if sz == 0 { - sz = 1 // illegal rune: copy byte-by-byte - } - p := outp + sz + p := outp + int(i.info.size) if p > len(i.buf) { break } outp = p - i.p += sz + i.p += int(i.info.size) if i.p >= i.rb.nsrc { i.setDone() break diff --git a/vendor/golang.org/x/text/unicode/norm/normalize.go b/vendor/golang.org/x/text/unicode/norm/normalize.go index 4747ad07a..60b1511ca 100644 --- a/vendor/golang.org/x/text/unicode/norm/normalize.go +++ b/vendor/golang.org/x/text/unicode/norm/normalize.go @@ -148,7 +148,7 @@ func (f Form) IsNormalString(s string) bool { // patched buffer and whether the decomposition is still in progress. func patchTail(rb *reorderBuffer) bool { info, p := lastRuneStart(&rb.f, rb.out) - if p == -1 || info.size == 0 { + if p == -1 || info.isInvalid() { return true } end := p + int(info.size) @@ -225,7 +225,7 @@ func doAppend(rb *reorderBuffer, out []byte, p int) []byte { } fd := &rb.f if doMerge { - var info Properties + info := Properties{flags: 0x80, size: 1} // invalid rune if p < n { info = fd.info(src, p) if !info.BoundaryBefore() || info.nLeadingNonStarters() > 0 { @@ -235,7 +235,7 @@ func doAppend(rb *reorderBuffer, out []byte, p int) []byte { p = decomposeSegment(rb, p, true) } } - if info.size == 0 { + if info.isInvalid() { rb.doFlush() // Append incomplete UTF-8 encoding. return src.appendSlice(rb.out, p, n) @@ -314,7 +314,7 @@ func (f *formInfo) quickSpan(src input, i, end int, atEOF bool) (n int, ok bool) continue } info := f.info(src, i) - if info.size == 0 { + if info.isInvalid() { if atEOF { // include incomplete runes return n, true @@ -379,7 +379,7 @@ func (f Form) firstBoundary(src input, nsrc int) int { // CGJ insertion points correctly. Luckily it doesn't have to. for { info := fd.info(src, i) - if info.size == 0 { + if info.isInvalid() { return -1 } if s := ss.next(info); s != ssSuccess { @@ -424,7 +424,7 @@ func (f Form) nextBoundary(src input, nsrc int, atEOF bool) int { } fd := formTable[f] info := fd.info(src, 0) - if info.size == 0 { + if info.isInvalid() { if atEOF { return 1 } @@ -435,7 +435,7 @@ func (f Form) nextBoundary(src input, nsrc int, atEOF bool) int { for i := int(info.size); i < nsrc; i += int(info.size) { info = fd.info(src, i) - if info.size == 0 { + if info.isInvalid() { if atEOF { return i } @@ -465,7 +465,7 @@ func lastBoundary(fd *formInfo, b []byte) int { if p == -1 { return -1 } - if info.size == 0 { // ends with incomplete rune + if info.isInvalid() { // ends with incomplete rune if p == 0 { // starts with incomplete rune return -1 } @@ -504,7 +504,7 @@ func lastBoundary(fd *formInfo, b []byte) int { func decomposeSegment(rb *reorderBuffer, sp int, atEOF bool) int { // Force one character to be consumed. info := rb.f.info(rb.src, sp) - if info.size == 0 { + if info.isInvalid() { return 0 } if s := rb.ss.next(info); s == ssStarter { @@ -528,7 +528,7 @@ func decomposeSegment(rb *reorderBuffer, sp int, atEOF bool) int { break } info = rb.f.info(rb.src, sp) - if info.size == 0 { + if info.isInvalid() { if !atEOF { return int(iShortSrc) } diff --git a/vendor/golang.org/x/tools/go/packages/packages.go b/vendor/golang.org/x/tools/go/packages/packages.go index de683684a..1e5549abe 100644 --- a/vendor/golang.org/x/tools/go/packages/packages.go +++ b/vendor/golang.org/x/tools/go/packages/packages.go @@ -815,6 +815,12 @@ func (ld *loader) refine(response *DriverResponse) ([]*Package, error) { needsrc: needsrc, goVersion: response.GoVersion, } + // Don't trust the driver to respond with duplicate-free + // package names (go.dev/issue/63822). + if _, ok := ld.pkgs[lpkg.ID]; ok { + return nil, fmt.Errorf("%s response contained duplicate packages for ID %q", + cond(ld.externalDriver, "go/packages driver", "go list"), lpkg.ID) + } ld.pkgs[lpkg.ID] = lpkg if rootIndex >= 0 { initial[rootIndex] = lpkg @@ -1589,3 +1595,11 @@ func usesExportData(cfg *Config) bool { } type unit struct{} + +func cond[T any](cond bool, t, f T) T { + if cond { + return t + } else { + return f + } +} diff --git a/vendor/golang.org/x/tools/internal/gcimporter/iexport.go b/vendor/golang.org/x/tools/internal/gcimporter/iexport.go index 4c9450f4e..686f171d3 100644 --- a/vendor/golang.org/x/tools/internal/gcimporter/iexport.go +++ b/vendor/golang.org/x/tools/internal/gcimporter/iexport.go @@ -823,6 +823,9 @@ func (p *iexporter) doDecl(obj types.Object) { w.pos(m.Pos()) w.string(m.Name()) sig, _ := m.Type().(*types.Signature) + if w.p.version >= iexportVersionGenericMethods && w.bool(sig.TypeParams().Len() > 0) { + w.tparamList(obj.Name()+"."+m.Name(), sig.TypeParams(), obj.Pkg()) + } // Receiver type parameters are type arguments of the receiver type, so // their name must be qualified before exporting recv. diff --git a/vendor/golang.org/x/tools/internal/gcimporter/iimport.go b/vendor/golang.org/x/tools/internal/gcimporter/iimport.go index 1ee4e9354..7b1723e0f 100644 --- a/vendor/golang.org/x/tools/internal/gcimporter/iimport.go +++ b/vendor/golang.org/x/tools/internal/gcimporter/iimport.go @@ -48,13 +48,14 @@ func (r *intReader) uint64() uint64 { // Keep this in sync with constants in iexport.go. const ( - iexportVersionGo1_11 = 0 - iexportVersionPosCol = 1 - iexportVersionGo1_18 = 2 - iexportVersionGenerics = 2 - iexportVersion = iexportVersionGenerics - - iexportVersionCurrent = 2 + iexportVersionGo1_11 = 0 + iexportVersionPosCol = 1 + iexportVersionGo1_18 = 2 + iexportVersionGenerics = 2 + iexportVersionGenericMethods = 3 + iexportVersion = iexportVersionGenericMethods + + iexportVersionCurrent = 3 ) type ident struct { @@ -179,9 +180,9 @@ func iimportCommon(fset *token.FileSet, getPackages GetPackagesFunc, data []byte version = int64(r.uint64()) switch version { - case iexportVersionGo1_18, iexportVersionPosCol, iexportVersionGo1_11: + case iexportVersionGenericMethods, iexportVersionGo1_18, iexportVersionPosCol, iexportVersionGo1_11: default: - if version > iexportVersionGo1_18 { + if version > iexportVersionGenericMethods { errorf("unstable iexport format version %d, just rebuild compiler and std library", version) } else { errorf("unknown iexport format version %d", version) @@ -614,6 +615,10 @@ func (r *importReader) obj(pkg *types.Package, name string) { for n := r.uint64(); n > 0; n-- { mpos := r.pos() mname := r.ident() + var tpars []*types.TypeParam + if r.p.version >= iexportVersionGenericMethods && r.bool() { + tpars = r.tparamList() + } recv := r.param(pkg) // If the receiver has any targs, set those as the @@ -628,8 +633,7 @@ func (r *importReader) obj(pkg *types.Package, name string) { rparams[i] = types.Unalias(targs.At(i)).(*types.TypeParam) } } - msig := r.signature(pkg, recv, rparams, nil) - + msig := r.signature(pkg, recv, rparams, tpars) named.AddMethod(types.NewFunc(mpos, pkg, mname, msig)) } } diff --git a/vendor/golang.org/x/tools/internal/imports/fix.go b/vendor/golang.org/x/tools/internal/imports/fix.go index d29ecfb01..b99ea6c91 100644 --- a/vendor/golang.org/x/tools/internal/imports/fix.go +++ b/vendor/golang.org/x/tools/internal/imports/fix.go @@ -273,7 +273,6 @@ func (p *pass) loadPackageNames(ctx context.Context, imports []*ImportInfo) erro } unknown = append(unknown, imp.ImportPath) } - names, err := p.source.LoadPackageNames(ctx, p.srcDir, unknown) if err != nil { return err diff --git a/vendor/golang.org/x/tools/internal/imports/imports.go b/vendor/golang.org/x/tools/internal/imports/imports.go index b5f5218b5..072293835 100644 --- a/vendor/golang.org/x/tools/internal/imports/imports.go +++ b/vendor/golang.org/x/tools/internal/imports/imports.go @@ -74,6 +74,10 @@ func Process(filename string, src []byte, opt *Options) (formatted []byte, err e // Note that filename's directory influences which imports can be chosen, // so it is important that filename be accurate. func FixImports(ctx context.Context, filename string, src []byte, goroot string, logf func(string, ...any), source Source) (fixes []*ImportFix, err error) { + if source == nil { + // In case someone adds a defective call from a new place + panic("source is nil") + } ctx, done := event.Start(ctx, "imports.FixImports") defer done() diff --git a/vendor/golang.org/x/tools/internal/stdlib/deps.go b/vendor/golang.org/x/tools/internal/stdlib/deps.go index dacfc1dff..0f73c719c 100644 --- a/vendor/golang.org/x/tools/internal/stdlib/deps.go +++ b/vendor/golang.org/x/tools/internal/stdlib/deps.go @@ -12,366 +12,386 @@ type pkginfo struct { } var deps = [...]pkginfo{ - {"archive/tar", "\x03q\x03F=\x01\n\x01$\x01\x01\x02\x05\b\x02\x01\x02\x02\r"}, - {"archive/zip", "\x02\x04g\a\x03\x13\x021=\x01+\x05\x01\x0f\x03\x02\x0f\x04"}, - {"bufio", "\x03q\x86\x01D\x15"}, - {"bytes", "t+[\x03\fH\x02\x02"}, + {"archive/tar", "\x03{\x03F>\x01\n\x01&\x01\x01\x02\x05\b\x02\x01\x02\x02\r"}, + {"archive/zip", "\x02\x04j\x0e\x03\x12\x022>\x01-\x05\x01\x0f\x03\x02\x0f\x04"}, + {"bufio", "\x03{\x87\x01F\x15"}, + {"bytes", "~*]\x03\fJ\x02\x02"}, {"cmp", ""}, - {"compress/bzip2", "\x02\x02\xf6\x01A"}, - {"compress/flate", "\x02r\x03\x83\x01\f\x033\x01\x03"}, - {"compress/gzip", "\x02\x04g\a\x03\x15nU"}, - {"compress/lzw", "\x02r\x03\x83\x01"}, - {"compress/zlib", "\x02\x04g\a\x03\x13\x01o"}, - {"container/heap", "\xbc\x02"}, + {"compress/bzip2", "\x02\x02\x81\x02C"}, + {"compress/flate", "\x02|\x03\x84\x01\f\x034\x02\x03"}, + {"compress/gzip", "\x02\x04j\x0e\x03\x14pW"}, + {"compress/lzw", "\x02|\x03\x84\x01"}, + {"compress/zlib", "\x02\x04j\x0e\x03\x12\x01q"}, + {"container/heap", "\xc9\x02"}, {"container/list", ""}, {"container/ring", ""}, - {"context", "t\\p\x01\x0e"}, - {"crypto", "\x8a\x01pC"}, - {"crypto/aes", "\x10\v\t\x99\x02"}, - {"crypto/cipher", "\x03!\x01\x01 \x12\x1c,Z"}, - {"crypto/des", "\x10\x16 .,\x9d\x01\x03"}, - {"crypto/dsa", "F\x03+\x86\x01\r"}, - {"crypto/ecdh", "\x03\v\r\x10\x04\x17\x03\x0f\x1c\x86\x01"}, - {"crypto/ecdsa", "\x0e\x05\x03\x05\x01\x10\b\v\x06\x01\x03\x0e\x01\x1c\x86\x01\r\x05L\x01"}, - {"crypto/ed25519", "\x0e\x1f\x12\a\x03\b\a\x1cI=C"}, - {"crypto/elliptic", "4@\x86\x01\r9"}, - {"crypto/fips140", "#\x05\x95\x01\x98\x01"}, - {"crypto/hkdf", "0\x15\x01.\x16"}, - {"crypto/hmac", "\x1b\x16\x14\x01\x122"}, - {"crypto/hpke", "\x03\v\x02\x03\x04\x01\f\x01\x05\x1f\x05\a\x01\x01\x1d\x03\x13\x16\x9b\x01\x1c"}, - {"crypto/internal/boring", "\x0e\x02\x0el"}, - {"crypto/internal/boring/bbig", "\x1b\xec\x01N"}, - {"crypto/internal/boring/bcache", "\xc1\x02\x14"}, + {"context", "~]r\x01\x0e"}, + {"crypto", "\x93\x01rE"}, + {"crypto/aes", "\x10\v\n\xa5\x02"}, + {"crypto/cipher", "\x03\"\x01\x01 \x13$+\\"}, + {"crypto/des", "\x10\x17 7+\xa1\x01\x03"}, + {"crypto/dsa", "G\x034\x87\x01\r"}, + {"crypto/ecdh", "\x03\v\r\x11\x04\x17\x03\x10$\x87\x01"}, + {"crypto/ecdsa", "\x0e\x05\x03\x05\x01\x11\b\v\x06\x01\x03\x0f\x01$\x87\x01\r\x05O\x01"}, + {"crypto/ed25519", "\x0e \x12\a\x03\t\a$I>E"}, + {"crypto/elliptic", "5I\x87\x01\r;"}, + {"crypto/fips140", "$\x05\x9e\x01\x9b\x01"}, + {"crypto/hkdf", "1\x15\x017\x15"}, + {"crypto/hmac", "\x1b\x17\x14\x01\x139"}, + {"crypto/hpke", "\x03\v\x02\x03\x04\x01\r\x01\x05\x1f\x06\a\x01\x01%\x03\x12\x16\x9f\x01\x1d"}, + {"crypto/internal/boring", "\x0e\x02\x0eu"}, + {"crypto/internal/boring/bbig", "\x1b\xf7\x01P"}, + {"crypto/internal/boring/bcache", "\xce\x02\x14"}, {"crypto/internal/boring/sig", ""}, {"crypto/internal/constanttime", ""}, - {"crypto/internal/cryptotest", "\x03\r\v\b%\x10\x19\x06\x13\x12 \x04\x06\t\x19\x01\x11\x11\x1b\x01\a\x05\b\x03\x05\f"}, - {"crypto/internal/entropy", "K"}, - {"crypto/internal/entropy/v1.0.0", "D0\x95\x018\x14"}, - {"crypto/internal/fips140", "C1\xbf\x01\v\x17"}, - {"crypto/internal/fips140/aes", "\x03 \x03\x02\x14\x05\x01\x01\x05,\x95\x014"}, - {"crypto/internal/fips140/aes/gcm", "#\x01\x02\x02\x02\x12\x05\x01\x06,\x92\x01"}, - {"crypto/internal/fips140/alias", "\xd5\x02"}, - {"crypto/internal/fips140/bigmod", "(\x19\x01\x06,\x95\x01"}, - {"crypto/internal/fips140/check", "#\x0e\a\t\x02\xb7\x01["}, - {"crypto/internal/fips140/check/checktest", "(\x8b\x02\""}, - {"crypto/internal/fips140/drbg", "\x03\x1f\x01\x01\x04\x14\x05\n)\x86\x01\x0f7\x01"}, - {"crypto/internal/fips140/ecdh", "\x03 \x05\x02\n\r3\x86\x01\x0f7"}, - {"crypto/internal/fips140/ecdsa", "\x03 \x04\x01\x02\a\x03\x06:\x16pF"}, - {"crypto/internal/fips140/ed25519", "\x03 \x05\x02\x04\f:\xc9\x01\x03"}, - {"crypto/internal/fips140/edwards25519", "\x1f\t\a\x123\x95\x017"}, - {"crypto/internal/fips140/edwards25519/field", "(\x14\x053\x95\x01"}, - {"crypto/internal/fips140/hkdf", "\x03 \x05\t\a<\x16"}, - {"crypto/internal/fips140/hmac", "\x03 \x15\x01\x01:\x16"}, - {"crypto/internal/fips140/mldsa", "\x03\x1c\x04\x05\x02\x0e\x01\x03\x053\x95\x017"}, - {"crypto/internal/fips140/mlkem", "\x03 \x05\x02\x0f\x03\x053\xcc\x01"}, - {"crypto/internal/fips140/nistec", "\x1f\t\r\f3\x95\x01*\r\x15"}, - {"crypto/internal/fips140/nistec/fiat", "(\x148\x95\x01"}, - {"crypto/internal/fips140/pbkdf2", "\x03 \x05\t\a<\x16"}, - {"crypto/internal/fips140/rsa", "\x03\x1c\x04\x04\x01\x02\x0e\x01\x01\x028\x16pF"}, - {"crypto/internal/fips140/sha256", "\x03 \x1e\x01\x06,\x16\x7f"}, - {"crypto/internal/fips140/sha3", "\x03 \x19\x05\x012\x95\x01L"}, - {"crypto/internal/fips140/sha512", "\x03 \x1e\x01\x06,\x16\x7f"}, - {"crypto/internal/fips140/ssh", "(b"}, - {"crypto/internal/fips140/subtle", "\x1f\a\x1b\xc8\x01"}, - {"crypto/internal/fips140/tls12", "\x03 \x05\t\a\x02:\x16"}, - {"crypto/internal/fips140/tls13", "\x03 \x05\b\b\t3\x16"}, - {"crypto/internal/fips140cache", "\xb3\x02\r'"}, + {"crypto/internal/cryptotest", "\x03\r\v\t%\x11\x1a\r\x12\x12!\x04\x06\n\x19\x01\x11\x11\x1d\x01\a\x03\x02\b\x02\x01\x05\f"}, + {"crypto/internal/cryptotest/wycheproof", "\x0e\x12S\x01\f\x01r@\x05\x03\x15\x10"}, + {"crypto/internal/entropy", "L"}, + {"crypto/internal/entropy/v1.0.0", "E9\x96\x01:\x14"}, + {"crypto/internal/fips140", "D:\xc2\x01\v\x17"}, + {"crypto/internal/fips140/aes", "\x03!\x03\x02\x14\x05\x01\x01\x055\x96\x016"}, + {"crypto/internal/fips140/aes/gcm", "$\x01\x02\x02\x02\x12\x05\x01\x065\x93\x01"}, + {"crypto/internal/fips140/alias", "\xe2\x02"}, + {"crypto/internal/fips140/bigmod", ")\x19\x01\x065\x96\x01"}, + {"crypto/internal/fips140/check", "$\x0e\a\t\x02\xc1\x01]"}, + {"crypto/internal/fips140/check/checktest", ")\x97\x02\""}, + {"crypto/internal/fips140/drbg", "\x03 \x01\x01\x04\x14\x05\n2\x87\x01\x0f9\x01"}, + {"crypto/internal/fips140/ecdh", "\x03!\x05\x02\n\r<\x87\x01\x0f9"}, + {"crypto/internal/fips140/ecdsa", "\x03!\x04\x01\x02\a\x03\x06C\x15r\x0f9"}, + {"crypto/internal/fips140/ed25519", "\x03!\x05\x02\x04\fC\xcc\x01\x03"}, + {"crypto/internal/fips140/edwards25519", "\x1f\n\a\x12<\x96\x019"}, + {"crypto/internal/fips140/edwards25519/field", ")\x14\x05<\x96\x01"}, + {"crypto/internal/fips140/hkdf", "\x03!\x05\t\aE\x15"}, + {"crypto/internal/fips140/hmac", "\x03!\x15\x01\x01C\x15"}, + {"crypto/internal/fips140/mldsa", "\x03\x1c\x05\x05\x02\x0e\x01\x03\x05<\x96\x019"}, + {"crypto/internal/fips140/mlkem", "\x03!\x05\x02\x0f\x03\x05<\xcf\x01"}, + {"crypto/internal/fips140/nistec", "\x1f\n\r\f<\x96\x01,\r\x15"}, + {"crypto/internal/fips140/nistec/fiat", ")\x14A\x96\x01"}, + {"crypto/internal/fips140/pbkdf2", "\x03!\x05\t\aE\x15"}, + {"crypto/internal/fips140/rsa", "\x03\x1c\x05\x04\x01\x02\x0e\x01\x01\x02A\x15rH"}, + {"crypto/internal/fips140/sha256", "\x03!\x1e\x01\x065\x15\x81\x01"}, + {"crypto/internal/fips140/sha3", "\x03!\x19\x05\x01;\x96\x01N"}, + {"crypto/internal/fips140/sha512", "\x03!\x1e\x01\x065\x15\x81\x01"}, + {"crypto/internal/fips140/ssh", ")j"}, + {"crypto/internal/fips140/subtle", "\x1f\b\x1b\xd2\x01"}, + {"crypto/internal/fips140/tls12", "\x03!\x05\t\a\x02C\x15"}, + {"crypto/internal/fips140/tls13", "\x03!\x05\b\b\t<\x15"}, + {"crypto/internal/fips140cache", "\xc0\x02\r."}, {"crypto/internal/fips140deps", ""}, - {"crypto/internal/fips140deps/byteorder", "\xa0\x01"}, - {"crypto/internal/fips140deps/cpu", "\xb5\x01\a"}, - {"crypto/internal/fips140deps/godebug", "\xbd\x01"}, - {"crypto/internal/fips140deps/time", "\xcf\x02"}, - {"crypto/internal/fips140hash", "9\x1d4\xcb\x01"}, - {"crypto/internal/fips140only", "\x17\x13\x0e\x01\x01Pp"}, + {"crypto/internal/fips140deps/byteorder", "\xa9\x01"}, + {"crypto/internal/fips140deps/cpu", "\xbe\x01\b"}, + {"crypto/internal/fips140deps/godebug", "\xc7\x01"}, + {"crypto/internal/fips140deps/time", "\xe2\x02"}, + {"crypto/internal/fips140hash", ":\x1e;\xcf\x01"}, + {"crypto/internal/fips140only", "\x17\x14\x0e\x01\x01Xr"}, {"crypto/internal/fips140test", ""}, - {"crypto/internal/impl", "\xbe\x02"}, - {"crypto/internal/rand", "\x1b\x0f s=["}, - {"crypto/internal/randutil", "\xfa\x01\x12"}, - {"crypto/internal/sysrand", "tq! \r\r\x01\x01\r\x06"}, - {"crypto/internal/sysrand/internal/seccomp", "t"}, - {"crypto/md5", "\x0e8.\x16\x16i"}, - {"crypto/mlkem", "\x0e%"}, - {"crypto/mlkem/mlkemtest", "3\x13\b&"}, - {"crypto/pbkdf2", "6\x0f\x01.\x16"}, - {"crypto/rand", "\x1b\x0f\x1c\x03+\x86\x01\rN"}, - {"crypto/rc4", "& .\xc9\x01"}, - {"crypto/rsa", "\x0e\r\x01\v\x10\x0e\x01\x03\b\a\x1c\x03\x133=\f\x01"}, - {"crypto/sha1", "\x0e\r+\x02,\x16\x16\x15T"}, - {"crypto/sha256", "\x0e\r\x1dR"}, - {"crypto/sha3", "\x0e+Q\xcb\x01"}, - {"crypto/sha512", "\x0e\r\x1fP"}, - {"crypto/subtle", "\x1f\x1d\x9f\x01z"}, - {"crypto/tls", "\x03\b\x02\x01\x01\x01\x01\x02\x01\x01\x01\x02\x01\x01\x01\t\x01\x18\x01\x0f\x01\x03\x01\x01\x01\x01\x02\x01\x02\x01\x17\x02\x03\x13\x16\x15\b=\x16\x16\r\b\x01\x01\x01\x02\x01\x0e\x06\x02\x01\x0f"}, - {"crypto/tls/internal/fips140tls", "\x17\xaa\x02"}, - {"crypto/x509", "\x03\v\x01\x01\x01\x01\x01\x01\x01\x017\x06\x01\x01\x02\x05\x0e\x06\x02\x02\x03F\x03:\x01\x02\b\x01\x01\x02\a\x10\x05\x01\x06\a\b\x02\x01\x02\x0f\x02\x01\x01\x02\x03\x01"}, - {"crypto/x509/pkix", "j\x06\a\x90\x01H"}, - {"database/sql", "\x03\nQ\x16\x03\x83\x01\v\a\"\x05\b\x02\x03\x01\x0e\x02\x02\x02"}, - {"database/sql/driver", "\rg\x03\xb7\x01\x0f\x12"}, - {"debug/buildinfo", "\x03^\x02\x01\x01\b\a\x03g\x1a\x02\x01+\x0f "}, - {"debug/dwarf", "\x03j\a\x03\x83\x011\x11\x01\x01"}, - {"debug/elf", "\x03\x06W\r\a\x03g\x1b\x01\f \x17\x01\x17"}, - {"debug/gosym", "\x03j\n$\xa1\x01\x01\x01\x02"}, - {"debug/macho", "\x03\x06W\r\ng\x1c,\x17\x01"}, - {"debug/pe", "\x03\x06W\r\a\x03g\x1c,\x17\x01\x17"}, - {"debug/plan9obj", "m\a\x03g\x1c,"}, - {"embed", "t+B\x19\x01T"}, + {"crypto/internal/impl", "\xcb\x02"}, + {"crypto/internal/rand", "\x1b\x10 |>]"}, + {"crypto/internal/randutil", "\x85\x02\x12"}, + {"crypto/internal/sysrand", "~r!\"\r\r\x01\x01\r\x06"}, + {"crypto/internal/sysrand/internal/seccomp", "~"}, + {"crypto/md5", "\x0e97\x15\x16k"}, + {"crypto/mldsa", "\x0e%K\x87\x01"}, + {"crypto/mlkem", "\x0e&"}, + {"crypto/mlkem/mlkemtest", "4\x13\t."}, + {"crypto/pbkdf2", "7\x0f\x017\x15"}, + {"crypto/rand", "\x1b\x10\x1c\x034\x87\x01\rP"}, + {"crypto/rc4", "' 7\xcc\x01"}, + {"crypto/rsa", "\x0e\r\x01\f\x10\x0e\x01\x03\t\a$\x03\x124>\f\x01"}, + {"crypto/sha1", "\x0e\r,\x025\x15\x16\x15V"}, + {"crypto/sha256", "\x0e\r\x1eZ"}, + {"crypto/sha3", "\x0e,Y\xcf\x01"}, + {"crypto/sha512", "\x0e\r X"}, + {"crypto/subtle", "\x1f\x1e\xa9\x01|"}, + {"crypto/tls", "\x03\b\x02\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\n\x01\x18\x01\x0f\x01\x01\x03\x01\x01\x01\x01\x02\x01\x02\x01\x1f\x02\x03\x12\x16\x15\t>\x16\x18\r\b\x01\x01\x01\x02\x01\x0e\x06\x03\x01\x15"}, + {"crypto/tls/internal/fips140tls", "\x17\xb7\x02"}, + {"crypto/x509", "\x03\v\x01\x01\x01\x01\x01\x01\x01\x01\x017\x01\x06\x01\x01\x02\x05\x0f\x06\t\x02\x03F\x03;\x01\x02\b\x01\x01\x02\a\x12\x05\x01\x06\a\b\x02\x01\x02\x0f\x02\x01\x01\x02\x04\x01"}, + {"crypto/x509/pkix", "m\x06\x0e\x91\x019\x11"}, + {"database/sql", "\x03\nS\x01\x1d\x03\x84\x01\v\a$\x05\b\x02\x03\x01\x0e\x02\x02\x02\x01"}, + {"database/sql/driver", "\rT\x1d\x03\xba\x01\x0f\x12\a"}, + {"database/sql/internal", ""}, + {"debug/buildinfo", "\x03a\x02\x01\x01\b\x0e\x03h\x1a\x02\x01-\x0f "}, + {"debug/dwarf", "\x03m\x0e\x03\x84\x013\x11\x01\x01"}, + {"debug/elf", "\x03\x06Z\r\x0e\x03h\x1b\x01\f\"\x17\x01\x17"}, + {"debug/gosym", "\x03m\x11#\xa5\x01\x01\x01\x02"}, + {"debug/macho", "\x03\x06Z\r\x11h\x1c.\x17\x01"}, + {"debug/pe", "\x03\x06Z\r\x0e\x03h\x1c.\x17\x01\x17"}, + {"debug/plan9obj", "p\x0e\x03h\x1c."}, + {"embed", "~*D\x19\x01V"}, {"embed/internal/embedtest", ""}, {"encoding", ""}, - {"encoding/ascii85", "\xfa\x01C"}, - {"encoding/asn1", "\x03q\x03g(\x01'\r\x02\x01\x11\x03\x01"}, - {"encoding/base32", "\xfa\x01A\x02"}, - {"encoding/base64", "\xa0\x01ZA\x02"}, - {"encoding/binary", "t\x86\x01\f(\r\x05"}, - {"encoding/csv", "\x02\x01q\x03\x83\x01D\x13\x02"}, - {"encoding/gob", "\x02f\x05\a\x03g\x1c\v\x01\x03\x1d\b\x12\x01\x10\x02"}, - {"encoding/hex", "t\x03\x83\x01A\x03"}, - {"encoding/json", "\x03\x01d\x04\b\x03\x83\x01\f(\r\x02\x01\x02\x11\x01\x01\x02"}, - {"encoding/pem", "\x03i\b\x86\x01A\x03"}, - {"encoding/xml", "\x02\x01e\f\x03\x83\x014\x05\n\x01\x02\x11\x02"}, - {"errors", "\xd0\x01\x85\x01"}, - {"expvar", "qLA\b\v\x15\r\b\x02\x03\x01\x12"}, - {"flag", "h\f\x03\x83\x01,\b\x05\b\x02\x01\x11"}, - {"fmt", "tF'\x19\f \b\r\x02\x03\x13"}, - {"go/ast", "\x03\x01s\x0f\x01s\x03)\b\r\x02\x01\x13\x02"}, - {"go/build", "\x02\x01q\x03\x01\x02\x02\b\x02\x01\x17\x1f\x04\x02\b\x1c\x13\x01+\x01\x04\x01\a\b\x02\x01\x13\x02\x02"}, - {"go/build/constraint", "t\xc9\x01\x01\x13\x02"}, - {"go/constant", "w\x10\x7f\x01\x024\x01\x02\x13"}, - {"go/doc", "\x04s\x01\x05\n=61\x10\x02\x01\x13\x02"}, - {"go/doc/comment", "\x03t\xc4\x01\x01\x01\x01\x13\x02"}, - {"go/format", "\x03t\x01\f\x01\x02sD"}, - {"go/importer", "y\a\x01\x02\x04\x01r9"}, - {"go/internal/gccgoimporter", "\x02\x01^\x13\x03\x04\f\x01p\x02,\x01\x05\x11\x01\r\b"}, - {"go/internal/gcimporter", "\x02u\x10\x010\x05\r0,\x15\x03\x02"}, - {"go/internal/scannerhooks", "\x87\x01"}, - {"go/internal/srcimporter", "w\x01\x01\v\x03\x01r,\x01\x05\x12\x02\x15"}, - {"go/parser", "\x03q\x03\x01\x02\b\x04\x01s\x01+\x06\x12"}, - {"go/printer", "w\x01\x02\x03\ns\f \x15\x02\x01\x02\f\x05\x02"}, - {"go/scanner", "\x03t\v\x05s2\x10\x01\x14\x02"}, - {"go/token", "\x04s\x86\x01>\x02\x03\x01\x10\x02"}, - {"go/types", "\x03\x01\x06j\x03\x01\x03\t\x03\x024\x063\x04\x03\t \x06\a\b\x01\x01\x01\x02\x01\x10\x02\x02"}, - {"go/version", "\xc2\x01|"}, - {"hash", "\xfa\x01"}, - {"hash/adler32", "t\x16\x16"}, - {"hash/crc32", "t\x16\x16\x15\x8b\x01\x01\x14"}, - {"hash/crc64", "t\x16\x16\xa0\x01"}, - {"hash/fnv", "t\x16\x16i"}, - {"hash/maphash", "\x8a\x01\x11<~"}, - {"html", "\xbe\x02\x02\x13"}, - {"html/template", "\x03n\x06\x19-=\x01\n!\x05\x01\x02\x03\f\x01\x02\r\x01\x03\x02"}, - {"image", "\x02r\x1fg\x0f4\x03\x01"}, + {"encoding/ascii85", "\x85\x02E"}, + {"encoding/asn1", "\x03{\x03h(\x01)\r\x02\x01\x11\x03\x01"}, + {"encoding/base32", "\x85\x02C\x02"}, + {"encoding/base64", "\xa9\x01\\C\x02"}, + {"encoding/binary", "~\x87\x01\f*\r\x05"}, + {"encoding/csv", "\x02\x01{\x03\x84\x01F\x13\x02"}, + {"encoding/gob", "\x02i\x05\x0e\x03h\x1c\v\x01\x03\x1f\b\x12\x01\x10\x02"}, + {"encoding/hex", "~\x03\x84\x01C\x03"}, + {"encoding/json", "\x03\x01g\n\x01\x01\x02\x01\x01\x03\x03\x84\x016\x0f\x01"}, + {"encoding/json/internal", "~"}, + {"encoding/json/internal/jsonflags", "u"}, + {"encoding/json/internal/jsonopts", "u\x01"}, + {"encoding/json/internal/jsontest", "\x03f\x15\x03\x83\x01\x01\x012\b\b\x03\x02\x0f"}, + {"encoding/json/internal/jsonwire", "\x04r\b\x87\x01\f7\x02\x01\x13\x01\x01"}, + {"encoding/json/jsontext", "\x03r\x01\x01\x02\x05\x87\x01\x03\t\x034\x02\x01\x02\x13"}, + {"encoding/json/v2", "\x03\x01g\x03\x01\x01\x03\x02\x01\x01\x02\x01\x04\x03\x84\x01\f\x03'\r\x02\x01\x02\x0f\x02\x02"}, + {"encoding/pem", "\x03l\x0f\x87\x01C\x03"}, + {"encoding/xml", "\x02\x01h\x13\x03\x84\x016\x05\n\x01\x02\x11\x02"}, + {"errors", "\xdb\x01\x87\x01"}, + {"expvar", "tSB\b\v\x17\r\b\x02\x03\x01\x12"}, + {"flag", "k\x13\x03\x84\x01.\b\x05\b\x02\x01\x11"}, + {"fmt", "~E)\x19\f\"\b\r\x02\x03\x13"}, + {"go/ast", "\x03\x01}\x0e\x01u\x03+\b\r\x02\x01\x13\x02"}, + {"go/build", "\x02\x01{\x03\x01\x02\x02\a\x02\x01\x17 \x04\x02\t\x1c\x13\x01-\x01\x04\x01\a\b\x02\x01\x13\x02\x02"}, + {"go/build/constraint", "~\xcc\x01\x01\x13\x02"}, + {"go/constant", "\x81\x01\x0f\x81\x01\x01\x026\x01\x02\x13"}, + {"go/doc", "\x04}\x01\x05\t>73\x10\x02\x01\x13\x02"}, + {"go/doc/comment", "\x03~\xc7\x01\x01\x01\x01\x13\x02"}, + {"go/format", "\x03~\x01\v\x01\x02uF"}, + {"go/importer", "\x83\x01\a\x01\x01\x04\x01t;"}, + {"go/internal/gccgoimporter", "\x02\x01a\x1a\x03\x04\v\x01r\x02.\x01\x05\x11\x01\r\b"}, + {"go/internal/gcimporter", "\x02\x7f\x0f\x010\x140.\x15\x03\x02"}, + {"go/internal/srcimporter", "\x81\x01\x01\x01\n\x03\x01t.\x01\x05\x12\x02\x15"}, + {"go/parser", "\x03{\x03\x01\x02\v\x01u\x01-\x06\x12"}, + {"go/printer", "\x81\x01\x01\x02\x03\tu\f\"\x15\x02\x01\x02\f\x05\x02"}, + {"go/scanner", "\x03~\x0fu4\x10\x01\x14\x02"}, + {"go/token", "\x04}\x87\x01@\x02\x03\x01\x10\x02"}, + {"go/types", "\x03\x01\x06t\x03\x01\x03\b\x03\x02\x0654\x04\x03\t\"\x06\a\b\x01\x01\x01\x02\x01\x10\x02\x02"}, + {"go/version", "\xcc\x01\x7f"}, + {"hash", "\x85\x02"}, + {"hash/adler32", "~\x15\x16"}, + {"hash/crc32", "~\x15\x16\x15\x8f\x01\x01\x14"}, + {"hash/crc64", "~\x15\x16\xa4\x01"}, + {"hash/fnv", "~\x15\x16k"}, + {"hash/maphash", "\x93\x01\x11>\x80\x01"}, + {"html", "\xcb\x02\x02\x13"}, + {"html/template", "\x03q\r\x18.>\x01\n#\x05\x01\x02\x03\n\x02\x01\x02\r\x01\x03\x02"}, + {"image", "\x02|\x1ei\x0f6\x03\x01"}, {"image/color", ""}, - {"image/color/palette", "\x93\x01"}, - {"image/draw", "\x92\x01\x01\x04"}, - {"image/gif", "\x02\x01\x05l\x03\x1b\x01\x01\x01\vZ\x0f"}, - {"image/internal/imageutil", "\x92\x01"}, - {"image/jpeg", "\x02r\x1e\x01\x04c"}, - {"image/png", "\x02\ad\n\x13\x02\x06\x01gC"}, - {"index/suffixarray", "\x03j\a\x86\x01\f+\n\x01"}, - {"internal/abi", "\xbc\x01\x99\x01"}, - {"internal/asan", "\xd5\x02"}, - {"internal/bisect", "\xb3\x02\r\x01"}, - {"internal/buildcfg", "wHg\x06\x02\x05\n\x01"}, - {"internal/bytealg", "\xb5\x01\xa0\x01"}, + {"image/color/palette", "\x9c\x01"}, + {"image/draw", "\x9b\x01\x01\x04"}, + {"image/gif", "\x02\x01\x05v\x03\x1a\x01\x01\x01\v\\\x0f"}, + {"image/internal/imageutil", "\x9b\x01"}, + {"image/jpeg", "\x02|\x1d\x01\x04e"}, + {"image/png", "\x02\ag\x11\x12\x02\x06\x01iE"}, + {"index/suffixarray", "\x03m\x0e\x87\x01\f-\n\x01"}, + {"internal/abi", "\xc6\x01\x9c\x01"}, + {"internal/asan", "\xe2\x02"}, + {"internal/bisect", "\xc0\x02\r\x01"}, + {"internal/buildcfg", "\x81\x01Hj\x06\x02\x05\n\x01"}, + {"internal/bytealg", "\xbe\x01\xa4\x01"}, {"internal/byteorder", ""}, {"internal/cfg", ""}, - {"internal/cgrouptest", "w[T\x06\x0f\x02\x01\x04\x01"}, - {"internal/chacha8rand", "\xa0\x01\x15\a\x99\x01"}, + {"internal/cgrouptest", "\x81\x01\\V\x06\x0f\x02\x01\x04\x01"}, + {"internal/chacha8rand", "\xa9\x01\x15\b\x9c\x01"}, {"internal/copyright", ""}, {"internal/coverage", ""}, {"internal/coverage/calloc", ""}, - {"internal/coverage/cfile", "q\x06\x17\x17\x01\x02\x01\x01\x01\x01\x01\x01\x01\"\x02',\x06\a\n\x01\x03\x0e\x06"}, - {"internal/coverage/cformat", "\x04s.\x04Q\v6\x01\x02\x0e"}, - {"internal/coverage/cmerge", "w.a"}, - {"internal/coverage/decodecounter", "m\n.\v\x02H,\x17\x18"}, - {"internal/coverage/decodemeta", "\x02k\n\x17\x17\v\x02H,"}, - {"internal/coverage/encodecounter", "\x02k\n.\f\x01\x02F\v!\x15"}, - {"internal/coverage/encodemeta", "\x02\x01j\n\x13\x04\x17\r\x02F,/"}, - {"internal/coverage/pods", "\x04s.\x81\x01\x06\x05\n\x02\x01"}, - {"internal/coverage/rtcov", "\xd5\x02"}, - {"internal/coverage/slicereader", "m\n\x83\x01["}, - {"internal/coverage/slicewriter", "w\x83\x01"}, - {"internal/coverage/stringtab", "w9\x04F"}, + {"internal/coverage/cfile", "t\r\x16\x17\x01\x02\x01\x01\x01\x01\x01\x01\x01$\x02'.\x06\a\n\x01\x03\x0e\x06"}, + {"internal/coverage/cformat", "\x04}-\x04S\v8\x01\x02\x0e"}, + {"internal/coverage/cmerge", "\x81\x01-c"}, + {"internal/coverage/decodecounter", "p\x11-\v\x02J.\x17\x18"}, + {"internal/coverage/decodemeta", "\x02n\x11\x16\x17\v\x02J."}, + {"internal/coverage/encodecounter", "\x02n\x11-\f\x01\x02H\v#\x15"}, + {"internal/coverage/encodemeta", "\x02\x01m\x11\x12\x04\x17\r\x02H./"}, + {"internal/coverage/pods", "\x04}-\x85\x01\x06\x05\n\x02\x01"}, + {"internal/coverage/rtcov", "\xe2\x02"}, + {"internal/coverage/slicereader", "p\x11\x84\x01]"}, + {"internal/coverage/slicewriter", "\x81\x01\x84\x01"}, + {"internal/coverage/stringtab", "\x81\x018\x04H"}, {"internal/coverage/test", ""}, {"internal/coverage/uleb128", ""}, - {"internal/cpu", "\xd5\x02"}, - {"internal/dag", "\x04s\xc4\x01\x03"}, - {"internal/diff", "\x03t\xc5\x01\x02"}, - {"internal/exportdata", "\x02\x01q\x03\x02e\x1c,\x01\x05\x11\x01\x02"}, - {"internal/filepathlite", "t+B\x1a@"}, - {"internal/fmtsort", "\x04\xaa\x02\r"}, - {"internal/fuzz", "\x03\nH\x18\x04\x03\x03\x01\f\x036=\f\x03\x1d\x01\x05\x02\x05\n\x01\x02\x01\x01\r\x04\x02"}, + {"internal/cpu", "\xe2\x02"}, + {"internal/dag", "\x04}\xc7\x01\x03"}, + {"internal/diff", "\x03~\xc8\x01\x02"}, + {"internal/exportdata", "\x02\x01{\x03\x02f\x1c.\x01\x05\x11\x01\x02"}, + {"internal/filepathlite", "~*D\x1aB"}, + {"internal/fmtsort", "\x04\xb7\x02\r"}, + {"internal/fuzz", "\x03\nJ\x19\x04\n\x03\x01\v\x037>\f\x03\x1f\x01\x05\x02\x05\n\x01\x02\x01\x01\r\x04\x02"}, + {"internal/gate", "\r"}, {"internal/goarch", ""}, - {"internal/godebug", "\x9d\x01!\x82\x01\x01\x14"}, + {"internal/godebug", "\xa6\x01\"\x85\x01\x01\x14"}, {"internal/godebugs", ""}, {"internal/goexperiment", ""}, {"internal/goos", ""}, - {"internal/goroot", "\xa6\x02\x01\x05\x12\x02"}, + {"internal/goroot", "\xb3\x02\x01\x05\x12\x02"}, {"internal/gover", "\x04"}, {"internal/goversion", ""}, - {"internal/lazyregexp", "\xa6\x02\v\r\x02"}, - {"internal/lazytemplate", "\xfa\x01,\x18\x02\r"}, - {"internal/msan", "\xd5\x02"}, + {"internal/lazyregexp", "\xb3\x02\v\r\x02"}, + {"internal/lazytemplate", "\x85\x02.\x18\x02\r"}, + {"internal/msan", "\xe2\x02"}, + {"internal/nettest", "\x03\nqG@\f\n\x12\x06\x15\x05\x0f"}, {"internal/nettrace", ""}, - {"internal/obscuretestdata", "l\x8e\x01,"}, - {"internal/oserror", "t"}, - {"internal/pkgbits", "\x03R\x18\a\x03\x04\fs\r\x1f\r\n\x01"}, + {"internal/obscuretestdata", "o\x96\x01."}, + {"internal/oserror", "~"}, + {"internal/pkgbits", "\x03T\x19\x0e\x03\x04\vu\r!\r\n\x01"}, {"internal/platform", ""}, - {"internal/poll", "tl\x05\x159\r\x01\x01\r\x06"}, - {"internal/profile", "\x03\x04m\x03\x83\x017\n\x01\x01\x01\x11"}, + {"internal/poll", "~m\x05\x15;\r\x01\x01\r\x06"}, + {"internal/profile", "\x03\x04w\x03\x84\x019\n\x01\x01\x01\x11"}, {"internal/profilerecord", ""}, - {"internal/race", "\x9b\x01\xba\x01"}, - {"internal/reflectlite", "\x9b\x01!;<\""}, - {"internal/runtime/atomic", "\xbc\x01\x99\x01"}, - {"internal/runtime/cgroup", "\x9f\x01=\x04u"}, - {"internal/runtime/exithook", "\xd1\x01\x84\x01"}, - {"internal/runtime/gc", "\xbc\x01"}, - {"internal/runtime/gc/internal/gen", "\nc\n\x18k\x04\v\x1d\b\x10\x02"}, - {"internal/runtime/gc/scan", "\xb5\x01\a\x18\az"}, - {"internal/runtime/maps", "\x9b\x01\x01 \n\t\t\x03z"}, - {"internal/runtime/math", "\xbc\x01"}, + {"internal/race", "\xa4\x01\xbe\x01"}, + {"internal/reflectlite", "\xa4\x01\"<>\""}, + {"internal/runtime/atomic", "\xc6\x01\x9c\x01"}, + {"internal/runtime/cgroup", "\xa8\x01?\x04w"}, + {"internal/runtime/exithook", "\xdc\x01\x86\x01"}, + {"internal/runtime/gc", "\xc6\x01"}, + {"internal/runtime/gc/internal/gen", "\nf\x11\x17m\x04\v\x1f\b\x10\x02"}, + {"internal/runtime/gc/scan", "\xbe\x01\b\x19\a|"}, + {"internal/runtime/maps", "\xa4\x01\x01\x04\x15\b\x03\a\n\t\x03.N"}, + {"internal/runtime/math", "\xc6\x01"}, {"internal/runtime/pprof/label", ""}, {"internal/runtime/startlinetest", ""}, - {"internal/runtime/sys", "\xbc\x01\x04"}, - {"internal/runtime/syscall/linux", "\xbc\x01\x99\x01"}, + {"internal/runtime/sys", "\xc6\x01\x04"}, + {"internal/runtime/syscall/linux", "\xc6\x01\x9c\x01"}, {"internal/runtime/wasitest", ""}, - {"internal/saferio", "\xfa\x01["}, - {"internal/singleflight", "\xc0\x02"}, - {"internal/strconv", "\x89\x02L"}, - {"internal/stringslite", "\x9f\x01\xb6\x01"}, - {"internal/sync", "\x9b\x01!\x13r\x14"}, - {"internal/synctest", "\x9b\x01\xba\x01"}, - {"internal/syscall/execenv", "\xc2\x02"}, - {"internal/syscall/unix", "\xb3\x02\x0e\x01\x13"}, - {"internal/sysinfo", "\x02\x01\xb2\x01E,\x18\x02"}, + {"internal/saferio", "\x85\x02]"}, + {"internal/singleflight", "\xcd\x02"}, + {"internal/strconv", "\x94\x02N"}, + {"internal/stringslite", "\xa8\x01\xba\x01"}, + {"internal/sync", "\xa4\x01\"\x14t\x14"}, + {"internal/synctest", "\xa4\x01\xbe\x01"}, + {"internal/syscall/execenv", "\xcf\x02"}, + {"internal/syscall/unix", "\xeb\x01U\x0e\x01\x13"}, + {"internal/sysinfo", "\x02\x01\xbb\x01G.\x18\x02"}, {"internal/syslist", ""}, - {"internal/testenv", "\x03\ng\x02\x01*\x1b\x0f0+\x01\x05\a\n\x01\x02\x02\x01\f"}, - {"internal/testhash", "\x03\x87\x01p\x118\f"}, - {"internal/testlog", "\xc0\x02\x01\x14"}, - {"internal/testpty", "t\x03\xaf\x01"}, - {"internal/trace", "\x02\x01\x01\x06c\a\x03w\x03\x03\x06\x03\t+\n\x01\x01\x01\x11\x06"}, - {"internal/trace/internal/testgen", "\x03j\nu\x03\x02\x03\x011\v\r\x11"}, - {"internal/trace/internal/tracev1", "\x03\x01i\a\x03}\x06\f5\x01"}, - {"internal/trace/raw", "\x02k\nz\x03\x06C\x01\x13"}, - {"internal/trace/testtrace", "\x02\x01q\x03q\x04\x03\x05\x01\x05,\v\x02\b\x02\x01\x05"}, + {"internal/testenv", "\x03\nq\x02\x01)\x1c\x100-\x01\x05\a\n\x01\x02\x02\x01\f"}, + {"internal/testhash", "\x03\x90\x01r\x11:\f"}, + {"internal/testlog", "\xcd\x02\x01\x14"}, + {"internal/testpty", "~\x03\xb2\x01"}, + {"internal/trace", "\x02\x01\x01\x06f\x0e\x03x\x03\x03\x06\x03\t-\n\x01\x01\x01\x11\x06"}, + {"internal/trace/internal/testgen", "\x03m\x11v\x03\x02\x03\x013\v\r\x11"}, + {"internal/trace/internal/tracev1", "\x03\x01l\x0e\x03~\x06\f7\x01"}, + {"internal/trace/raw", "\x02n\x11{\x03\x06E\x01\x13"}, + {"internal/trace/testtrace", "\x02\x01{\x03r\x04\x03\x05\x01\x05.\v\x02\b\x02\x01\x05"}, {"internal/trace/tracev2", ""}, - {"internal/trace/traceviewer", "\x02d\v\x06\x1a<\x1f\a\a\x04\b\v\x15\x01\x05\a\n\x01\x02\x0f"}, + {"internal/trace/traceviewer", "\x02g\v\r\x19>\x1f\a\a\x04\b\v\x17\x01\x05\a\n\x01\x02\x0f"}, {"internal/trace/traceviewer/format", ""}, - {"internal/trace/version", "wz\t"}, - {"internal/txtar", "\x03t\xaf\x01\x18"}, - {"internal/types/errors", "\xbd\x02"}, - {"internal/unsafeheader", "\xd5\x02"}, - {"internal/xcoff", "`\r\a\x03g\x1c,\x17\x01"}, - {"internal/zstd", "m\a\x03\x83\x01\x0f"}, - {"io", "t\xcc\x01"}, - {"io/fs", "t+*11\x10\x14\x04"}, - {"io/ioutil", "\xfa\x01\x01+\x15\x03"}, - {"iter", "\xcf\x01d\""}, - {"log", "w\x83\x01\x05'\r\r\x01\x0e"}, + {"internal/trace/version", "\x81\x01{\t"}, + {"internal/txtar", "\x03~\xb2\x01\x18"}, + {"internal/types/errors", "\xca\x02"}, + {"internal/unsafeheader", "\xe2\x02"}, + {"internal/xcoff", "c\r\x0e\x03h\x1c.\x17\x01"}, + {"internal/zstd", "p\x0e\x03\x84\x01\x0f"}, + {"io", "~\xcf\x01"}, + {"io/fs", "~*,13\x10\x14\x04"}, + {"io/ioutil", "\x85\x02\x01-\x15\x03"}, + {"iter", "\xda\x01f\""}, + {"log", "\x81\x01\x84\x01\x05)\r\r\x01\x0e"}, {"log/internal", ""}, - {"log/slog", "\x03\n[\t\x03\x03\x83\x01\x04\x01\x02\x02\x03(\x05\b\x02\x01\x02\x01\x0e\x02\x02\x02"}, + {"log/slog", "\x03\n^\t\n\x03H<\x04\x01\x02\x02\x03*\x05\b\x02\x01\x02\x01\x0e\x02\x02\x02"}, {"log/slog/internal", ""}, - {"log/slog/internal/benchmarks", "\rg\x03\x83\x01\x06\x03:\x12"}, - {"log/slog/internal/buffer", "\xc0\x02"}, - {"log/syslog", "t\x03\x87\x01\x12\x16\x18\x02\x0f"}, - {"maps", "\xfd\x01X"}, - {"math", "\xb5\x01TL"}, - {"math/big", "\x03q\x03)\x15E\f\x03\x020\x02\x01\x02\x15"}, - {"math/big/internal/asmgen", "\x03\x01s\x92\x012\x03"}, - {"math/bits", "\xd5\x02"}, - {"math/cmplx", "\x86\x02\x03"}, - {"math/rand", "\xbd\x01I:\x01\x14"}, - {"math/rand/v2", "t,\x03c\x03L"}, - {"mime", "\x02\x01i\b\x03\x83\x01\v!\x15\x03\x02\x11\x02"}, - {"mime/multipart", "\x02\x01N#\x03F=\v\x01\a\x02\x15\x02\x06\x0f\x02\x01\x17"}, - {"mime/quotedprintable", "\x02\x01t\x83\x01"}, - {"net", "\x04\tg+\x1e\n\x05\x13\x01\x01\x04\x15\x01%\x06\r\b\x05\x01\x01\r\x06\a"}, - {"net/http", "\x02\x01\x03\x01\x04\x02D\b\x13\x01\a\x03F=\x01\x03\a\x01\x03\x02\x02\x01\x02\x06\x02\x01\x01\n\x01\x01\x05\x01\x02\x05\b\x01\x01\x01\x02\x01\x0e\x02\x02\x02\b\x01\x01\x01"}, - {"net/http/cgi", "\x02W\x1b\x03\x83\x01\x04\a\v\x01\x13\x01\x01\x01\x04\x01\x05\x02\b\x02\x01\x11\x0e"}, - {"net/http/cookiejar", "\x04p\x03\x99\x01\x01\b\a\x05\x16\x03\x02\x0f\x04"}, - {"net/http/fcgi", "\x02\x01\n`\a\x03\x83\x01\x16\x01\x01\x14\x18\x02\x0f"}, - {"net/http/httptest", "\x02\x01\nL\x02\x1b\x01\x83\x01\x04\x12\x01\n\t\x02\x17\x01\x02\x0f\x0e"}, - {"net/http/httptrace", "\rLnI\x14\n!"}, - {"net/http/httputil", "\x02\x01\ng\x03\x83\x01\x04\x0f\x03\x01\x05\x02\x01\v\x01\x19\x02\x01\x0e\x0e"}, - {"net/http/internal", "\x02\x01q\x03\x83\x01"}, - {"net/http/internal/ascii", "\xbe\x02\x13"}, - {"net/http/internal/httpcommon", "\rg\x03\x9f\x01\x0e\x01\x17\x01\x01\x02\x1d\x02"}, - {"net/http/internal/testcert", "\xbe\x02"}, - {"net/http/pprof", "\x02\x01\nj\x19-\x02\x0e-\x04\x13\x14\x01\r\x04\x03\x01\x02\x01\x11"}, + {"log/slog/internal/benchmarks", "\rq\x03\x84\x01\x06\x03<\x12"}, + {"log/slog/internal/buffer", "\xcd\x02"}, + {"log/syslog", "~\x03\x88\x01\x12\x18\x18\x02\x0f"}, + {"maps", "\x88\x02Z"}, + {"math", "\xbe\x01VN"}, + {"math/big", "\x03{\x03(\x15G\f\x03\x022\x02\x01\x02\x15"}, + {"math/big/internal/asmgen", "\x03\x01}\x93\x014\x03"}, + {"math/bits", "\xe2\x02"}, + {"math/cmplx", "\x91\x02\x03"}, + {"math/rand", "\xc7\x01J<\x01\x14"}, + {"math/rand/v2", "~+\x03e\x03N"}, + {"mime", "\x02\x01l\x0f\x03\x84\x01\v#\x15\x03\x02\x11\x02"}, + {"mime/multipart", "\x02\x01P+\x03F>\v\x01\a\x02\x17\x02\x06\x0f\x02\x01\x17"}, + {"mime/quotedprintable", "\x02\x01~\x84\x01"}, + {"net", "\x04\tq*\x1f\v\x05\x13\x01\x01\x04\x15\x01'\x06\r\b\x05\x01\x01\r\x06\t"}, + {"net/http", "\x02\x01\x03\x01\x04\x02N\x14\x0f\x03F>\x01\x03\a\x01\x06\x01\x01\x02\x06\x02\x01\x01\f\x01\x01\x05\x01\x02\x05\b\x01\x01\x01\x02\x01\x0e\x02\x02\x02\n\x01\x03"}, + {"net/http/cgi", "\x02Y#\x03\x84\x01\x04\a\v\x01\x15\x01\x01\x01\x04\x01\x05\x02\b\x02\x01\x11\x10"}, + {"net/http/cookiejar", "\x04z\x03\x9a\x01\x01\b\t\x05\x16\x03\x02\x0f\x04"}, + {"net/http/fcgi", "\x02\x01\nc\x0e\x03\x84\x01\x16\x01\x01\x16\x18\x02\x0f"}, + {"net/http/httptest", "\x02\x01\nN\x02#\x01P4\x04\x12\x01\f\t\x02\r\n\x01\x02\x03\f\x06\n"}, + {"net/http/httptrace", "\rNwI\x16+"}, + {"net/http/httputil", "\x02\x01\nq\x03F>\x04\x0f\x03\x01\x05\x02\x01\r\x01\x19\x02\x01\x0e\x10"}, + {"net/http/internal", "\x02\x01m\x0e\x03\x84\x01"}, + {"net/http/internal/ascii", "\xcb\x02\x13"}, + {"net/http/internal/http2", "\x02\x01\x03\x01\x06F\b\x15\x0e\x03\x84\x01\x01\x03\b\x03\x02\x03\x02\x06\x02\x03\x01\n\x01\x01\b\x05\b\x02\x01\x02\x01\x0e\x10\x02\x02"}, + {"net/http/internal/httpcommon", "\rq\x03\xa0\x01\x10\x01\x17\x01\x01\x02\x1f\x02"}, + {"net/http/internal/httpsfv", "\xc8\x02\x02\x01\x11\x04"}, + {"net/http/internal/testcert", "\xcb\x02"}, + {"net/http/pprof", "\x02\x01\nt\x18.\x11-\x04\x13\x16\x01\r\x04\x03\x01\x02\x01\x11"}, {"net/internal/cgotest", ""}, - {"net/internal/socktest", "w\xc9\x01\x02"}, - {"net/mail", "\x02r\x03\x83\x01\x04\x0f\x03\x14\x1a\x02\x0f\x04"}, - {"net/netip", "\x04p+\x01f\x034\x17"}, - {"net/rpc", "\x02m\x05\x03\x10\ni\x04\x12\x01\x1d\r\x03\x02"}, - {"net/rpc/jsonrpc", "q\x03\x03\x83\x01\x16\x11\x1f"}, - {"net/smtp", "\x194\f\x13\b\x03\x83\x01\x16\x14\x1a"}, - {"net/textproto", "\x02\x01q\x03\x83\x01\f\n-\x01\x02\x15"}, - {"net/url", "t\x03Fc\v\x10\x02\x01\x17"}, - {"os", "t+\x01\x19\x03\x10\x14\x01\x03\x01\x05\x10\x018\b\x05\x01\x01\r\x06"}, - {"os/exec", "\x03\ngI'\x01\x15\x01+\x06\a\n\x01\x03\x01\r"}, - {"os/exec/internal/fdtest", "\xc2\x02"}, - {"os/signal", "\r\x99\x02\x15\x05\x02"}, - {"os/user", "\x02\x01q\x03\x83\x01,\r\n\x01\x02"}, - {"path", "t+\xb4\x01"}, - {"path/filepath", "t+\x1aB+\r\b\x03\x04\x11"}, - {"plugin", "t"}, - {"reflect", "t'\x04\x1d\x13\b\x04\x05\x17\x06\t-\n\x03\x11\x02\x02"}, + {"net/internal/socktest", "\x81\x01\xcc\x01\x02"}, + {"net/mail", "\x02|\x03\x84\x01\x04\x0f\x03\x16\x1a\x02\x0f\x04"}, + {"net/netip", "\x04z*\x01h\x036\x17"}, + {"net/rpc", "\x02p\f\x03\x0f\nk\x04\x12\x01\x1f\r\x03\x02"}, + {"net/rpc/jsonrpc", "t\n\x03\x84\x01\x16\x13\x1f"}, + {"net/smtp", "\x195\r\x14\x0f\x03\x84\x01\x16\x16\x1a"}, + {"net/textproto", "\x02\x01{\x03\x84\x01\f\n/\x01\x02\x15"}, + {"net/url", "~\x03Ff\v\x10\x02\x01\x17"}, + {"os", "~*\x01\x19\x04\x11\x14\x01\x03\x01\x05\x10\x01:\b\x05\x01\x01\r\x06"}, + {"os/exec", "\x03\nqI(\x01\x15\x01-\x06\a\n\x01\x03\x01\r"}, + {"os/exec/internal/fdtest", "\xcf\x02"}, + {"os/signal", "\r\xa6\x02\x15\x05\x02"}, + {"os/user", "\x02\x01{\x03\x84\x01.\r\n\x01\x02"}, + {"path", "~*\xb8\x01"}, + {"path/filepath", "~*\x1aD-\r\b\x03\x04\x11"}, + {"plugin", "~"}, + {"reflect", "~&\x04\x1e\x03\x11\b\x04\x05\x17\x06\t/\n\x03\x11\x02\x02"}, {"reflect/internal/example1", ""}, {"reflect/internal/example2", ""}, - {"regexp", "\x03\xf7\x018\t\x02\x01\x02\x11\x02"}, - {"regexp/syntax", "\xbb\x02\x01\x01\x01\x02\x11\x02"}, - {"runtime", "\x9b\x01\x04\x01\x03\f\x06\a\x02\x01\x01\x0e\x03\x01\x01\x01\x02\x01\x01\x01\x02\x01\x04\x01\x10\x18L"}, - {"runtime/coverage", "\xa7\x01S"}, - {"runtime/debug", "wUZ\r\b\x02\x01\x11\x06"}, - {"runtime/metrics", "\xbe\x01H-\""}, - {"runtime/pprof", "\x02\x01\x01\x03\x06`\a\x03$$\x0f\v!\f \r\b\x01\x01\x01\x02\x02\n\x03\x06"}, - {"runtime/race", "\xb9\x02"}, + {"regexp", "\x03\x82\x02\x037\t\x02\x01\x02\x11\x02"}, + {"regexp/syntax", "\xc8\x02\x01\x01\x01\x02\x11\x02"}, + {"runtime", "\xa4\x01\x04\x01\x03\f\x06\b\x02\x01\x01\x0f\x03\x01\x01\x01\x02\x01\x01\x01\x02\x01\x04\x01\x10\x18N"}, + {"runtime/coverage", "\xb0\x01U"}, + {"runtime/debug", "\x81\x01V\\\r\b\x02\x01\x11\x06"}, + {"runtime/metrics", "\xc8\x01I/\""}, + {"runtime/pprof", "\x02\x01\x01\x03\x06c\x0e\x03#5\v!\f\"\r\b\x01\x01\x01\x02\x02\n\x03\x06"}, + {"runtime/race", "\xc6\x02"}, {"runtime/race/internal/amd64v1", ""}, - {"runtime/trace", "\rg\x03z\t9\b\x05\x01\x0e\x06"}, - {"slices", "\x04\xf9\x01\fL"}, - {"sort", "\xd0\x0192"}, - {"strconv", "t+A\x01r"}, - {"strings", "t'\x04B\x19\x03\f7\x11\x02\x02"}, + {"runtime/trace", "\rq\x03{\t;\b\x05\x01\x0e\x06"}, + {"slices", "\x04\x84\x02\fN"}, + {"sort", "\xdb\x0194"}, + {"strconv", "~*C\x01t"}, + {"strings", "~&\x04D\x19\x03\f9\x11\x02\x02"}, {"structs", ""}, - {"sync", "\xcf\x01\x13\x01P\x0e\x14"}, - {"sync/atomic", "\xd5\x02"}, - {"syscall", "t(\x03\x01\x1c\n\x03\x06\r\x04S\b\x05\x01\x14"}, - {"testing", "\x03\ng\x02\x01X\x17\x14\f\x05\x1b\x06\x02\x05\x02\x05\x01\x02\x01\x02\x01\x0e\x02\x04"}, - {"testing/cryptotest", "QOZ\x124\x03\x12"}, - {"testing/fstest", "t\x03\x83\x01\x01\n&\x10\x03\t\b"}, - {"testing/internal/testdeps", "\x02\v\xae\x01/\x10,\x03\x05\x03\x06\a\x02\x0f"}, - {"testing/iotest", "\x03q\x03\x83\x01\x04"}, - {"testing/quick", "v\x01\x8f\x01\x05#\x10\x11"}, - {"testing/slogtest", "\rg\x03\x89\x01.\x05\x10\f"}, - {"testing/synctest", "\xe3\x01`\x12"}, - {"text/scanner", "\x03t\x83\x01,+\x02"}, - {"text/tabwriter", "w\x83\x01Y"}, - {"text/template", "t\x03C@\x01\n \x01\x05\x01\x02\x05\v\x02\x0e\x03\x02"}, - {"text/template/parse", "\x03t\xbc\x01\n\x01\x13\x02"}, - {"time", "t+\x1e$(*\r\x02\x13"}, - {"time/tzdata", "t\xce\x01\x13"}, + {"sync", "\xda\x01\x02\x11\x01R\x0e\x14"}, + {"sync/atomic", "\xe2\x02"}, + {"syscall", "~'\x03\x01\x1d\n\x04\x06\r\x04U\b\x05\x01\x14"}, + {"testing", "\x03\nq\x02\x01Y\x17\x14\f\x05\x1d\x06\x02\x05\x02\x05\x01\x02\x01\x02\x01\x0e\x02\x04"}, + {"testing/cryptotest", "SV\\\x126\x03\x12"}, + {"testing/fstest", "~\x03\x84\x01\x01\n(\x10\x03\t\b"}, + {"testing/internal/testdeps", "\x02\v\xb7\x011\x10.\x03\x05\x03\x06\a\x02\x0f"}, + {"testing/iotest", "\x03{\x03\x84\x01\x04"}, + {"testing/quick", "\x80\x01\x01\x90\x01\x05%\x10\x11"}, + {"testing/slogtest", "\rq\x03\x8a\x010\x05\x10\f"}, + {"testing/synctest", "\xee\x01b\f\x06"}, + {"text/scanner", "\x03~\x84\x01.+\x02"}, + {"text/tabwriter", "\x81\x01\x84\x01["}, + {"text/template", "~\x03BB\x01\n\"\x01\x05\x01\x02\x05\v\x02\x0e\x03\x02"}, + {"text/template/parse", "\x03~\xbf\x01\n\x01\x13\x02"}, + {"time", "~*D(,\r\x02\x13"}, + {"time/tzdata", "~\xd1\x01\x13"}, {"unicode", ""}, {"unicode/utf16", ""}, {"unicode/utf8", ""}, - {"unique", "\x9b\x01!%\x01Q\r\x01\x14\x12"}, + {"unique", "\xa4\x01\"&\x01S\r\x01\x14\x19"}, {"unsafe", ""}, - {"vendor/golang.org/x/crypto/chacha20", "\x10]\a\x95\x01*'"}, - {"vendor/golang.org/x/crypto/chacha20poly1305", "\x10\aV\a\xe2\x01\x04\x01\a"}, - {"vendor/golang.org/x/crypto/cryptobyte", "j\n\x03\x90\x01'!\n"}, + {"uuid", "\x03\x01O\x1d\x03\v\xcf\x01\x0f"}, + {"vendor/golang.org/x/crypto/chacha20", "\x10`\x0e\x96\x01,)"}, + {"vendor/golang.org/x/crypto/chacha20poly1305", "\x10\aY\x0e\xe6\x01\x05\x01\f"}, + {"vendor/golang.org/x/crypto/cryptobyte", "m\x11\x03\x91\x01)!\v"}, {"vendor/golang.org/x/crypto/cryptobyte/asn1", ""}, - {"vendor/golang.org/x/crypto/internal/alias", "\xd5\x02"}, - {"vendor/golang.org/x/crypto/internal/poly1305", "X\x15\x9c\x01"}, - {"vendor/golang.org/x/net/dns/dnsmessage", "t\xc7\x01"}, - {"vendor/golang.org/x/net/http/httpguts", "\x90\x02\x14\x1a\x15\r"}, - {"vendor/golang.org/x/net/http/httpproxy", "t\x03\x99\x01\x10\x05\x01\x18\x15\r"}, - {"vendor/golang.org/x/net/http2/hpack", "\x03q\x03\x83\x01F"}, - {"vendor/golang.org/x/net/idna", "w\x8f\x018\x15\x10\x02\x01"}, - {"vendor/golang.org/x/net/nettest", "\x03j\a\x03\x83\x01\x11\x05\x16\x01\f\n\x01\x02\x02\x01\f"}, - {"vendor/golang.org/x/sys/cpu", "\xa6\x02\r\n\x01\x17"}, - {"vendor/golang.org/x/text/secure/bidirule", "t\xdf\x01\x11\x01"}, - {"vendor/golang.org/x/text/transform", "\x03q\x86\x01Y"}, - {"vendor/golang.org/x/text/unicode/bidi", "\x03\bl\x87\x01>\x17"}, - {"vendor/golang.org/x/text/unicode/norm", "m\n\x83\x01F\x13\x11"}, - {"weak", "\x9b\x01\x98\x01\""}, + {"vendor/golang.org/x/crypto/hkdf", "\x18\x01e\x15r"}, + {"vendor/golang.org/x/crypto/internal/alias", "\xe2\x02"}, + {"vendor/golang.org/x/crypto/internal/poly1305", "Z\x16\xa4\x01"}, + {"vendor/golang.org/x/net/dns/dnsmessage", "~\xca\x01"}, + {"vendor/golang.org/x/net/http/httpguts", "\x9b\x02\x16\x1a\x15\x10"}, + {"vendor/golang.org/x/net/http/httpproxy", "~\x03\x9a\x01\x12\x05\x01\x18\x15\x10"}, + {"vendor/golang.org/x/net/http2/hpack", "\x03{\x03\x84\x01H"}, + {"vendor/golang.org/x/net/http3", "\x9c\x02@\x06\x0f\x04"}, + {"vendor/golang.org/x/net/idna", "\x81\x01\x90\x01:\x13\x02\x17\x02\x01"}, + {"vendor/golang.org/x/net/internal/http3", "\rN#\x03\x84\x01\v\x04\a\x01\x05\x10\x01\x16\x02\x01\x02\x0f\x10\x02\x04\x03"}, + {"vendor/golang.org/x/net/internal/httpcommon", "\rq\x03\xa0\x01\x10\x01\x17\x01\x01\x02\x1f\x02"}, + {"vendor/golang.org/x/net/internal/quic/quicwire", "p"}, + {"vendor/golang.org/x/net/nettest", "\x03m\x0e\x03\x84\x01\x11\x05\x18\x01\f\n\x01\x02\x02\x01\f"}, + {"vendor/golang.org/x/net/quic", "\x03\n\x01\x01\x01\t:\x04\x04\x15\x03\v\x03\x12r\x06\x06\x06\x04\x12\x06\x15\x02\x01\x02\x01\x01\r\x06\x02\x01\x01\x02\v"}, + {"vendor/golang.org/x/sys/cpu", "\xb3\x02\r\n\x01\x17"}, + {"vendor/golang.org/x/text/secure/bidirule", "~\xe2\x01\x18\x01"}, + {"vendor/golang.org/x/text/transform", "\x03{\x87\x01["}, + {"vendor/golang.org/x/text/unicode/bidi", "\x03\bv\x88\x01@\x17"}, + {"vendor/golang.org/x/text/unicode/norm", "p\x11\x84\x01H\x13\x18"}, + {"weak", "\xa4\x01\x9c\x01\""}, } // bootstrap is the list of bootstrap packages extracted from cmd/dist. @@ -408,6 +428,7 @@ var bootstrap = map[string]bool{ "cmd/compile/internal/logopt": true, "cmd/compile/internal/loong64": true, "cmd/compile/internal/loopvar": true, + "cmd/compile/internal/midway": true, "cmd/compile/internal/mips": true, "cmd/compile/internal/mips64": true, "cmd/compile/internal/noder": true, @@ -512,6 +533,7 @@ var bootstrap = map[string]bool{ "internal/race": true, "internal/runtime/gc": true, "internal/saferio": true, + "internal/strconv": true, "internal/syscall/unix": true, "internal/types/errors": true, "internal/unsafeheader": true, diff --git a/vendor/golang.org/x/tools/internal/stdlib/manifest.go b/vendor/golang.org/x/tools/internal/stdlib/manifest.go index 33e4f505f..2180c296e 100644 --- a/vendor/golang.org/x/tools/internal/stdlib/manifest.go +++ b/vendor/golang.org/x/tools/internal/stdlib/manifest.go @@ -270,6 +270,7 @@ var PackageSymbols = map[string][]Symbol{ {"ContainsRune", Func, 7, "func(b []byte, r rune) bool"}, {"Count", Func, 0, "func(s []byte, sep []byte) int"}, {"Cut", Func, 18, "func(s []byte, sep []byte) (before []byte, after []byte, found bool)"}, + {"CutLast", Func, 27, "func(s []byte, sep []byte) (before []byte, after []byte, found bool)"}, {"CutPrefix", Func, 20, "func(s []byte, prefix []byte) (after []byte, found bool)"}, {"CutSuffix", Func, 20, "func(s []byte, suffix []byte) (before []byte, found bool)"}, {"Equal", Func, 0, "func(a []byte, b []byte) bool"}, @@ -538,6 +539,7 @@ var PackageSymbols = map[string][]Symbol{ {"MD4", Const, 0, ""}, {"MD5", Const, 0, ""}, {"MD5SHA1", Const, 0, ""}, + {"MLDSAMu", Const, 27, ""}, {"MessageSigner", Type, 25, ""}, {"PrivateKey", Type, 0, ""}, {"PublicKey", Type, 2, ""}, @@ -812,6 +814,40 @@ var PackageSymbols = map[string][]Symbol{ {"Size", Const, 0, ""}, {"Sum", Func, 2, "func(data []byte) [16]byte"}, }, + "crypto/mldsa": { + {"(*Options).HashFunc", Method, 27, ""}, + {"(*PrivateKey).Bytes", Method, 27, ""}, + {"(*PrivateKey).Equal", Method, 27, ""}, + {"(*PrivateKey).Public", Method, 27, ""}, + {"(*PrivateKey).PublicKey", Method, 27, ""}, + {"(*PrivateKey).Sign", Method, 27, ""}, + {"(*PrivateKey).SignDeterministic", Method, 27, ""}, + {"(*PublicKey).Bytes", Method, 27, ""}, + {"(*PublicKey).Equal", Method, 27, ""}, + {"(*PublicKey).Parameters", Method, 27, ""}, + {"(Parameters).PublicKeySize", Method, 27, ""}, + {"(Parameters).SignatureSize", Method, 27, ""}, + {"(Parameters).String", Method, 27, ""}, + {"GenerateKey", Func, 27, "func(params Parameters) (*PrivateKey, error)"}, + {"MLDSA44", Func, 27, "func() Parameters"}, + {"MLDSA44PublicKeySize", Const, 27, ""}, + {"MLDSA44SignatureSize", Const, 27, ""}, + {"MLDSA65", Func, 27, "func() Parameters"}, + {"MLDSA65PublicKeySize", Const, 27, ""}, + {"MLDSA65SignatureSize", Const, 27, ""}, + {"MLDSA87", Func, 27, "func() Parameters"}, + {"MLDSA87PublicKeySize", Const, 27, ""}, + {"MLDSA87SignatureSize", Const, 27, ""}, + {"NewPrivateKey", Func, 27, "func(params Parameters, seed []byte) (*PrivateKey, error)"}, + {"NewPublicKey", Func, 27, "func(params Parameters, encoding []byte) (*PublicKey, error)"}, + {"Options", Type, 27, ""}, + {"Options.Context", Field, 27, ""}, + {"Parameters", Type, 27, ""}, + {"PrivateKey", Type, 27, ""}, + {"PrivateKeySize", Const, 27, ""}, + {"PublicKey", Type, 27, ""}, + {"Verify", Func, 27, "func(pk *PublicKey, message []byte, signature []byte, opts *Options) error"}, + }, "crypto/mlkem": { {"(*DecapsulationKey1024).Bytes", Method, 24, ""}, {"(*DecapsulationKey1024).Decapsulate", Method, 24, ""}, @@ -1120,6 +1156,7 @@ var PackageSymbols = map[string][]Symbol{ {"ConnectionState.ECHAccepted", Field, 23, ""}, {"ConnectionState.HandshakeComplete", Field, 0, ""}, {"ConnectionState.HelloRetryRequest", Field, 26, ""}, + {"ConnectionState.LocalCertificate", Field, 27, ""}, {"ConnectionState.NegotiatedProtocol", Field, 0, ""}, {"ConnectionState.NegotiatedProtocolIsMutual", Field, 0, ""}, {"ConnectionState.OCSPResponse", Field, 5, ""}, @@ -1152,6 +1189,10 @@ var PackageSymbols = map[string][]Symbol{ {"InsecureCipherSuites", Func, 14, "func() []*CipherSuite"}, {"Listen", Func, 0, "func(network string, laddr string, config *Config) (net.Listener, error)"}, {"LoadX509KeyPair", Func, 0, "func(certFile string, keyFile string) (Certificate, error)"}, + {"MLDSA44", Const, 27, ""}, + {"MLDSA65", Const, 27, ""}, + {"MLDSA87", Const, 27, ""}, + {"MLKEM1024", Const, 27, ""}, {"NewLRUClientSessionCache", Func, 3, "func(capacity int) ClientSessionCache"}, {"NewListener", Func, 0, "func(inner net.Listener, config *Config) net.Listener"}, {"NewResumptionState", Func, 21, "func(ticket []byte, state *SessionState) (*ClientSessionState, error)"}, @@ -1166,6 +1207,7 @@ var PackageSymbols = map[string][]Symbol{ {"ParseSessionState", Func, 21, "func(data []byte) (*SessionState, error)"}, {"QUICClient", Func, 21, "func(config *QUICConfig) *QUICConn"}, {"QUICConfig", Type, 21, ""}, + {"QUICConfig.ClientHelloInfoConn", Field, 27, ""}, {"QUICConfig.EnableSessionEvents", Field, 23, ""}, {"QUICConfig.TLSConfig", Field, 21, ""}, {"QUICConn", Type, 21, ""}, @@ -1334,6 +1376,7 @@ var PackageSymbols = map[string][]Symbol{ {"Certificate.PublicKeyAlgorithm", Field, 0, ""}, {"Certificate.Raw", Field, 0, ""}, {"Certificate.RawIssuer", Field, 0, ""}, + {"Certificate.RawSignatureAlgorithm", Field, 27, ""}, {"Certificate.RawSubject", Field, 0, ""}, {"Certificate.RawSubjectPublicKeyInfo", Field, 0, ""}, {"Certificate.RawTBSCertificate", Field, 0, ""}, @@ -1362,6 +1405,7 @@ var PackageSymbols = map[string][]Symbol{ {"CertificateRequest.PublicKey", Field, 3, ""}, {"CertificateRequest.PublicKeyAlgorithm", Field, 3, ""}, {"CertificateRequest.Raw", Field, 3, ""}, + {"CertificateRequest.RawSignatureAlgorithm", Field, 27, ""}, {"CertificateRequest.RawSubject", Field, 3, ""}, {"CertificateRequest.RawSubjectPublicKeyInfo", Field, 3, ""}, {"CertificateRequest.RawTBSCertificateRequest", Field, 3, ""}, @@ -1422,6 +1466,10 @@ var PackageSymbols = map[string][]Symbol{ {"KeyUsageKeyEncipherment", Const, 0, ""}, {"MD2WithRSA", Const, 0, ""}, {"MD5WithRSA", Const, 0, ""}, + {"MLDSA", Const, 27, ""}, + {"MLDSA44", Const, 27, ""}, + {"MLDSA65", Const, 27, ""}, + {"MLDSA87", Const, 27, ""}, {"MarshalECPrivateKey", Func, 2, "func(key *ecdsa.PrivateKey) ([]byte, error)"}, {"MarshalPKCS1PrivateKey", Func, 0, "func(key *rsa.PrivateKey) []byte"}, {"MarshalPKCS1PublicKey", Func, 10, "func(key *rsa.PublicKey) []byte"}, @@ -1468,6 +1516,7 @@ var PackageSymbols = map[string][]Symbol{ {"RevocationList.Number", Field, 15, ""}, {"RevocationList.Raw", Field, 19, ""}, {"RevocationList.RawIssuer", Field, 19, ""}, + {"RevocationList.RawSignatureAlgorithm", Field, 27, ""}, {"RevocationList.RawTBSRevocationList", Field, 19, ""}, {"RevocationList.RevokedCertificateEntries", Field, 21, ""}, {"RevocationList.RevokedCertificates", Field, 15, ""}, @@ -1648,6 +1697,7 @@ var PackageSymbols = map[string][]Symbol{ {"(Scanner).Scan", Method, 0, ""}, {"ColumnType", Type, 8, ""}, {"Conn", Type, 9, ""}, + {"ConvertAssign", Func, 27, "func(scanCtx driver.ScanContext, dest any, src driver.Value) error"}, {"DB", Type, 0, ""}, {"DBStats", Type, 5, ""}, {"DBStats.Idle", Field, 11, ""}, @@ -1744,6 +1794,11 @@ var PackageSymbols = map[string][]Symbol{ {"(Rows).Next", Method, 0, ""}, {"(RowsAffected).LastInsertId", Method, 0, ""}, {"(RowsAffected).RowsAffected", Method, 0, ""}, + {"(RowsColumnScanner).Close", Method, 27, ""}, + {"(RowsColumnScanner).Columns", Method, 27, ""}, + {"(RowsColumnScanner).Next", Method, 27, ""}, + {"(RowsColumnScanner).NextRow", Method, 27, ""}, + {"(RowsColumnScanner).ScanColumn", Method, 27, ""}, {"(RowsColumnTypeDatabaseTypeName).Close", Method, 8, ""}, {"(RowsColumnTypeDatabaseTypeName).ColumnTypeDatabaseTypeName", Method, 8, ""}, {"(RowsColumnTypeDatabaseTypeName).Columns", Method, 8, ""}, @@ -1815,12 +1870,14 @@ var PackageSymbols = map[string][]Symbol{ {"ResultNoRows", Var, 0, ""}, {"Rows", Type, 0, ""}, {"RowsAffected", Type, 0, ""}, + {"RowsColumnScanner", Type, 27, ""}, {"RowsColumnTypeDatabaseTypeName", Type, 8, ""}, {"RowsColumnTypeLength", Type, 8, ""}, {"RowsColumnTypeNullable", Type, 8, ""}, {"RowsColumnTypePrecisionScale", Type, 8, ""}, {"RowsColumnTypeScanType", Type, 8, ""}, {"RowsNextResultSet", Type, 8, ""}, + {"ScanContext", Type, 27, ""}, {"SessionResetter", Type, 10, ""}, {"Stmt", Type, 0, ""}, {"StmtExecContext", Type, 8, ""}, @@ -5038,24 +5095,32 @@ var PackageSymbols = map[string][]Symbol{ {"(*InvalidUnmarshalError).Error", Method, 0, ""}, {"(*MarshalerError).Error", Method, 0, ""}, {"(*MarshalerError).Unwrap", Method, 13, ""}, + {"(*Number).UnmarshalJSONFrom", Method, 27, ""}, {"(*RawMessage).MarshalJSON", Method, 0, ""}, {"(*RawMessage).UnmarshalJSON", Method, 0, ""}, {"(*SyntaxError).Error", Method, 0, ""}, {"(*UnmarshalFieldError).Error", Method, 0, ""}, {"(*UnmarshalTypeError).Error", Method, 0, ""}, + {"(*UnmarshalTypeError).Unwrap", Method, 27, ""}, {"(*UnsupportedTypeError).Error", Method, 0, ""}, {"(*UnsupportedValueError).Error", Method, 0, ""}, {"(Delim).String", Method, 5, ""}, {"(Marshaler).MarshalJSON", Method, 0, ""}, {"(Number).Float64", Method, 1, ""}, {"(Number).Int64", Method, 1, ""}, + {"(Number).MarshalJSONTo", Method, 27, ""}, {"(Number).String", Method, 1, ""}, {"(RawMessage).MarshalJSON", Method, 8, ""}, {"(Unmarshaler).UnmarshalJSON", Method, 0, ""}, + {"CallMethodsWithLegacySemantics", Func, 27, "func(v bool) Options"}, {"Compact", Func, 0, "func(dst *bytes.Buffer, src []byte) error"}, {"Decoder", Type, 0, ""}, + {"DefaultOptionsV1", Func, 27, "func() Options"}, {"Delim", Type, 5, ""}, {"Encoder", Type, 0, ""}, + {"FormatByteArrayAsArray", Func, 27, "func(v bool) Options"}, + {"FormatBytesWithLegacySemantics", Func, 27, "func(v bool) Options"}, + {"FormatDurationAsNano", Func, 27, "func(v bool) Options"}, {"HTMLEscape", Func, 0, "func(dst *bytes.Buffer, src []byte)"}, {"Indent", Func, 0, "func(dst *bytes.Buffer, src []byte, prefix string, indent string) error"}, {"InvalidUTF8Error", Type, 0, ""}, @@ -5068,19 +5133,29 @@ var PackageSymbols = map[string][]Symbol{ {"MarshalerError", Type, 0, ""}, {"MarshalerError.Err", Field, 0, ""}, {"MarshalerError.Type", Field, 0, ""}, + {"MatchCaseSensitiveDelimiter", Func, 27, "func(v bool) Options"}, + {"MergeWithLegacySemantics", Func, 27, "func(v bool) Options"}, {"NewDecoder", Func, 0, "func(r io.Reader) *Decoder"}, {"NewEncoder", Func, 0, "func(w io.Writer) *Encoder"}, {"Number", Type, 1, ""}, + {"OmitEmptyWithLegacySemantics", Func, 27, "func(v bool) Options"}, + {"Options", Type, 27, ""}, + {"ParseBytesWithLooseRFC4648", Func, 27, "func(v bool) Options"}, + {"ParseTimeWithLooseRFC3339", Func, 27, "func(v bool) Options"}, {"RawMessage", Type, 0, ""}, + {"ReportErrorsWithLegacySemantics", Func, 27, "func(v bool) Options"}, + {"StringifyWithLegacySemantics", Func, 27, "func(v bool) Options"}, {"SyntaxError", Type, 0, ""}, {"SyntaxError.Offset", Field, 0, ""}, {"Token", Type, 5, ""}, {"Unmarshal", Func, 0, "func(data []byte, v any) error"}, + {"UnmarshalArrayFromAnyLength", Func, 27, "func(v bool) Options"}, {"UnmarshalFieldError", Type, 0, ""}, {"UnmarshalFieldError.Field", Field, 0, ""}, {"UnmarshalFieldError.Key", Field, 0, ""}, {"UnmarshalFieldError.Type", Field, 0, ""}, {"UnmarshalTypeError", Type, 0, ""}, + {"UnmarshalTypeError.Err", Field, 27, ""}, {"UnmarshalTypeError.Field", Field, 8, ""}, {"UnmarshalTypeError.Offset", Field, 5, ""}, {"UnmarshalTypeError.Struct", Field, 8, ""}, @@ -5094,6 +5169,158 @@ var PackageSymbols = map[string][]Symbol{ {"UnsupportedValueError.Value", Field, 0, ""}, {"Valid", Func, 9, "func(data []byte) bool"}, }, + "encoding/json/jsontext": { + {"(*Decoder).InputOffset", Method, 27, ""}, + {"(*Decoder).Options", Method, 27, ""}, + {"(*Decoder).PeekKind", Method, 27, ""}, + {"(*Decoder).ReadToken", Method, 27, ""}, + {"(*Decoder).ReadValue", Method, 27, ""}, + {"(*Decoder).Reset", Method, 27, ""}, + {"(*Decoder).SkipValue", Method, 27, ""}, + {"(*Decoder).StackDepth", Method, 27, ""}, + {"(*Decoder).StackIndex", Method, 27, ""}, + {"(*Decoder).StackPointer", Method, 27, ""}, + {"(*Decoder).UnreadBuffer", Method, 27, ""}, + {"(*Encoder).AvailableBuffer", Method, 27, ""}, + {"(*Encoder).Options", Method, 27, ""}, + {"(*Encoder).OutputOffset", Method, 27, ""}, + {"(*Encoder).Reset", Method, 27, ""}, + {"(*Encoder).StackDepth", Method, 27, ""}, + {"(*Encoder).StackIndex", Method, 27, ""}, + {"(*Encoder).StackPointer", Method, 27, ""}, + {"(*Encoder).WriteToken", Method, 27, ""}, + {"(*Encoder).WriteValue", Method, 27, ""}, + {"(*SyntacticError).Error", Method, 27, ""}, + {"(*SyntacticError).Unwrap", Method, 27, ""}, + {"(*Value).Canonicalize", Method, 27, ""}, + {"(*Value).Compact", Method, 27, ""}, + {"(*Value).Format", Method, 27, ""}, + {"(*Value).Indent", Method, 27, ""}, + {"(*Value).UnmarshalJSON", Method, 27, ""}, + {"(Kind).String", Method, 27, ""}, + {"(Pointer).AppendToken", Method, 27, ""}, + {"(Pointer).Contains", Method, 27, ""}, + {"(Pointer).IsValid", Method, 27, ""}, + {"(Pointer).LastToken", Method, 27, ""}, + {"(Pointer).Parent", Method, 27, ""}, + {"(Pointer).Tokens", Method, 27, ""}, + {"(Token).Bool", Method, 27, ""}, + {"(Token).Clone", Method, 27, ""}, + {"(Token).Float", Method, 27, ""}, + {"(Token).Float32", Method, 27, ""}, + {"(Token).Int", Method, 27, ""}, + {"(Token).Kind", Method, 27, ""}, + {"(Token).String", Method, 27, ""}, + {"(Token).Uint", Method, 27, ""}, + {"(Value).Clone", Method, 27, ""}, + {"(Value).IsValid", Method, 27, ""}, + {"(Value).Kind", Method, 27, ""}, + {"(Value).MarshalJSON", Method, 27, ""}, + {"(Value).String", Method, 27, ""}, + {"AllowDuplicateNames", Func, 27, "func(v bool) Options"}, + {"AllowInvalidUTF8", Func, 27, "func(v bool) Options"}, + {"AppendFloat", Func, 27, "func(dst []byte, src float64, bits int) []byte"}, + {"AppendFormat", Func, 27, "func[Bytes ~[]byte | ~string](dst []byte, src Bytes, opts ...Options) ([]byte, error)"}, + {"AppendQuote", Func, 27, "func[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error)"}, + {"AppendUnquote", Func, 27, "func[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error)"}, + {"BeginArray", Var, 27, ""}, + {"BeginObject", Var, 27, ""}, + {"Bool", Func, 27, "func(b bool) Token"}, + {"CanonicalizeRawFloats", Func, 27, "func(v bool) Options"}, + {"CanonicalizeRawInts", Func, 27, "func(v bool) Options"}, + {"Decoder", Type, 27, ""}, + {"Encoder", Type, 27, ""}, + {"EndArray", Var, 27, ""}, + {"EndObject", Var, 27, ""}, + {"ErrDuplicateName", Var, 27, ""}, + {"ErrNonStringName", Var, 27, ""}, + {"EscapeForHTML", Func, 27, "func(v bool) Options"}, + {"EscapeForJS", Func, 27, "func(v bool) Options"}, + {"False", Var, 27, ""}, + {"Float", Func, 27, "func(n float64) Token"}, + {"Float32", Func, 27, "func(n float32) Token"}, + {"Int", Func, 27, "func(n int64) Token"}, + {"Internal", Var, 27, ""}, + {"Kind", Type, 27, ""}, + {"KindBeginArray", Const, 27, ""}, + {"KindBeginObject", Const, 27, ""}, + {"KindEndArray", Const, 27, ""}, + {"KindEndObject", Const, 27, ""}, + {"KindFalse", Const, 27, ""}, + {"KindInvalid", Const, 27, ""}, + {"KindNull", Const, 27, ""}, + {"KindNumber", Const, 27, ""}, + {"KindString", Const, 27, ""}, + {"KindTrue", Const, 27, ""}, + {"Multiline", Func, 27, "func(v bool) Options"}, + {"NewDecoder", Func, 27, "func(r io.Reader, opts ...Options) *Decoder"}, + {"NewEncoder", Func, 27, "func(w io.Writer, opts ...Options) *Encoder"}, + {"Null", Var, 27, ""}, + {"Options", Type, 27, ""}, + {"Pointer", Type, 27, ""}, + {"PreserveRawStrings", Func, 27, "func(v bool) Options"}, + {"ReorderRawObjects", Func, 27, "func(v bool) Options"}, + {"SpaceAfterColon", Func, 27, "func(v bool) Options"}, + {"SpaceAfterComma", Func, 27, "func(v bool) Options"}, + {"String", Func, 27, "func(s string) Token"}, + {"SyntacticError", Type, 27, ""}, + {"SyntacticError.ByteOffset", Field, 27, ""}, + {"SyntacticError.Err", Field, 27, ""}, + {"SyntacticError.JSONPointer", Field, 27, ""}, + {"Token", Type, 27, ""}, + {"True", Var, 27, ""}, + {"Uint", Func, 27, "func(n uint64) Token"}, + {"Value", Type, 27, ""}, + {"WithIndent", Func, 27, "func(indent string) Options"}, + {"WithIndentPrefix", Func, 27, "func(prefix string) Options"}, + }, + "encoding/json/v2": { + {"(*SemanticError).Error", Method, 27, ""}, + {"(*SemanticError).Unwrap", Method, 27, ""}, + {"(Marshaler).MarshalJSON", Method, 27, ""}, + {"(MarshalerTo).MarshalJSONTo", Method, 27, ""}, + {"(Unmarshaler).UnmarshalJSON", Method, 27, ""}, + {"(UnmarshalerFrom).UnmarshalJSONFrom", Method, 27, ""}, + {"DefaultOptionsV2", Func, 27, "func() Options"}, + {"Deterministic", Func, 27, "func(v bool) Options"}, + {"ErrUnknownName", Var, 27, ""}, + {"FormatNilMapAsNull", Func, 27, "func(v bool) Options"}, + {"FormatNilSliceAsNull", Func, 27, "func(v bool) Options"}, + {"GetOption", Func, 27, "func[T any](opts Options, setter func(T) Options) (T, bool)"}, + {"JoinMarshalers", Func, 27, "func(ms ...*Marshalers) *Marshalers"}, + {"JoinOptions", Func, 27, "func(srcs ...Options) Options"}, + {"JoinUnmarshalers", Func, 27, "func(us ...*Unmarshalers) *Unmarshalers"}, + {"Marshal", Func, 27, "func(in any, opts ...Options) (out []byte, err error)"}, + {"MarshalEncode", Func, 27, "func(out *jsontext.Encoder, in any, opts ...Options) (err error)"}, + {"MarshalFunc", Func, 27, "func[T any](fn func(T) ([]byte, error)) *Marshalers"}, + {"MarshalToFunc", Func, 27, "func[T any](fn func(*jsontext.Encoder, T) error) *Marshalers"}, + {"MarshalWrite", Func, 27, "func(out io.Writer, in any, opts ...Options) (err error)"}, + {"Marshaler", Type, 27, ""}, + {"MarshalerTo", Type, 27, ""}, + {"Marshalers", Type, 27, ""}, + {"MatchCaseInsensitiveNames", Func, 27, "func(v bool) Options"}, + {"OmitZeroStructFields", Func, 27, "func(v bool) Options"}, + {"Options", Type, 27, ""}, + {"RejectUnknownMembers", Func, 27, "func(v bool) Options"}, + {"SemanticError", Type, 27, ""}, + {"SemanticError.ByteOffset", Field, 27, ""}, + {"SemanticError.Err", Field, 27, ""}, + {"SemanticError.GoType", Field, 27, ""}, + {"SemanticError.JSONKind", Field, 27, ""}, + {"SemanticError.JSONPointer", Field, 27, ""}, + {"SemanticError.JSONValue", Field, 27, ""}, + {"StringifyNumbers", Func, 27, "func(v bool) Options"}, + {"Unmarshal", Func, 27, "func(in []byte, out any, opts ...Options) (err error)"}, + {"UnmarshalDecode", Func, 27, "func(in *jsontext.Decoder, out any, opts ...Options) (err error)"}, + {"UnmarshalFromFunc", Func, 27, "func[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers"}, + {"UnmarshalFunc", Func, 27, "func[T any](fn func([]byte, T) error) *Unmarshalers"}, + {"UnmarshalRead", Func, 27, "func(in io.Reader, out any, opts ...Options) (err error)"}, + {"Unmarshaler", Type, 27, ""}, + {"UnmarshalerFrom", Type, 27, ""}, + {"Unmarshalers", Type, 27, ""}, + {"WithMarshalers", Func, 27, "func(v *Marshalers) Options"}, + {"WithUnmarshalers", Func, 27, "func(v *Unmarshalers) Options"}, + }, "encoding/pem": { {"Block", Type, 0, ""}, {"Block.Bytes", Field, 0, ""}, @@ -6002,6 +6229,7 @@ var PackageSymbols = map[string][]Symbol{ {"Shift", Func, 5, "func(x Value, op token.Token, s uint) Value"}, {"Sign", Func, 5, "func(x Value) int"}, {"String", Const, 5, ""}, + {"StringLen", Func, 27, "func(x Value) int64"}, {"StringVal", Func, 5, "func(x Value) string"}, {"ToComplex", Func, 6, "func(x Value) Value"}, {"ToFloat", Func, 6, "func(x Value) Value"}, @@ -6183,6 +6411,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*ErrorList).Add", Method, 0, ""}, {"(*ErrorList).RemoveMultiples", Method, 0, ""}, {"(*ErrorList).Reset", Method, 0, ""}, + {"(*Scanner).End", Method, 27, ""}, {"(*Scanner).Init", Method, 0, ""}, {"(*Scanner).Scan", Method, 0, ""}, {"(Error).Error", Method, 0, ""}, @@ -6222,6 +6451,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*File).SetLines", Method, 0, ""}, {"(*File).SetLinesForContent", Method, 0, ""}, {"(*File).Size", Method, 0, ""}, + {"(*File).String", Method, 27, ""}, {"(*FileSet).AddExistingFiles", Method, 25, ""}, {"(*FileSet).AddFile", Method, 0, ""}, {"(*FileSet).Base", Method, 0, ""}, @@ -6529,6 +6759,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Tuple).Variables", Method, 24, ""}, {"(*TypeList).At", Method, 18, ""}, {"(*TypeList).Len", Method, 18, ""}, + {"(*TypeList).String", Method, 27, ""}, {"(*TypeList).Types", Method, 24, ""}, {"(*TypeName).Exported", Method, 5, ""}, {"(*TypeName).Id", Method, 5, ""}, @@ -6547,6 +6778,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*TypeParam).Underlying", Method, 18, ""}, {"(*TypeParamList).At", Method, 18, ""}, {"(*TypeParamList).Len", Method, 18, ""}, + {"(*TypeParamList).String", Method, 27, ""}, {"(*TypeParamList).TypeParams", Method, 24, ""}, {"(*Union).Len", Method, 18, ""}, {"(*Union).String", Method, 18, ""}, @@ -6571,9 +6803,14 @@ var PackageSymbols = map[string][]Symbol{ {"(Checker).PkgNameOf", Method, 22, ""}, {"(Checker).TypeOf", Method, 5, ""}, {"(Error).Error", Method, 5, ""}, + {"(Hasher).Equal", Method, 27, ""}, + {"(Hasher).Hash", Method, 27, ""}, + {"(HasherIgnoreTags).Equal", Method, 27, ""}, + {"(HasherIgnoreTags).Hash", Method, 27, ""}, {"(Importer).Import", Method, 5, ""}, {"(ImporterFrom).Import", Method, 6, ""}, {"(ImporterFrom).ImportFrom", Method, 6, ""}, + {"(Instance).String", Method, 27, ""}, {"(Object).Exported", Method, 5, ""}, {"(Object).Id", Method, 5, ""}, {"(Object).Name", Method, 5, ""}, @@ -6643,6 +6880,8 @@ var PackageSymbols = map[string][]Symbol{ {"Float32", Const, 5, ""}, {"Float64", Const, 5, ""}, {"Func", Type, 5, ""}, + {"Hasher", Type, 27, ""}, + {"HasherIgnoreTags", Type, 27, ""}, {"Id", Func, 5, "func(pkg *Package, name string) string"}, {"Identical", Func, 5, "func(x Type, y Type) bool"}, {"IdenticalIgnoreTags", Func, 8, "func(x Type, y Type) bool"}, @@ -6877,9 +7116,13 @@ var PackageSymbols = map[string][]Symbol{ {"(*Hash).Write", Method, 14, ""}, {"(*Hash).WriteByte", Method, 14, ""}, {"(*Hash).WriteString", Method, 14, ""}, + {"(ComparableHasher).Equal", Method, 27, ""}, + {"(ComparableHasher).Hash", Method, 27, ""}, {"Bytes", Func, 19, "func(seed Seed, b []byte) uint64"}, {"Comparable", Func, 24, "func[T comparable](seed Seed, v T) uint64"}, + {"ComparableHasher", Type, 27, ""}, {"Hash", Type, 14, ""}, + {"Hasher", Type, 27, ""}, {"MakeSeed", Func, 14, "func() Seed"}, {"Seed", Type, 14, ""}, {"String", Func, 19, "func(seed Seed, s string) uint64"}, @@ -8035,6 +8278,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Int).CmpAbs", Method, 10, ""}, {"(*Int).Div", Method, 0, ""}, {"(*Int).DivMod", Method, 0, ""}, + {"(*Int).Divide", Method, 27, ""}, {"(*Int).Exp", Method, 0, ""}, {"(*Int).FillBytes", Method, 15, ""}, {"(*Int).Float64", Method, 21, ""}, @@ -8119,9 +8363,11 @@ var PackageSymbols = map[string][]Symbol{ {"Accuracy", Type, 5, ""}, {"AwayFromZero", Const, 5, ""}, {"Below", Const, 5, ""}, + {"Ceil", Const, 27, ""}, {"ErrNaN", Type, 5, ""}, {"Exact", Const, 5, ""}, {"Float", Type, 5, ""}, + {"Floor", Const, 27, ""}, {"Int", Type, 0, ""}, {"Jacobi", Func, 5, "func(x *Int, y *Int) int"}, {"MaxBase", Const, 0, ""}, @@ -8133,12 +8379,14 @@ var PackageSymbols = map[string][]Symbol{ {"NewRat", Func, 0, "func(a int64, b int64) *Rat"}, {"ParseFloat", Func, 5, "func(s string, base int, prec uint, mode RoundingMode) (f *Float, b int, err error)"}, {"Rat", Type, 0, ""}, + {"Round", Const, 27, ""}, {"RoundingMode", Type, 5, ""}, {"ToNearestAway", Const, 5, ""}, {"ToNearestEven", Const, 5, ""}, {"ToNegativeInf", Const, 5, ""}, {"ToPositiveInf", Const, 5, ""}, {"ToZero", Const, 5, ""}, + {"Trunc", Const, 27, ""}, {"Word", Type, 0, ""}, }, "math/bits": { @@ -8290,6 +8538,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Rand).Int64", Method, 22, ""}, {"(*Rand).Int64N", Method, 22, ""}, {"(*Rand).IntN", Method, 22, ""}, + {"(*Rand).N", Method, 27, ""}, {"(*Rand).NormFloat64", Method, 22, ""}, {"(*Rand).Perm", Method, 22, ""}, {"(*Rand).Shuffle", Method, 22, ""}, @@ -8985,7 +9234,7 @@ var PackageSymbols = map[string][]Symbol{ {"NoBody", Var, 8, ""}, {"NotFound", Func, 0, "func(w ResponseWriter, r *Request)"}, {"NotFoundHandler", Func, 0, "func() Handler"}, - {"ParseCookie", Func, 23, "func(line string) ([]*Cookie, error)"}, + {"ParseCookie", Func, 23, "func(line string) (#rv1 []*Cookie, #rv2 error)"}, {"ParseHTTPVersion", Func, 0, "func(vers string) (major int, minor int, ok bool)"}, {"ParseSetCookie", Func, 23, "func(line string) (*Cookie, error)"}, {"ParseTime", Func, 1, "func(text string) (t time.Time, err error)"}, @@ -9061,6 +9310,7 @@ var PackageSymbols = map[string][]Symbol{ {"Server.BaseContext", Field, 13, ""}, {"Server.ConnContext", Field, 13, ""}, {"Server.ConnState", Field, 3, ""}, + {"Server.DisableClientPriority", Field, 27, ""}, {"Server.DisableGeneralOptionsHandler", Field, 20, ""}, {"Server.ErrorLog", Field, 3, ""}, {"Server.HTTP2", Field, 24, ""}, @@ -9226,6 +9476,7 @@ var PackageSymbols = map[string][]Symbol{ {"NewRequestWithContext", Func, 23, "func(ctx context.Context, method string, target string, body io.Reader) *http.Request"}, {"NewServer", Func, 0, "func(handler http.Handler) *Server"}, {"NewTLSServer", Func, 0, "func(handler http.Handler) *Server"}, + {"NewTestServer", Func, 27, "func(t testing.TB, handler http.Handler) *Server"}, {"NewUnstartedServer", Func, 0, "func(handler http.Handler) *Server"}, {"ResponseRecorder", Type, 0, ""}, {"ResponseRecorder.Body", Field, 0, ""}, @@ -9596,6 +9847,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Error).Timeout", Method, 6, ""}, {"(*Error).Unwrap", Method, 13, ""}, {"(*URL).AppendBinary", Method, 24, ""}, + {"(*URL).Clone", Method, 27, ""}, {"(*URL).EscapedFragment", Method, 15, ""}, {"(*URL).EscapedPath", Method, 5, ""}, {"(*URL).Hostname", Method, 8, ""}, @@ -9616,6 +9868,7 @@ var PackageSymbols = map[string][]Symbol{ {"(EscapeError).Error", Method, 0, ""}, {"(InvalidHostError).Error", Method, 6, ""}, {"(Values).Add", Method, 0, ""}, + {"(Values).Clone", Method, 27, ""}, {"(Values).Del", Method, 0, ""}, {"(Values).Encode", Method, 0, ""}, {"(Values).Get", Method, 0, ""}, @@ -10793,6 +11046,7 @@ var PackageSymbols = map[string][]Symbol{ {"ContainsRune", Func, 0, "func(s string, r rune) bool"}, {"Count", Func, 0, "func(s string, substr string) int"}, {"Cut", Func, 18, "func(s string, sep string) (before string, after string, found bool)"}, + {"CutLast", Func, 27, "func(s string, sep string) (before string, after string, found bool)"}, {"CutPrefix", Func, 20, "func(s string, prefix string) (after string, found bool)"}, {"CutSuffix", Func, 20, "func(s string, suffix string) (before string, found bool)"}, {"EqualFold", Func, 0, "func(s string, t string) bool"}, @@ -17476,6 +17730,7 @@ var PackageSymbols = map[string][]Symbol{ {"TestHandler", Func, 21, "func(h slog.Handler, results func() []map[string]any) error"}, }, "testing/synctest": { + {"Sleep", Func, 27, "func(d time.Duration)"}, {"Test", Func, 25, "func(t *testing.T, f func(*testing.T))"}, {"Wait", Func, 25, "func()"}, }, @@ -17980,6 +18235,7 @@ var PackageSymbols = map[string][]Symbol{ {"Bassa_Vah", Var, 4, ""}, {"Batak", Var, 0, ""}, {"Bengali", Var, 0, ""}, + {"Beria_Erfe", Var, 27, ""}, {"Bhaiksuki", Var, 7, ""}, {"Bidi_Control", Var, 0, ""}, {"Bopomofo", Var, 0, ""}, @@ -18029,6 +18285,7 @@ var PackageSymbols = map[string][]Symbol{ {"Extender", Var, 0, ""}, {"FoldCategory", Var, 0, ""}, {"FoldScript", Var, 0, ""}, + {"Garay", Var, 27, ""}, {"Georgian", Var, 0, ""}, {"Glagolitic", Var, 0, ""}, {"Gothic", Var, 0, ""}, @@ -18038,6 +18295,7 @@ var PackageSymbols = map[string][]Symbol{ {"Gujarati", Var, 0, ""}, {"Gunjala_Gondi", Var, 13, ""}, {"Gurmukhi", Var, 0, ""}, + {"Gurung_Khema", Var, 27, ""}, {"Han", Var, 0, ""}, {"Hangul", Var, 0, ""}, {"Hanifi_Rohingya", Var, 13, ""}, @@ -18049,6 +18307,9 @@ var PackageSymbols = map[string][]Symbol{ {"Hyphen", Var, 0, ""}, {"IDS_Binary_Operator", Var, 0, ""}, {"IDS_Trinary_Operator", Var, 0, ""}, + {"IDS_Unary_Operator", Var, 27, ""}, + {"ID_Compat_Math_Continue", Var, 27, ""}, + {"ID_Compat_Math_Start", Var, 27, ""}, {"Ideographic", Var, 0, ""}, {"Imperial_Aramaic", Var, 0, ""}, {"In", Func, 2, "func(r rune, ranges ...*RangeTable) bool"}, @@ -18082,6 +18343,7 @@ var PackageSymbols = map[string][]Symbol{ {"Khmer", Var, 0, ""}, {"Khojki", Var, 4, ""}, {"Khudawadi", Var, 4, ""}, + {"Kirat_Rai", Var, 27, ""}, {"L", Var, 0, ""}, {"LC", Var, 25, ""}, {"Lao", Var, 0, ""}, @@ -18125,6 +18387,7 @@ var PackageSymbols = map[string][]Symbol{ {"Miao", Var, 1, ""}, {"Mn", Var, 0, ""}, {"Modi", Var, 4, ""}, + {"Modifier_Combining_Mark", Var, 27, ""}, {"Mongolian", Var, 0, ""}, {"Mro", Var, 4, ""}, {"Multani", Var, 5, ""}, @@ -18145,6 +18408,7 @@ var PackageSymbols = map[string][]Symbol{ {"Nyiakeng_Puachue_Hmong", Var, 14, ""}, {"Ogham", Var, 0, ""}, {"Ol_Chiki", Var, 0, ""}, + {"Ol_Onal", Var, 27, ""}, {"Old_Hungarian", Var, 5, ""}, {"Old_Italic", Var, 0, ""}, {"Old_North_Arabian", Var, 4, ""}, @@ -18214,6 +18478,7 @@ var PackageSymbols = map[string][]Symbol{ {"Sharada", Var, 1, ""}, {"Shavian", Var, 0, ""}, {"Siddham", Var, 4, ""}, + {"Sidetic", Var, 27, ""}, {"SignWriting", Var, 5, ""}, {"SimpleFold", Func, 0, "func(r rune) rune"}, {"Sinhala", Var, 0, ""}, @@ -18227,6 +18492,7 @@ var PackageSymbols = map[string][]Symbol{ {"Space", Var, 0, ""}, {"SpecialCase", Type, 0, ""}, {"Sundanese", Var, 0, ""}, + {"Sunuwar", Var, 27, ""}, {"Syloti_Nagri", Var, 0, ""}, {"Symbol", Var, 0, ""}, {"Syriac", Var, 0, ""}, @@ -18235,6 +18501,7 @@ var PackageSymbols = map[string][]Symbol{ {"Tai_Le", Var, 0, ""}, {"Tai_Tham", Var, 0, ""}, {"Tai_Viet", Var, 0, ""}, + {"Tai_Yo", Var, 27, ""}, {"Takri", Var, 1, ""}, {"Tamil", Var, 0, ""}, {"Tangsa", Var, 21, ""}, @@ -18252,7 +18519,10 @@ var PackageSymbols = map[string][]Symbol{ {"ToLower", Func, 0, "func(r rune) rune"}, {"ToTitle", Func, 0, "func(r rune) rune"}, {"ToUpper", Func, 0, "func(r rune) rune"}, + {"Todhri", Var, 27, ""}, + {"Tolong_Siki", Var, 27, ""}, {"Toto", Var, 21, ""}, + {"Tulu_Tigalari", Var, 27, ""}, {"TurkishCase", Var, 0, ""}, {"Ugaritic", Var, 0, ""}, {"Unified_Ideograph", Var, 0, ""}, @@ -18320,6 +18590,21 @@ var PackageSymbols = map[string][]Symbol{ {"String", Func, 0, ""}, {"StringData", Func, 0, ""}, }, + "uuid": { + {"(*UUID).UnmarshalText", Method, 27, ""}, + {"(UUID).AppendText", Method, 27, ""}, + {"(UUID).Compare", Method, 27, ""}, + {"(UUID).MarshalText", Method, 27, ""}, + {"(UUID).String", Method, 27, ""}, + {"Max", Func, 27, "func() UUID"}, + {"MustParse", Func, 27, "func(s string) UUID"}, + {"New", Func, 27, "func() UUID"}, + {"NewV4", Func, 27, "func() UUID"}, + {"NewV7", Func, 27, "func() UUID"}, + {"Nil", Func, 27, "func() UUID"}, + {"Parse", Func, 27, "func(s string) (UUID, error)"}, + {"UUID", Type, 27, ""}, + }, "weak": { {"(Pointer).Value", Method, 24, ""}, {"Make", Func, 24, "func[T any](ptr *T) Pointer[T]"}, diff --git a/vendor/golang.org/x/tools/internal/typesinternal/element.go b/vendor/golang.org/x/tools/internal/typesinternal/element.go index 5fe4d8abc..89eeea165 100644 --- a/vendor/golang.org/x/tools/internal/typesinternal/element.go +++ b/vendor/golang.org/x/tools/internal/typesinternal/element.go @@ -37,6 +37,10 @@ func ForEachElement(rtypes *typeutil.Map, msets *typeutil.MethodSetCache, T type tmset := msets.MethodSet(T) for method := range tmset.Methods() { sig := method.Type().(*types.Signature) + if sig.TypeParams() != nil { + continue // skip type-parameterized methods + } + // It is tempting to call visit(sig, false) // but, as noted in golang.org/cl/65450043, // the Signature.Recv field is ignored by @@ -123,10 +127,10 @@ func ForEachElement(rtypes *typeutil.Map, msets *typeutil.MethodSetCache, T type case *types.TypeParam, *types.Union: // forEachReachable must not be called on parameterized types. - panic(T) + panic(fmt.Sprintf("ForEachElement called on type containing %T", T)) default: - panic(T) + panic(fmt.Sprintf("ForEachElement called on unexpected type %T", T)) } } visit(T, false) diff --git a/vendor/golang.org/x/tools/internal/typesinternal/types.go b/vendor/golang.org/x/tools/internal/typesinternal/types.go index 6582cc81f..d2c0b4c5f 100644 --- a/vendor/golang.org/x/tools/internal/typesinternal/types.go +++ b/vendor/golang.org/x/tools/internal/typesinternal/types.go @@ -22,6 +22,7 @@ import ( "go/ast" "go/token" "go/types" + "iter" "reflect" "golang.org/x/tools/go/ast/inspector" @@ -242,3 +243,30 @@ func ObjectKind(obj types.Object) string { } return "unknown symbol" } + +// ImplicitFieldSelections returns the sequence of implicit embedded fields +// traversed by the given selection. It skips the final leaf field or method. +// The boolean component indicates whether the traversal traversed a pointer. +func ImplicitFieldSelections(seln types.Selection) iter.Seq2[*types.Var, bool] { + return func(yield func(*types.Var, bool) bool) { + var ( + t = seln.Recv() + indices = seln.Index() + ) + for _, idx := range indices[:len(indices)-1] { + ptr, isPtr := t.Underlying().(*types.Pointer) + if isPtr { + t = ptr.Elem() + } + structType, ok := t.Underlying().(*types.Struct) + if !ok { + break + } + field := structType.Field(idx) + if !yield(field, isPtr) { + break + } + t = field.Type() + } + } +} diff --git a/vendor/golang.org/x/tools/internal/typesinternal/zerovalue.go b/vendor/golang.org/x/tools/internal/typesinternal/zerovalue.go index d612a7102..706ad33ef 100644 --- a/vendor/golang.org/x/tools/internal/typesinternal/zerovalue.go +++ b/vendor/golang.org/x/tools/internal/typesinternal/zerovalue.go @@ -259,13 +259,13 @@ func TypeExpr(t types.Type, qual types.Qualifier) ast.Expr { case *types.Signature: var params []*ast.Field for v := range t.Params().Variables() { + var names []*ast.Ident + if v.Name() != "" { + names = []*ast.Ident{ast.NewIdent(v.Name())} + } params = append(params, &ast.Field{ - Type: TypeExpr(v.Type(), qual), - Names: []*ast.Ident{ - { - Name: v.Name(), - }, - }, + Type: TypeExpr(v.Type(), qual), + Names: names, }) } if t.Variadic() { @@ -328,10 +328,10 @@ func TypeExpr(t types.Type, qual types.Qualifier) ast.Expr { return expr case *types.Struct: - return ast.NewIdent(t.String()) + return ast.NewIdent(types.TypeString(t, qual)) case *types.Interface: - return ast.NewIdent(t.String()) + return ast.NewIdent(types.TypeString(t, qual)) case *types.Union: if t.Len() == 0 { diff --git a/vendor/modules.txt b/vendor/modules.txt index 0f571cb45..9c4fb9244 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1842,7 +1842,7 @@ golang.org/x/crypto/ssh/internal/bcrypt_pbkdf golang.org/x/exp/constraints golang.org/x/exp/maps golang.org/x/exp/slices -# golang.org/x/mod v0.36.0 +# golang.org/x/mod v0.37.0 ## explicit; go 1.25.0 golang.org/x/mod/internal/lazyregexp golang.org/x/mod/module @@ -1888,7 +1888,7 @@ golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows golang.org/x/sys/windows/registry -# golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 +# golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 ## explicit; go 1.25.0 golang.org/x/telemetry/counter golang.org/x/telemetry/internal/counter @@ -1897,7 +1897,7 @@ golang.org/x/telemetry/internal/telemetry # golang.org/x/term v0.44.0 ## explicit; go 1.25.0 golang.org/x/term -# golang.org/x/text v0.38.0 +# golang.org/x/text v0.39.0 ## explicit; go 1.25.0 golang.org/x/text/cases golang.org/x/text/encoding @@ -1932,7 +1932,7 @@ golang.org/x/text/width # golang.org/x/time v0.15.0 ## explicit; go 1.25.0 golang.org/x/time/rate -# golang.org/x/tools v0.45.0 +# golang.org/x/tools v0.47.0 ## explicit; go 1.25.0 golang.org/x/tools/cmd/goimports golang.org/x/tools/cmd/stringer From 2ad8f852da56202fe135f2adbbc537a0c559c274 Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Thu, 30 Jul 2026 13:47:12 +0200 Subject: [PATCH 03/11] =?UTF-8?q?Fix=20GO-2026-6061:=20upgrade=20google.go?= =?UTF-8?q?lang.org/grpc=20v1.81.1=20=E2=86=92=20v1.82.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- .../filters/http/gcp_authn/v3/gcp_authn.pb.go | 371 ++++++++++ .../gcp_authn/v3/gcp_authn.pb.validate.go | 649 ++++++++++++++++++ .../http/gcp_authn/v3/gcp_authn_vtproto.pb.go | 358 ++++++++++ .../grpc/balancer/balancer.go | 4 +- .../grpc_lb_v1/load_balancer_grpc.pb.go | 2 +- .../grpc/balancer/pickfirst/pickfirst.go | 2 +- .../grpc/balancer/ringhash/ringhash.go | 2 +- .../grpc/balancer/rls/control_channel.go | 124 ++-- .../grpc/credentials/alts/alts.go | 23 +- .../proto/grpc_gcp/handshaker_grpc.pb.go | 2 +- vendor/google.golang.org/grpc/dialoptions.go | 22 +- .../grpc/encoding/encoding.go | 3 + .../grpc/encoding/gzip/gzip.go | 14 +- .../balancer/hostname/hostname.go | 47 ++ .../balancer/weight/weight.go | 36 +- .../health/grpc_health_v1/health_grpc.pb.go | 2 +- .../grpc/internal/envconfig/envconfig.go | 105 ++- .../grpc/internal/envconfig/xds.go | 10 + .../grpc/internal/grpcutil/encode_duration.go | 1 - .../proto/grpc_lookup_v1/rls_grpc.pb.go | 2 +- .../grpc/internal/resolver/config_selector.go | 24 +- .../grpc/internal/stats/labels.go | 60 +- .../grpc/internal/transport/client_stream.go | 36 +- .../grpc/internal/transport/controlbuf.go | 215 +++--- .../grpc/internal/transport/flowcontrol.go | 10 +- .../grpc/internal/transport/handler_server.go | 4 +- .../grpc/internal/transport/http2_client.go | 58 +- .../grpc/internal/transport/http2_server.go | 29 +- .../internal/internal.go} | 20 +- .../grpc/internal/transport/transport.go | 5 + .../xds/balancer/cdsbalancer/configbuilder.go | 5 +- .../xds/balancer/clusterimpl/clusterimpl.go | 8 +- .../xds/balancer/clusterimpl/picker.go | 30 +- .../grpc/internal/xds/httpfilter/extconfig.go | 88 +++ .../internal/xds/httpfilter/httpfilter.go | 10 + .../internal/xds/matcher/matcher_header.go | 3 +- .../internal/xds/matcher/string_matcher.go | 18 +- .../grpc/internal/xds/rbac/matchers.go | 78 ++- .../internal/xds/resolver/serviceconfig.go | 24 +- .../internal/xds/resolver/xds_resolver.go | 48 +- .../xds/server/filter_chain_manager.go | 103 ++- .../internal/xds/server/listener_wrapper.go | 22 +- .../xds/xdsclient/xdsresource/grpc_service.go | 49 ++ .../xds/xdsclient/xdsresource/matcher_path.go | 4 +- .../xds/xdsclient/xdsresource/metadata.go | 32 +- .../xds/xdsclient/xdsresource/type_cds.go | 3 + .../xds/xdsclient/xdsresource/type_lds.go | 3 + .../xdsclient/xdsresource/unmarshal_cds.go | 9 + .../xdsclient/xdsresource/unmarshal_eds.go | 30 +- .../xdsclient/xdsresource/unmarshal_lds.go | 16 +- .../xdsclient/xdsresource/unmarshal_rds.go | 4 +- vendor/google.golang.org/grpc/rpc_util.go | 42 +- vendor/google.golang.org/grpc/server.go | 52 +- .../stats/opentelemetry/client_metrics.go | 30 +- .../stats/opentelemetry/client_tracing.go | 25 +- .../grpc/stats/opentelemetry/opentelemetry.go | 8 +- .../grpc/stats/opentelemetry/trace.go | 14 - vendor/google.golang.org/grpc/version.go | 2 +- vendor/modules.txt | 7 +- 61 files changed, 2470 insertions(+), 543 deletions(-) create mode 100644 vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.pb.go create mode 100644 vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.pb.validate.go create mode 100644 vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn_vtproto.pb.go create mode 100644 vendor/google.golang.org/grpc/experimental/balancer/hostname/hostname.go rename vendor/google.golang.org/grpc/{internal => experimental}/balancer/weight/weight.go (71%) rename vendor/google.golang.org/grpc/internal/{grpcutil/regex.go => transport/internal/internal.go} (59%) create mode 100644 vendor/google.golang.org/grpc/internal/xds/httpfilter/extconfig.go create mode 100644 vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/grpc_service.go diff --git a/go.mod b/go.mod index 70aeb8b19..17514252f 100644 --- a/go.mod +++ b/go.mod @@ -385,7 +385,7 @@ require ( gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/api v0.279.0 google.golang.org/genproto v0.0.0-20260622175928-b703f567277d // indirect - google.golang.org/grpc v1.81.1 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index c1c23d1fe..6a9abb4bc 100644 --- a/go.sum +++ b/go.sum @@ -1735,8 +1735,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.pb.go b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.pb.go new file mode 100644 index 000000000..5c5e7bbdd --- /dev/null +++ b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.pb.go @@ -0,0 +1,371 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v6.33.2 +// source: envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto + +package gcp_authnv3 + +import ( + _ "github.com/cncf/xds/go/udpa/annotations" + _ "github.com/envoyproxy/go-control-plane/envoy/annotations" + v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + _ "github.com/envoyproxy/protoc-gen-validate/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Filter configuration. +// [#next-free-field: 7] +type GcpAuthnFilterConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The HTTP URI to fetch tokens from GCE Metadata Server(https://cloud.google.com/compute/docs/metadata/overview). + // The URL format is "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=[AUDIENCE]" + // + // This field is deprecated because it does not match the API surface provided by the google auth libraries. + // Control planes should not attempt to override the metadata server URI. + // The cluster and timeout can be configured using the “cluster“ and “timeout“ fields instead. + // For backward compatibility, the cluster and timeout configured in this field will be used + // if the new “cluster“ and “timeout“ fields are not set. + // + // Deprecated: Marked as deprecated in envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto. + HttpUri *v3.HttpUri `protobuf:"bytes,1,opt,name=http_uri,json=httpUri,proto3" json:"http_uri,omitempty"` + // Retry policy for fetching tokens. + // Not supported by all data planes. + RetryPolicy *v3.RetryPolicy `protobuf:"bytes,2,opt,name=retry_policy,json=retryPolicy,proto3" json:"retry_policy,omitempty"` + // Token cache configuration. This field is optional. + CacheConfig *TokenCacheConfig `protobuf:"bytes,3,opt,name=cache_config,json=cacheConfig,proto3" json:"cache_config,omitempty"` + // Request header location to extract the token. By default (i.e. if this field is not specified), the token + // is extracted to the Authorization HTTP header, in the format "Authorization: Bearer ". + // Not supported by all data planes. + TokenHeader *TokenHeader `protobuf:"bytes,4,opt,name=token_header,json=tokenHeader,proto3" json:"token_header,omitempty"` + // Cluster to send traffic to the GCE metadata server. Not supported + // by all data planes; a data plane may instead have its own mechanism + // for contacting the metadata server. + Cluster string `protobuf:"bytes,5,opt,name=cluster,proto3" json:"cluster,omitempty"` + // Timeout for fetching the tokens from the GCE metadata server. + // Not supported by all data planes. + Timeout *durationpb.Duration `protobuf:"bytes,6,opt,name=timeout,proto3" json:"timeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GcpAuthnFilterConfig) Reset() { + *x = GcpAuthnFilterConfig{} + mi := &file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GcpAuthnFilterConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GcpAuthnFilterConfig) ProtoMessage() {} + +func (x *GcpAuthnFilterConfig) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GcpAuthnFilterConfig.ProtoReflect.Descriptor instead. +func (*GcpAuthnFilterConfig) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDescGZIP(), []int{0} +} + +// Deprecated: Marked as deprecated in envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto. +func (x *GcpAuthnFilterConfig) GetHttpUri() *v3.HttpUri { + if x != nil { + return x.HttpUri + } + return nil +} + +func (x *GcpAuthnFilterConfig) GetRetryPolicy() *v3.RetryPolicy { + if x != nil { + return x.RetryPolicy + } + return nil +} + +func (x *GcpAuthnFilterConfig) GetCacheConfig() *TokenCacheConfig { + if x != nil { + return x.CacheConfig + } + return nil +} + +func (x *GcpAuthnFilterConfig) GetTokenHeader() *TokenHeader { + if x != nil { + return x.TokenHeader + } + return nil +} + +func (x *GcpAuthnFilterConfig) GetCluster() string { + if x != nil { + return x.Cluster + } + return "" +} + +func (x *GcpAuthnFilterConfig) GetTimeout() *durationpb.Duration { + if x != nil { + return x.Timeout + } + return nil +} + +// Audience is the URL of the receiving service that performs token authentication. +// It will be provided to the filter through cluster's typed_filter_metadata. +type Audience struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Audience) Reset() { + *x = Audience{} + mi := &file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Audience) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Audience) ProtoMessage() {} + +func (x *Audience) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Audience.ProtoReflect.Descriptor instead. +func (*Audience) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDescGZIP(), []int{1} +} + +func (x *Audience) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +// Token Cache configuration. +type TokenCacheConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The number of cache entries. The maximum number of entries is INT64_MAX as it is constrained by underlying cache implementation. + // Default value 0 (i.e., proto3 defaults) disables the cache by default. Other default values will enable the cache. + CacheSize *wrapperspb.UInt64Value `protobuf:"bytes,1,opt,name=cache_size,json=cacheSize,proto3" json:"cache_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TokenCacheConfig) Reset() { + *x = TokenCacheConfig{} + mi := &file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TokenCacheConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TokenCacheConfig) ProtoMessage() {} + +func (x *TokenCacheConfig) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TokenCacheConfig.ProtoReflect.Descriptor instead. +func (*TokenCacheConfig) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDescGZIP(), []int{2} +} + +func (x *TokenCacheConfig) GetCacheSize() *wrapperspb.UInt64Value { + if x != nil { + return x.CacheSize + } + return nil +} + +type TokenHeader struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The HTTP header's name. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The header's prefix. The format is "value_prefix" + // For example, for "Authorization: Bearer ", value_prefix="Bearer " with a space at the + // end. + ValuePrefix string `protobuf:"bytes,2,opt,name=value_prefix,json=valuePrefix,proto3" json:"value_prefix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TokenHeader) Reset() { + *x = TokenHeader{} + mi := &file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TokenHeader) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TokenHeader) ProtoMessage() {} + +func (x *TokenHeader) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TokenHeader.ProtoReflect.Descriptor instead. +func (*TokenHeader) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDescGZIP(), []int{3} +} + +func (x *TokenHeader) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TokenHeader) GetValuePrefix() string { + if x != nil { + return x.ValuePrefix + } + return "" +} + +var File_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto protoreflect.FileDescriptor + +const file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDesc = "" + + "\n" + + ":envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto\x12*envoy.extensions.filters.http.gcp_authn.v3\x1a\x1fenvoy/config/core/v3/base.proto\x1a#envoy/config/core/v3/http_uri.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a#envoy/annotations/deprecation.proto\x1a\x1dudpa/annotations/status.proto\x1a\x17validate/validate.proto\"\xc1\x03\n" + + "\x14GcpAuthnFilterConfig\x12E\n" + + "\bhttp_uri\x18\x01 \x01(\v2\x1d.envoy.config.core.v3.HttpUriB\v\x92dž\xd8\x04\x033.0\x18\x01R\ahttpUri\x12D\n" + + "\fretry_policy\x18\x02 \x01(\v2!.envoy.config.core.v3.RetryPolicyR\vretryPolicy\x12_\n" + + "\fcache_config\x18\x03 \x01(\v2<.envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfigR\vcacheConfig\x12Z\n" + + "\ftoken_header\x18\x04 \x01(\v27.envoy.extensions.filters.http.gcp_authn.v3.TokenHeaderR\vtokenHeader\x12\x18\n" + + "\acluster\x18\x05 \x01(\tR\acluster\x12E\n" + + "\atimeout\x18\x06 \x01(\v2\x19.google.protobuf.DurationB\x10\xfaB\r\xaa\x01\n" + + "\x1a\x06\b\x80\x80\x80\x80\x102\x00R\atimeout\"%\n" + + "\bAudience\x12\x19\n" + + "\x03url\x18\x01 \x01(\tB\a\xfaB\x04r\x02\x10\x01R\x03url\"`\n" + + "\x10TokenCacheConfig\x12L\n" + + "\n" + + "cache_size\x18\x01 \x01(\v2\x1c.google.protobuf.UInt64ValueB\x0f\xfaB\f2\n" + + "\x18\xff\xff\xff\xff\xff\xff\xff\xff\x7fR\tcacheSize\"`\n" + + "\vTokenHeader\x12!\n" + + "\x04name\x18\x01 \x01(\tB\r\xfaB\n" + + "r\b\x10\x01\xc8\x01\x00\xc0\x01\x01R\x04name\x12.\n" + + "\fvalue_prefix\x18\x02 \x01(\tB\v\xfaB\br\x06\xc8\x01\x00\xc0\x01\x02R\vvaluePrefixB\xb2\x01\xba\x80\xc8\xd1\x06\x02\x10\x02\n" + + "8io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3B\rGcpAuthnProtoP\x01Z]github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3;gcp_authnv3b\x06proto3" + +var ( + file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDescOnce sync.Once + file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDescData []byte +) + +func file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDescGZIP() []byte { + file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDescOnce.Do(func() { + file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDesc), len(file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDesc))) + }) + return file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDescData +} + +var file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_goTypes = []any{ + (*GcpAuthnFilterConfig)(nil), // 0: envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig + (*Audience)(nil), // 1: envoy.extensions.filters.http.gcp_authn.v3.Audience + (*TokenCacheConfig)(nil), // 2: envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig + (*TokenHeader)(nil), // 3: envoy.extensions.filters.http.gcp_authn.v3.TokenHeader + (*v3.HttpUri)(nil), // 4: envoy.config.core.v3.HttpUri + (*v3.RetryPolicy)(nil), // 5: envoy.config.core.v3.RetryPolicy + (*durationpb.Duration)(nil), // 6: google.protobuf.Duration + (*wrapperspb.UInt64Value)(nil), // 7: google.protobuf.UInt64Value +} +var file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_depIdxs = []int32{ + 4, // 0: envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig.http_uri:type_name -> envoy.config.core.v3.HttpUri + 5, // 1: envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig.retry_policy:type_name -> envoy.config.core.v3.RetryPolicy + 2, // 2: envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig.cache_config:type_name -> envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig + 3, // 3: envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig.token_header:type_name -> envoy.extensions.filters.http.gcp_authn.v3.TokenHeader + 6, // 4: envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig.timeout:type_name -> google.protobuf.Duration + 7, // 5: envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig.cache_size:type_name -> google.protobuf.UInt64Value + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_init() } +func file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_init() { + if File_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDesc), len(file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_goTypes, + DependencyIndexes: file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_depIdxs, + MessageInfos: file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_msgTypes, + }.Build() + File_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto = out.File + file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_goTypes = nil + file_envoy_extensions_filters_http_gcp_authn_v3_gcp_authn_proto_depIdxs = nil +} diff --git a/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.pb.validate.go b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.pb.validate.go new file mode 100644 index 000000000..340cc2758 --- /dev/null +++ b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.pb.validate.go @@ -0,0 +1,649 @@ +//go:build !disable_pgv +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto + +package gcp_authnv3 + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// Validate checks the field values on GcpAuthnFilterConfig with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *GcpAuthnFilterConfig) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on GcpAuthnFilterConfig with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// GcpAuthnFilterConfigMultiError, or nil if none found. +func (m *GcpAuthnFilterConfig) ValidateAll() error { + return m.validate(true) +} + +func (m *GcpAuthnFilterConfig) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if all { + switch v := interface{}(m.GetHttpUri()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, GcpAuthnFilterConfigValidationError{ + field: "HttpUri", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, GcpAuthnFilterConfigValidationError{ + field: "HttpUri", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetHttpUri()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return GcpAuthnFilterConfigValidationError{ + field: "HttpUri", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetRetryPolicy()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, GcpAuthnFilterConfigValidationError{ + field: "RetryPolicy", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, GcpAuthnFilterConfigValidationError{ + field: "RetryPolicy", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetRetryPolicy()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return GcpAuthnFilterConfigValidationError{ + field: "RetryPolicy", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetCacheConfig()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, GcpAuthnFilterConfigValidationError{ + field: "CacheConfig", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, GcpAuthnFilterConfigValidationError{ + field: "CacheConfig", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetCacheConfig()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return GcpAuthnFilterConfigValidationError{ + field: "CacheConfig", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetTokenHeader()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, GcpAuthnFilterConfigValidationError{ + field: "TokenHeader", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, GcpAuthnFilterConfigValidationError{ + field: "TokenHeader", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetTokenHeader()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return GcpAuthnFilterConfigValidationError{ + field: "TokenHeader", + reason: "embedded message failed validation", + cause: err, + } + } + } + + // no validation rules for Cluster + + if d := m.GetTimeout(); d != nil { + dur, err := d.AsDuration(), d.CheckValid() + if err != nil { + err = GcpAuthnFilterConfigValidationError{ + field: "Timeout", + reason: "value is not a valid duration", + cause: err, + } + if !all { + return err + } + errors = append(errors, err) + } else { + + lt := time.Duration(4294967296*time.Second + 0*time.Nanosecond) + gte := time.Duration(0*time.Second + 0*time.Nanosecond) + + if dur < gte || dur >= lt { + err := GcpAuthnFilterConfigValidationError{ + field: "Timeout", + reason: "value must be inside range [0s, 1193046h28m16s)", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + } + + if len(errors) > 0 { + return GcpAuthnFilterConfigMultiError(errors) + } + + return nil +} + +// GcpAuthnFilterConfigMultiError is an error wrapping multiple validation +// errors returned by GcpAuthnFilterConfig.ValidateAll() if the designated +// constraints aren't met. +type GcpAuthnFilterConfigMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m GcpAuthnFilterConfigMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m GcpAuthnFilterConfigMultiError) AllErrors() []error { return m } + +// GcpAuthnFilterConfigValidationError is the validation error returned by +// GcpAuthnFilterConfig.Validate if the designated constraints aren't met. +type GcpAuthnFilterConfigValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e GcpAuthnFilterConfigValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e GcpAuthnFilterConfigValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e GcpAuthnFilterConfigValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e GcpAuthnFilterConfigValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e GcpAuthnFilterConfigValidationError) ErrorName() string { + return "GcpAuthnFilterConfigValidationError" +} + +// Error satisfies the builtin error interface +func (e GcpAuthnFilterConfigValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sGcpAuthnFilterConfig.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = GcpAuthnFilterConfigValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = GcpAuthnFilterConfigValidationError{} + +// Validate checks the field values on Audience with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *Audience) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on Audience with the rules defined in +// the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in AudienceMultiError, or nil +// if none found. +func (m *Audience) ValidateAll() error { + return m.validate(true) +} + +func (m *Audience) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if utf8.RuneCountInString(m.GetUrl()) < 1 { + err := AudienceValidationError{ + field: "Url", + reason: "value length must be at least 1 runes", + } + if !all { + return err + } + errors = append(errors, err) + } + + if len(errors) > 0 { + return AudienceMultiError(errors) + } + + return nil +} + +// AudienceMultiError is an error wrapping multiple validation errors returned +// by Audience.ValidateAll() if the designated constraints aren't met. +type AudienceMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m AudienceMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m AudienceMultiError) AllErrors() []error { return m } + +// AudienceValidationError is the validation error returned by +// Audience.Validate if the designated constraints aren't met. +type AudienceValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e AudienceValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e AudienceValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e AudienceValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e AudienceValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e AudienceValidationError) ErrorName() string { return "AudienceValidationError" } + +// Error satisfies the builtin error interface +func (e AudienceValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sAudience.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = AudienceValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = AudienceValidationError{} + +// Validate checks the field values on TokenCacheConfig with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *TokenCacheConfig) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on TokenCacheConfig with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// TokenCacheConfigMultiError, or nil if none found. +func (m *TokenCacheConfig) ValidateAll() error { + return m.validate(true) +} + +func (m *TokenCacheConfig) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if wrapper := m.GetCacheSize(); wrapper != nil { + + if wrapper.GetValue() > 9223372036854775807 { + err := TokenCacheConfigValidationError{ + field: "CacheSize", + reason: "value must be less than or equal to 9223372036854775807", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + + if len(errors) > 0 { + return TokenCacheConfigMultiError(errors) + } + + return nil +} + +// TokenCacheConfigMultiError is an error wrapping multiple validation errors +// returned by TokenCacheConfig.ValidateAll() if the designated constraints +// aren't met. +type TokenCacheConfigMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TokenCacheConfigMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TokenCacheConfigMultiError) AllErrors() []error { return m } + +// TokenCacheConfigValidationError is the validation error returned by +// TokenCacheConfig.Validate if the designated constraints aren't met. +type TokenCacheConfigValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TokenCacheConfigValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TokenCacheConfigValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TokenCacheConfigValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TokenCacheConfigValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TokenCacheConfigValidationError) ErrorName() string { return "TokenCacheConfigValidationError" } + +// Error satisfies the builtin error interface +func (e TokenCacheConfigValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTokenCacheConfig.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TokenCacheConfigValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TokenCacheConfigValidationError{} + +// Validate checks the field values on TokenHeader with the rules defined in +// the proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *TokenHeader) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on TokenHeader with the rules defined in +// the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in TokenHeaderMultiError, or +// nil if none found. +func (m *TokenHeader) ValidateAll() error { + return m.validate(true) +} + +func (m *TokenHeader) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if utf8.RuneCountInString(m.GetName()) < 1 { + err := TokenHeaderValidationError{ + field: "Name", + reason: "value length must be at least 1 runes", + } + if !all { + return err + } + errors = append(errors, err) + } + + if !_TokenHeader_Name_Pattern.MatchString(m.GetName()) { + err := TokenHeaderValidationError{ + field: "Name", + reason: "value does not match regex pattern \"^[^\\x00\\n\\r]*$\"", + } + if !all { + return err + } + errors = append(errors, err) + } + + if !_TokenHeader_ValuePrefix_Pattern.MatchString(m.GetValuePrefix()) { + err := TokenHeaderValidationError{ + field: "ValuePrefix", + reason: "value does not match regex pattern \"^[^\\x00\\n\\r]*$\"", + } + if !all { + return err + } + errors = append(errors, err) + } + + if len(errors) > 0 { + return TokenHeaderMultiError(errors) + } + + return nil +} + +// TokenHeaderMultiError is an error wrapping multiple validation errors +// returned by TokenHeader.ValidateAll() if the designated constraints aren't met. +type TokenHeaderMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TokenHeaderMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TokenHeaderMultiError) AllErrors() []error { return m } + +// TokenHeaderValidationError is the validation error returned by +// TokenHeader.Validate if the designated constraints aren't met. +type TokenHeaderValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TokenHeaderValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TokenHeaderValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TokenHeaderValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TokenHeaderValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TokenHeaderValidationError) ErrorName() string { return "TokenHeaderValidationError" } + +// Error satisfies the builtin error interface +func (e TokenHeaderValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTokenHeader.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TokenHeaderValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TokenHeaderValidationError{} + +var _TokenHeader_Name_Pattern = regexp.MustCompile("^[^\x00\n\r]*$") + +var _TokenHeader_ValuePrefix_Pattern = regexp.MustCompile("^[^\x00\n\r]*$") diff --git a/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn_vtproto.pb.go b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn_vtproto.pb.go new file mode 100644 index 000000000..1b08fd98a --- /dev/null +++ b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn_vtproto.pb.go @@ -0,0 +1,358 @@ +//go:build vtprotobuf +// +build vtprotobuf + +// Code generated by protoc-gen-go-vtproto. DO NOT EDIT. +// source: envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto + +package gcp_authnv3 + +import ( + protohelpers "github.com/planetscale/vtprotobuf/protohelpers" + durationpb "github.com/planetscale/vtprotobuf/types/known/durationpb" + wrapperspb "github.com/planetscale/vtprotobuf/types/known/wrapperspb" + proto "google.golang.org/protobuf/proto" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +func (m *GcpAuthnFilterConfig) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GcpAuthnFilterConfig) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *GcpAuthnFilterConfig) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.Timeout != nil { + size, err := (*durationpb.Duration)(m.Timeout).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x32 + } + if len(m.Cluster) > 0 { + i -= len(m.Cluster) + copy(dAtA[i:], m.Cluster) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Cluster))) + i-- + dAtA[i] = 0x2a + } + if m.TokenHeader != nil { + size, err := m.TokenHeader.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x22 + } + if m.CacheConfig != nil { + size, err := m.CacheConfig.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x1a + } + if m.RetryPolicy != nil { + if vtmsg, ok := interface{}(m.RetryPolicy).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.RetryPolicy) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x12 + } + if m.HttpUri != nil { + if vtmsg, ok := interface{}(m.HttpUri).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.HttpUri) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *Audience) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Audience) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *Audience) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Url) > 0 { + i -= len(m.Url) + copy(dAtA[i:], m.Url) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Url))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *TokenCacheConfig) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TokenCacheConfig) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *TokenCacheConfig) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.CacheSize != nil { + size, err := (*wrapperspb.UInt64Value)(m.CacheSize).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *TokenHeader) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TokenHeader) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *TokenHeader) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.ValuePrefix) > 0 { + i -= len(m.ValuePrefix) + copy(dAtA[i:], m.ValuePrefix) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.ValuePrefix))) + i-- + dAtA[i] = 0x12 + } + if len(m.Name) > 0 { + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *GcpAuthnFilterConfig) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.HttpUri != nil { + if size, ok := interface{}(m.HttpUri).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(m.HttpUri) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.RetryPolicy != nil { + if size, ok := interface{}(m.RetryPolicy).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(m.RetryPolicy) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.CacheConfig != nil { + l = m.CacheConfig.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.TokenHeader != nil { + l = m.TokenHeader.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.Cluster) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.Timeout != nil { + l = (*durationpb.Duration)(m.Timeout).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *Audience) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Url) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *TokenCacheConfig) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.CacheSize != nil { + l = (*wrapperspb.UInt64Value)(m.CacheSize).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *TokenHeader) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.ValuePrefix) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} diff --git a/vendor/google.golang.org/grpc/balancer/balancer.go b/vendor/google.golang.org/grpc/balancer/balancer.go index 326888ae3..7e3dbaad2 100644 --- a/vendor/google.golang.org/grpc/balancer/balancer.go +++ b/vendor/google.golang.org/grpc/balancer/balancer.go @@ -60,7 +60,7 @@ func Register(b Builder) { if !envconfig.CaseSensitiveBalancerRegistries { name = strings.ToLower(name) if name != b.Name() { - logger.Warningf("Balancer registered with name %q. grpc-go will be switching to case sensitive balancer registries soon. After 2 releases, we will enable the env var by default.", b.Name()) + logger.Warningf("Balancer registered with name %q. grpc-go has switched to case sensitive balancer registries. GRPC_GO_EXPERIMENTAL_CASE_SENSITIVE_BALANCER_REGISTRIES env variable will be removed in release v1.82.0", b.Name()) } } m[name] = b @@ -85,7 +85,7 @@ func Get(name string) Builder { if !envconfig.CaseSensitiveBalancerRegistries { lowerName := strings.ToLower(name) if lowerName != name { - logger.Warningf("Balancer retrieved for name %q. grpc-go will be switching to case sensitive balancer registries soon. After 2 releases, we will enable the env var by default.", name) + logger.Warningf("Balancer retrieved for name %q. grpc-go has switched to case sensitive balancer registries. GRPC_GO_EXPERIMENTAL_CASE_SENSITIVE_BALANCER_REGISTRIES env variable will be removed in release v1.82.0", name) } name = lowerName } diff --git a/vendor/google.golang.org/grpc/balancer/grpclb/grpc_lb_v1/load_balancer_grpc.pb.go b/vendor/google.golang.org/grpc/balancer/grpclb/grpc_lb_v1/load_balancer_grpc.pb.go index 942b7e967..6aeb81981 100644 --- a/vendor/google.golang.org/grpc/balancer/grpclb/grpc_lb_v1/load_balancer_grpc.pb.go +++ b/vendor/google.golang.org/grpc/balancer/grpclb/grpc_lb_v1/load_balancer_grpc.pb.go @@ -19,7 +19,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 +// - protoc-gen-go-grpc v1.6.2 // - protoc v5.27.1 // source: grpc/lb/v1/load_balancer.proto diff --git a/vendor/google.golang.org/grpc/balancer/pickfirst/pickfirst.go b/vendor/google.golang.org/grpc/balancer/pickfirst/pickfirst.go index 518a69d57..d48bc304c 100644 --- a/vendor/google.golang.org/grpc/balancer/pickfirst/pickfirst.go +++ b/vendor/google.golang.org/grpc/balancer/pickfirst/pickfirst.go @@ -35,9 +35,9 @@ import ( "google.golang.org/grpc/balancer" "google.golang.org/grpc/balancer/pickfirst/internal" "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/experimental/balancer/weight" expstats "google.golang.org/grpc/experimental/stats" "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/internal/balancer/weight" "google.golang.org/grpc/internal/envconfig" internalgrpclog "google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/internal/pretty" diff --git a/vendor/google.golang.org/grpc/balancer/ringhash/ringhash.go b/vendor/google.golang.org/grpc/balancer/ringhash/ringhash.go index 027b4339b..171cdd964 100644 --- a/vendor/google.golang.org/grpc/balancer/ringhash/ringhash.go +++ b/vendor/google.golang.org/grpc/balancer/ringhash/ringhash.go @@ -42,7 +42,7 @@ import ( "google.golang.org/grpc/balancer/lazy" "google.golang.org/grpc/balancer/pickfirst" "google.golang.org/grpc/connectivity" - "google.golang.org/grpc/internal/balancer/weight" + "google.golang.org/grpc/experimental/balancer/weight" "google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/internal/pretty" iringhash "google.golang.org/grpc/internal/ringhash" diff --git a/vendor/google.golang.org/grpc/balancer/rls/control_channel.go b/vendor/google.golang.org/grpc/balancer/rls/control_channel.go index 60e6a021d..844cfc3d9 100644 --- a/vendor/google.golang.org/grpc/balancer/rls/control_channel.go +++ b/vendor/google.golang.org/grpc/balancer/rls/control_channel.go @@ -29,7 +29,6 @@ import ( "google.golang.org/grpc/connectivity" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/internal" - "google.golang.org/grpc/internal/buffer" internalgrpclog "google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/internal/grpcsync" "google.golang.org/grpc/internal/pretty" @@ -39,6 +38,12 @@ import ( var newAdaptiveThrottler = func() adaptiveThrottler { return adaptive.New() } +// newConnectivityStateSubscriber is a variable that can be overridden in tests +// to wrap the connectivity state subscriber for testing purposes. +var newConnectivityStateSubscriber = func(sub grpcsync.Subscriber) grpcsync.Subscriber { + return sub +} + type adaptiveThrottler interface { ShouldThrottle() bool RegisterBackendResponse(throttled bool) @@ -57,12 +62,11 @@ type controlChannel struct { // hammering the RLS service while it is overloaded or down. throttler adaptiveThrottler - cc *grpc.ClientConn - client rlsgrpc.RouteLookupServiceClient - logger *internalgrpclog.PrefixLogger - connectivityStateCh *buffer.Unbounded - unsubscribe func() - monitorDoneCh chan struct{} + cc *grpc.ClientConn + client rlsgrpc.RouteLookupServiceClient + logger *internalgrpclog.PrefixLogger + dropConnStateSubscriber func() + seenTransientFailure bool } // newControlChannel creates a controlChannel to rlsServerName and uses @@ -70,11 +74,9 @@ type controlChannel struct { // gRPC channel. func newControlChannel(rlsServerName, serviceConfig string, rpcTimeout time.Duration, bOpts balancer.BuildOptions, backToReadyFunc func()) (*controlChannel, error) { ctrlCh := &controlChannel{ - rpcTimeout: rpcTimeout, - backToReadyFunc: backToReadyFunc, - throttler: newAdaptiveThrottler(), - connectivityStateCh: buffer.NewUnbounded(), - monitorDoneCh: make(chan struct{}), + rpcTimeout: rpcTimeout, + backToReadyFunc: backToReadyFunc, + throttler: newAdaptiveThrottler(), } ctrlCh.logger = internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf("[rls-control-channel %p] ", ctrlCh)) @@ -88,11 +90,11 @@ func newControlChannel(rlsServerName, serviceConfig string, rpcTimeout time.Dura } // Subscribe to connectivity state before connecting to avoid missing initial // updates, which are only delivered to active subscribers. - ctrlCh.unsubscribe = internal.SubscribeToConnectivityStateChanges.(func(cc *grpc.ClientConn, s grpcsync.Subscriber) func())(ctrlCh.cc, ctrlCh) + subscribe := internal.SubscribeToConnectivityStateChanges.(func(cc *grpc.ClientConn, s grpcsync.Subscriber) func()) + ctrlCh.dropConnStateSubscriber = subscribe(ctrlCh.cc, newConnectivityStateSubscriber(ctrlCh)) ctrlCh.cc.Connect() ctrlCh.client = rlsgrpc.NewRouteLookupServiceClient(ctrlCh.cc) ctrlCh.logger.Infof("Control channel created to RLS server at: %v", rlsServerName) - go ctrlCh.monitorConnectivityState() return ctrlCh, nil } @@ -101,7 +103,37 @@ func (cc *controlChannel) OnMessage(msg any) { if !ok { panic(fmt.Sprintf("Unexpected message type %T , wanted connectectivity.State type", msg)) } - cc.connectivityStateCh.Put(st) + + switch st { + case connectivity.Ready: + // Only reset backoff when transitioning from TRANSIENT_FAILURE to READY. + // This indicates the RLS server has recovered from being unreachable, so + // we reset backoff state in all cache entries to allow pending RPCs to + // proceed immediately. We skip benign transitions like READY → IDLE → READY + // since those don't represent actual failures. + if cc.seenTransientFailure { + if cc.logger.V(2) { + cc.logger.Infof("Control channel back to READY after TRANSIENT_FAILURE") + } + cc.seenTransientFailure = false + if cc.backToReadyFunc != nil { + cc.backToReadyFunc() + } + } else { + if cc.logger.V(2) { + cc.logger.Infof("Control channel is READY") + } + } + case connectivity.TransientFailure: + // Track that we've entered TRANSIENT_FAILURE state so we know to reset + // backoffs when we recover to READY. + cc.logger.Warningf("Control channel is TRANSIENT_FAILURE") + cc.seenTransientFailure = true + default: + if cc.logger.V(2) { + cc.logger.Infof("Control channel connectivity state is %s", st) + } + } } // dialOpts constructs the dial options for the control plane channel. @@ -148,68 +180,8 @@ func (cc *controlChannel) dialOpts(bOpts balancer.BuildOptions, serviceConfig st return dopts, nil } -func (cc *controlChannel) monitorConnectivityState() { - cc.logger.Infof("Starting connectivity state monitoring goroutine") - defer close(cc.monitorDoneCh) - - // Since we use two mechanisms to deal with RLS server being down: - // - adaptive throttling for the channel as a whole - // - exponential backoff on a per-request basis - // we need a way to avoid double-penalizing requests by counting failures - // toward both mechanisms when the RLS server is unreachable. - // - // To accomplish this, we monitor the state of the control plane channel. If - // the state has been TRANSIENT_FAILURE since the last time it was in state - // READY, and it then transitions into state READY, we push on a channel - // which is being read by the LB policy. - // - // The LB the policy will iterate through the cache to reset the backoff - // timeouts in all cache entries. Specifically, this means that it will - // reset the backoff state and cancel the pending backoff timer. Note that - // when cancelling the backoff timer, just like when the backoff timer fires - // normally, a new picker is returned to the channel, to force it to - // re-process any wait-for-ready RPCs that may still be queued if we failed - // them while we were in backoff. However, we should optimize this case by - // returning only one new picker, regardless of how many backoff timers are - // cancelled. - - // Wait for the control channel to become READY for the first time. - for s, ok := <-cc.connectivityStateCh.Get(); s != connectivity.Ready; s, ok = <-cc.connectivityStateCh.Get() { - if !ok { - return - } - - cc.connectivityStateCh.Load() - if s == connectivity.Shutdown { - return - } - } - cc.connectivityStateCh.Load() - cc.logger.Infof("Connectivity state is READY") - - for { - s, ok := <-cc.connectivityStateCh.Get() - if !ok { - return - } - cc.connectivityStateCh.Load() - - if s == connectivity.Shutdown { - return - } - if s == connectivity.Ready { - cc.logger.Infof("Control channel back to READY") - cc.backToReadyFunc() - } - - cc.logger.Infof("Connectivity state is %s", s) - } -} - func (cc *controlChannel) close() { - cc.unsubscribe() - cc.connectivityStateCh.Close() - <-cc.monitorDoneCh + cc.dropConnStateSubscriber() cc.cc.Close() cc.logger.Infof("Shutdown") } diff --git a/vendor/google.golang.org/grpc/credentials/alts/alts.go b/vendor/google.golang.org/grpc/credentials/alts/alts.go index 35539eb1a..b8afa917c 100644 --- a/vendor/google.golang.org/grpc/credentials/alts/alts.go +++ b/vendor/google.golang.org/grpc/credentials/alts/alts.go @@ -181,16 +181,9 @@ func (g *altsTC) ClientHandshake(ctx context.Context, addr string, rawConn net.C } // Do not close hsConn since it is shared with other handshakes. - // Possible context leak: - // The cancel function for the child context we create will only be - // called a non-nil error is returned. var cancel context.CancelFunc ctx, cancel = context.WithCancel(ctx) - defer func() { - if err != nil { - cancel() - } - }() + defer cancel() opts := handshaker.DefaultClientHandshakerOptions() opts.TargetName = addr @@ -204,11 +197,8 @@ func (g *altsTC) ClientHandshake(ctx context.Context, addr string, rawConn net.C if err != nil { return nil, nil, err } - defer func() { - if err != nil { - chs.Close() - } - }() + // Close the handshaker since we have obtained a connection. + defer chs.Close() secConn, authInfo, err := chs.ClientHandshake(ctx) if err != nil { return nil, nil, err @@ -247,15 +237,12 @@ func (g *altsTC) ServerHandshake(rawConn net.Conn) (_ net.Conn, _ credentials.Au if err != nil { return nil, nil, err } - defer func() { - if err != nil { - shs.Close() - } - }() secConn, authInfo, err := shs.ServerHandshake(ctx) if err != nil { return nil, nil, err } + // Close the handshaker since we have obtained a connection. + defer shs.Close() altsAuthInfo, ok := authInfo.(AuthInfo) if !ok { return nil, nil, errors.New("server-side auth info is not of type alts.AuthInfo") diff --git a/vendor/google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp/handshaker_grpc.pb.go b/vendor/google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp/handshaker_grpc.pb.go index 2dee74f16..96a40fc35 100644 --- a/vendor/google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp/handshaker_grpc.pb.go +++ b/vendor/google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp/handshaker_grpc.pb.go @@ -17,7 +17,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 +// - protoc-gen-go-grpc v1.6.2 // - protoc v5.27.1 // source: grpc/gcp/handshaker.proto diff --git a/vendor/google.golang.org/grpc/dialoptions.go b/vendor/google.golang.org/grpc/dialoptions.go index 4ec5f9cd0..3af08e1ab 100644 --- a/vendor/google.golang.org/grpc/dialoptions.go +++ b/vendor/google.golang.org/grpc/dialoptions.go @@ -173,10 +173,8 @@ func newJoinDialOption(opts ...DialOption) DialOption { // If this option is set to true every connection will release the buffer after // flushing the data on the wire. // -// # Experimental -// -// Notice: This API is EXPERIMENTAL and may be changed or removed in a -// later release. +// Deprecated: shared write buffer is enabled by default. WithSharedWriteBuffer +// will be removed in a future release. func WithSharedWriteBuffer(val bool) DialOption { return newFuncDialOption(func(o *dialOptions) { o.copts.SharedWriteBuffer = val @@ -229,6 +227,14 @@ func WithInitialConnWindowSize(s int32) DialOption { // WithStaticStreamWindowSize returns a DialOption which sets the initial // stream window size to the value provided and disables dynamic flow control. +// +// Note that this also disables dynamic flow control for the connection, +// falling back to a default static connection-level window of 64KB. To +// use a larger connection-level window, you must also use the +// [WithStaticConnWindowSize] DialOption. +// +// Most users should not configure static flow control windows unless +// operating in a memory-constrained environment. func WithStaticStreamWindowSize(s int32) DialOption { return newFuncDialOption(func(o *dialOptions) { o.copts.InitialWindowSize = s @@ -239,6 +245,14 @@ func WithStaticStreamWindowSize(s int32) DialOption { // WithStaticConnWindowSize returns a DialOption which sets the initial // connection window size to the value provided and disables dynamic flow // control. +// +// Note that this also disables dynamic flow control for individual streams, +// falling back to a default static connection-level window of 64KB. To +// explicitly configure the stream-level window size, you must also use the +// [WithStaticStreamWindowSize] DialOption. +// +// Most users should not configure static flow control windows unless +// operating in a memory-constrained environment. func WithStaticConnWindowSize(s int32) DialOption { return newFuncDialOption(func(o *dialOptions) { o.copts.InitialConnWindowSize = s diff --git a/vendor/google.golang.org/grpc/encoding/encoding.go b/vendor/google.golang.org/grpc/encoding/encoding.go index 296f38c3a..bfa8b268f 100644 --- a/vendor/google.golang.org/grpc/encoding/encoding.go +++ b/vendor/google.golang.org/grpc/encoding/encoding.go @@ -66,6 +66,9 @@ type Compressor interface { // Decompress reads data from r, decompresses it, and provides the // uncompressed data via the returned io.Reader. If an error occurs while // initializing the decompressor, that error is returned instead. + // + // The returned io.Reader may optionally implement io.ReadCloser, and if it + // does, gRPC will call Close() exactly once. Decompress(r io.Reader) (io.Reader, error) // Name is the name of the compression codec and is used to set the content // coding header. The result must be static; the result cannot change diff --git a/vendor/google.golang.org/grpc/encoding/gzip/gzip.go b/vendor/google.golang.org/grpc/encoding/gzip/gzip.go index 153e4dbfb..65908d9a2 100644 --- a/vendor/google.golang.org/grpc/encoding/gzip/gzip.go +++ b/vendor/google.golang.org/grpc/encoding/gzip/gzip.go @@ -81,6 +81,8 @@ func (z *writer) Close() error { return z.Writer.Close() } +var _ io.Closer = &reader{} + type reader struct { *gzip.Reader pool *sync.Pool @@ -102,14 +104,16 @@ func (c *compressor) Decompress(r io.Reader) (io.Reader, error) { return z, nil } -func (z *reader) Read(p []byte) (n int, err error) { - n, err = z.Reader.Read(p) - if err == io.EOF { - z.pool.Put(z) - } +func (r *reader) Read(p []byte) (n int, err error) { + n, err = r.Reader.Read(p) return n, err } +func (r *reader) Close() error { + defer r.pool.Put(r) + return r.Reader.Close() +} + func (c *compressor) Name() string { return Name } diff --git a/vendor/google.golang.org/grpc/experimental/balancer/hostname/hostname.go b/vendor/google.golang.org/grpc/experimental/balancer/hostname/hostname.go new file mode 100644 index 000000000..f42a7cf2f --- /dev/null +++ b/vendor/google.golang.org/grpc/experimental/balancer/hostname/hostname.go @@ -0,0 +1,47 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package hostname contains utilities for the endpoint hostname attribute +// (used for per-endpoint :authority / SNI override). +// +// # Experimental +// +// Notice: All APIs in this package are EXPERIMENTAL and may be changed +// or removed in a later release. +package hostname + +import "google.golang.org/grpc/resolver" + +type hostnameKey struct{} + +// Set returns a copy of the given endpoint with the hostname attribute +// set. If hostname is empty the endpoint is returned unmodified. +func Set(endpoint resolver.Endpoint, hostname string) resolver.Endpoint { + if hostname == "" { + return endpoint + } + endpoint.Attributes = endpoint.Attributes.WithValue(hostnameKey{}, hostname) + return endpoint +} + +// FromEndpoint returns the hostname attribute of endpoint. If this +// attribute is not set, it returns the empty string. +func FromEndpoint(endpoint resolver.Endpoint) string { + h, _ := endpoint.Attributes.Value(hostnameKey{}).(string) + return h +} diff --git a/vendor/google.golang.org/grpc/internal/balancer/weight/weight.go b/vendor/google.golang.org/grpc/experimental/balancer/weight/weight.go similarity index 71% rename from vendor/google.golang.org/grpc/internal/balancer/weight/weight.go rename to vendor/google.golang.org/grpc/experimental/balancer/weight/weight.go index 11beb07d1..beab9e07c 100644 --- a/vendor/google.golang.org/grpc/internal/balancer/weight/weight.go +++ b/vendor/google.golang.org/grpc/experimental/balancer/weight/weight.go @@ -16,23 +16,23 @@ * */ -// Package weight contains utilities to manage endpoint weights. Weights are -// used by LB policies such as ringhash to distribute load across multiple -// endpoints. +// Package weight contains utilities to manage endpoint weights. +// Weights may be used by LB policies to distribute load across +// multiple endpoints. +// +// # Experimental +// +// Notice: All APIs in this package are EXPERIMENTAL and may be changed +// or removed in a later release. package weight -import ( - "fmt" - - "google.golang.org/grpc/resolver" -) +import "google.golang.org/grpc/resolver" // attributeKey is the type used as the key to store EndpointInfo in the // Attributes field of resolver.Endpoint. type attributeKey struct{} -// EndpointInfo will be stored in the Attributes field of Endpoints in order to -// use the ringhash balancer. +// EndpointInfo will be stored in the Attributes field of Endpoints. type EndpointInfo struct { Weight uint32 } @@ -43,22 +43,16 @@ func (a EndpointInfo) Equal(o any) bool { return ok && oa.Weight == a.Weight } -// Set returns a copy of endpoint in which the Attributes field is updated with -// EndpointInfo. +// Set returns a copy of endpoint in which the Attributes field is +// updated with EndpointInfo. func Set(endpoint resolver.Endpoint, epInfo EndpointInfo) resolver.Endpoint { endpoint.Attributes = endpoint.Attributes.WithValue(attributeKey{}, epInfo) return endpoint } -// String returns a human-readable representation of EndpointInfo. -// This method is intended for logging, testing, and debugging purposes only. -// Do not rely on the output format, as it is not guaranteed to remain stable. -func (a EndpointInfo) String() string { - return fmt.Sprintf("Weight: %d", a.Weight) -} - -// FromEndpoint returns the EndpointInfo stored in the Attributes field of an -// endpoint. It returns an empty EndpointInfo if attribute is not found. +// FromEndpoint returns the EndpointInfo stored in the Attributes +// field of an endpoint. It returns an empty EndpointInfo if attribute +// is not found. func FromEndpoint(endpoint resolver.Endpoint) EndpointInfo { v := endpoint.Attributes.Value(attributeKey{}) ei, _ := v.(EndpointInfo) diff --git a/vendor/google.golang.org/grpc/health/grpc_health_v1/health_grpc.pb.go b/vendor/google.golang.org/grpc/health/grpc_health_v1/health_grpc.pb.go index 9e10fdd2e..537ba0571 100644 --- a/vendor/google.golang.org/grpc/health/grpc_health_v1/health_grpc.pb.go +++ b/vendor/google.golang.org/grpc/health/grpc_health_v1/health_grpc.pb.go @@ -17,7 +17,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 +// - protoc-gen-go-grpc v1.6.2 // - protoc v5.27.1 // source: grpc/health/v1/health.proto diff --git a/vendor/google.golang.org/grpc/internal/envconfig/envconfig.go b/vendor/google.golang.org/grpc/internal/envconfig/envconfig.go index 8ca87a57a..29d332e7b 100644 --- a/vendor/google.golang.org/grpc/internal/envconfig/envconfig.go +++ b/vendor/google.golang.org/grpc/internal/envconfig/envconfig.go @@ -59,6 +59,15 @@ var ( // unconditionally. XDSEndpointHashKeyBackwardCompat = boolFromEnv("GRPC_XDS_ENDPOINT_HASH_KEY_BACKWARD_COMPAT", false) + // LabelServerGoroutines controls setting [runtime/pprof.Labels] on the + // goroutines spawned by [grpc.Server] type. + // For now, this is limited to the goroutines spawned to handle incoming + // requests on the server. + // Set "GRPC_GO_SERVER_GOROUTINE_LABELS" to "grpc.method=true" to + // enable this grpc.method label, or "all" to enable all valid labels. + // This variable is a bit-field. + LabelServerGoroutines = goroutineLabelsFromEnv("GRPC_GO_SERVER_GOROUTINE_LABELS", 0) + // RingHashSetRequestHashKey is set if the ring hash balancer can get the // request hash header by setting the "requestHashHeader" field, according // to gRFC A76. It can be disabled by setting the environment variable @@ -78,12 +87,12 @@ var ( EnableDefaultPortForProxyTarget = boolFromEnv("GRPC_EXPERIMENTAL_ENABLE_DEFAULT_PORT_FOR_PROXY_TARGET", true) // CaseSensitiveBalancerRegistries is set if the balancer registry should be - // case-sensitive. This is disabled by default, but can be enabled by setting + // case-sensitive. This is enabled by default, but can be disabled by setting // the env variable "GRPC_GO_EXPERIMENTAL_CASE_SENSITIVE_BALANCER_REGISTRIES" - // to "true". + // to "false". // - // TODO: After 2 releases, we will enable the env var by default. - CaseSensitiveBalancerRegistries = boolFromEnv("GRPC_GO_EXPERIMENTAL_CASE_SENSITIVE_BALANCER_REGISTRIES", false) + // This env varible will be removed in release v1.82.0. + CaseSensitiveBalancerRegistries = boolFromEnv("GRPC_GO_EXPERIMENTAL_CASE_SENSITIVE_BALANCER_REGISTRIES", true) // XDSAuthorityRewrite indicates whether xDS authority rewriting is enabled. // This feature is defined in gRFC A81 and is enabled by setting the @@ -104,22 +113,6 @@ var ( // to "false". XDSRecoverPanicInResourceParsing = boolFromEnv("GRPC_GO_EXPERIMENTAL_XDS_RESOURCE_PANIC_RECOVERY", true) - // DisableStrictPathChecking indicates whether strict path checking is - // disabled. This feature can be disabled by setting the environment - // variable GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING to "true". - // - // When strict path checking is enabled, gRPC will reject requests with - // paths that do not conform to the gRPC over HTTP/2 specification found at - // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md. - // - // When disabled, gRPC will allow paths that do not contain a leading slash. - // Enabling strict path checking is recommended for security reasons, as it - // prevents potential path traversal vulnerabilities. - // - // A future release will remove this environment variable, enabling strict - // path checking behavior unconditionally. - DisableStrictPathChecking = boolFromEnv("GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING", false) - // EnablePriorityLBChildPolicyCache controls whether the priority balancer // should cache child balancers that are removed from the LB policy config, // for a period of 15 minutes. This is disabled by default, but can be @@ -127,6 +120,18 @@ var ( // GRPC_EXPERIMENTAL_ENABLE_PRIORITY_LB_CHILD_POLICY_CACHE to true. EnablePriorityLBChildPolicyCache = boolFromEnv("GRPC_EXPERIMENTAL_ENABLE_PRIORITY_LB_CHILD_POLICY_CACHE", false) + // Enable8KBDefaultHeaderListSize indicates that default maximum header list + // size is restricted to 8KB. This is disabled by default, but can be enabled + // by setting the environment variable + // "GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE" to "true". + // When disabled, the default maximum header list size of 16MB is used. + // + // When enabled, RPCs with a total size of headers exceeding 8KB will fail + // unless explicitly configured otherwise by the user. + // + // TODO: In release v1.82.0, env var will be enabled by default. + Enable8KBDefaultHeaderListSize = boolFromEnv("GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE", false) + // EnableHTTPFramerReadBufferPooling enables the use of the // readyreader.Reader interface to perform non-memory-pinning reads, // provided the underlying net.Conn supports it. This reduces memory usage @@ -136,6 +141,17 @@ var ( // feature if unforeseen issues arise, and it will be removed in a future // release. EnableHTTPFramerReadBufferPooling = boolFromEnv("GRPC_GO_EXPERIMENTAL_HTTP_FRAMER_READ_BUFFER_POOLING", true) + + // ControlBufferThrottleLimit is the maximum number of control frames that can + // be queued in the control buffer before throttling is applied. The value + // must be between 1 and 10,000, and is set to 100 by default. + // + // This environment variable serves as an escape hatch to increase the + // throttling limit if unforeseen issues arise, and it will be removed in a + // future release. + // + // TODO: Remove this env var once v1.83.0 is release. + ControlBufferThrottleLimit = uint64FromEnv("GRPC_GO_EXPERIMENTAL_CONTROL_BUFFER_THROTTLE_LIMIT", 100, 1, 10000) ) func boolFromEnv(envVar string, def bool) bool { @@ -160,3 +176,52 @@ func uint64FromEnv(envVar string, def, min, max uint64) uint64 { } return v } + +// GoroutineLabels is a bitfield indicating which goroutine labels are enabled. +type GoroutineLabels uint16 + +func goroutineLabelsFromEnv(envVar string, def GoroutineLabels) GoroutineLabels { + val := def + v := os.Getenv(envVar) + if strings.EqualFold(v, "all") { + return AllGoroutineLabels + } else if strings.EqualFold(v, "none") { + return 0 + } + for s := range strings.SplitSeq(v, ",") { + s = strings.TrimSpace(s) + if len(s) == 0 { + continue + } + pre, post, ok := strings.Cut(s, "=") + if !ok { + // no equals sign + continue + } + post = strings.TrimSpace(post) + pre = strings.TrimSpace(pre) + bitDesignator := GoroutineLabels(0) + switch { + case strings.EqualFold(pre, "grpc.method"): + bitDesignator = GoroutineLabelServerMethod + default: + continue + } + if strings.EqualFold(post, "true") { + val |= bitDesignator + } else if strings.EqualFold(post, "false") { + val &^= bitDesignator + } + } + return val +} + +const ( + // GoroutineLabelServerMethod sets the grpc.method label on new + // server-side gRPC streams. + GoroutineLabelServerMethod GoroutineLabels = 1 << iota +) + +// AllGoroutineLabels is an or'd together bitfield of all valid GoroutineLabels +// constant values (above). +const AllGoroutineLabels = GoroutineLabelServerMethod diff --git a/vendor/google.golang.org/grpc/internal/envconfig/xds.go b/vendor/google.golang.org/grpc/internal/envconfig/xds.go index 333d8a0b0..a2312f8ea 100644 --- a/vendor/google.golang.org/grpc/internal/envconfig/xds.go +++ b/vendor/google.golang.org/grpc/internal/envconfig/xds.go @@ -89,4 +89,14 @@ var ( // filtered and prefix-propagated to the LRS server. For more details, see: // https://github.com/grpc/proposal/blob/master/A85-lrs-custom-metrics-changes.md XDSORCAToLRSPropEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_ORCA_LRS_PROPAGATION", false) + + // XDSClientExtProcEnabled indicates whether ExtProc filter is enabled on + // the client side. For more details, see: + // https://github.com/grpc/proposal/blob/master/A93-xds-ext-proc.md + XDSClientExtProcEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false) + + // GCPAuthenticationFilterEnabled enables the xDS GCP Authentication + // filter. For more details, see: + // https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md + GCPAuthenticationFilterEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER", false) ) diff --git a/vendor/google.golang.org/grpc/internal/grpcutil/encode_duration.go b/vendor/google.golang.org/grpc/internal/grpcutil/encode_duration.go index b25b0baec..1cc43fc6b 100644 --- a/vendor/google.golang.org/grpc/internal/grpcutil/encode_duration.go +++ b/vendor/google.golang.org/grpc/internal/grpcutil/encode_duration.go @@ -39,7 +39,6 @@ func div(d, r time.Duration) int64 { // // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests func EncodeDuration(t time.Duration) string { - // TODO: This is simplistic and not bandwidth efficient. Improve it. if t <= 0 { return "0n" } diff --git a/vendor/google.golang.org/grpc/internal/proto/grpc_lookup_v1/rls_grpc.pb.go b/vendor/google.golang.org/grpc/internal/proto/grpc_lookup_v1/rls_grpc.pb.go index 0386dcf5c..0bf8477aa 100644 --- a/vendor/google.golang.org/grpc/internal/proto/grpc_lookup_v1/rls_grpc.pb.go +++ b/vendor/google.golang.org/grpc/internal/proto/grpc_lookup_v1/rls_grpc.pb.go @@ -14,7 +14,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 +// - protoc-gen-go-grpc v1.6.2 // - protoc v5.27.1 // source: grpc/lookup/v1/rls.proto diff --git a/vendor/google.golang.org/grpc/internal/resolver/config_selector.go b/vendor/google.golang.org/grpc/internal/resolver/config_selector.go index 3db62ccad..6320e9b57 100644 --- a/vendor/google.golang.org/grpc/internal/resolver/config_selector.go +++ b/vendor/google.golang.org/grpc/internal/resolver/config_selector.go @@ -106,14 +106,24 @@ type ClientStream interface { // ClientInterceptor is an interceptor for gRPC client streams. type ClientInterceptor interface { - // NewStream produces a ClientStream for an RPC which may optionally use - // the provided function to produce a stream for delegation. Note: - // RPCInfo.Context should not be used (will be nil). + // NewStream creates a ClientStream for an RPC. // - // done is invoked when the RPC is finished using its connection, or could - // not be assigned a connection. RPC operations may still occur on - // ClientStream after done is called, since the interceptor is invoked by - // application-layer operations. done must never be nil when called. + // Implementations must delegate stream creation to the provided newStream + // function. To intercept or override stream behavior, implementations + // may wrap the ClientStream returned by the delegate. + // + // Note: RPCInfo.Context is currently unused and will be nil. + // + // The done function is invoked when the RPC has finished using its + // underlying connection or if a connection could not be assigned. Because + // interceptors operate at the application layer, RPC operations may + // continue on the ClientStream even after done has been called. The + // caller must ensure done is non-nil. + // + // To ensure RPC completion notifications propagate through the entire + // interceptor chain, implementations must ensure that the done function + // passed to the delegate newStream invokes the done function passed to + // NewStream. NewStream(ctx context.Context, ri RPCInfo, done func(), newStream func(ctx context.Context, done func()) (ClientStream, error)) (ClientStream, error) // Close closes the interceptor. Once called, no new calls to NewStream are // accepted. Ongoing calls to NewStream are allowed to complete. diff --git a/vendor/google.golang.org/grpc/internal/stats/labels.go b/vendor/google.golang.org/grpc/internal/stats/labels.go index fd33af51a..5ea898cb5 100644 --- a/vendor/google.golang.org/grpc/internal/stats/labels.go +++ b/vendor/google.golang.org/grpc/internal/stats/labels.go @@ -19,24 +19,56 @@ // Package stats provides internal stats related functionality. package stats -import "context" +import ( + "context" + "maps" +) -// Labels are the labels for metrics. -type Labels struct { - // TelemetryLabels are the telemetry labels to record. - TelemetryLabels map[string]string +// LabelCallback is a function that is executed when telemetry +// label keys are updated. +type LabelCallback func(map[string]string) +type telemetryLabelCallbackKey struct{} + +// UpdateLabels executes registered telemetry callbacks with the update labels. Labels +// are copied before being processed by any callbacks to ensure mutations are not +// shared among derived contexts. +// +// It is the responsibility of the registrant to handle conflicts or label resets. +func UpdateLabels(ctx context.Context, update map[string]string) { + executeTelemetryLabelCallbacks(ctx, update) } -type labelsKey struct{} +// RegisterTelemetryLabelCallback registers a callback function that is executed whenever +// telemetry labels are updated. +func RegisterTelemetryLabelCallback(ctx context.Context, callback LabelCallback) context.Context { + if callback == nil { + return ctx + } + + callbacks, ok := ctx.Value(telemetryLabelCallbackKey{}).([]LabelCallback) + if !ok { + return context.WithValue(ctx, telemetryLabelCallbackKey{}, []LabelCallback{callback}) + } + return context.WithValue(ctx, telemetryLabelCallbackKey{}, append(append([]LabelCallback(nil), callbacks...), callback)) -// GetLabels returns the Labels stored in the context, or nil if there is one. -func GetLabels(ctx context.Context) *Labels { - labels, _ := ctx.Value(labelsKey{}).(*Labels) - return labels } -// SetLabels sets the Labels in the context. -func SetLabels(ctx context.Context, labels *Labels) context.Context { - // could also append - return context.WithValue(ctx, labelsKey{}, labels) +// executeTelemetryLabelCallback runs the registered callbacks in the order they were +// registered on the context with the provided labels. If no callbacks are registered +// it does nothing. +// +// To ensure callbacks do not mutate the state of the provided label map it is copied +// before execution. +func executeTelemetryLabelCallbacks(ctx context.Context, labels map[string]string) { + callbacks, ok := ctx.Value(telemetryLabelCallbackKey{}).([]LabelCallback) + if !ok { + return + } + + labelsCopy := map[string]string{} + maps.Copy(labelsCopy, labels) + for _, callback := range callbacks { + callback(labelsCopy) + } + } diff --git a/vendor/google.golang.org/grpc/internal/transport/client_stream.go b/vendor/google.golang.org/grpc/internal/transport/client_stream.go index cd8152ef1..ad382b0fd 100644 --- a/vendor/google.golang.org/grpc/internal/transport/client_stream.go +++ b/vendor/google.golang.org/grpc/internal/transport/client_stream.go @@ -19,6 +19,7 @@ package transport import ( + "fmt" "sync/atomic" "golang.org/x/net/http2" @@ -28,6 +29,12 @@ import ( "google.golang.org/grpc/status" ) +// nonGRPCDataMaxLen is the maximum length of nonGRPCDataBuf. +// +// NOTE: If changed this value, you MUST update the corresponding test in: +// - /test/end2end_test.go:TestHTTPServerSendsNonGRPCHeaderSurfaceFurtherData +const nonGRPCDataMaxLen = 1024 + // ClientStream implements streaming functionality for a gRPC client. type ClientStream struct { Stream // Embed for common stream functionality. @@ -46,7 +53,11 @@ type ClientStream struct { // headerValid indicates whether a valid header was received. Only // meaningful after headerChan is closed (always call waitOnHeader() before // reading its value). - headerValid bool + headerValid bool + + nonGRPCStatus *status.Status // the initial status from the non-gRPC response header, finalized with collected data before closing. + nonGRPCDataBuf []byte // stores the data of a non-gRPC response. + noHeaders bool // set if the client never received headers (set only after the stream is done). headerChanClosed uint32 // set when headerChan is closed. Used to avoid closing headerChan multiple times. bytesReceived atomic.Bool // indicates whether any bytes have been received on this stream @@ -54,6 +65,29 @@ type ClientStream struct { statsHandler stats.Handler // nil for internal streams (e.g., health check, ORCA) where telemetry is not supported. } +func (s *ClientStream) startNonGRPCDataCollection(st *status.Status) { + s.nonGRPCStatus = st + s.nonGRPCDataBuf = make([]byte, 0, nonGRPCDataMaxLen) +} + +// finalizeNonGRPCStatus builds the terminal status by appending the collected +// response body to the original non-gRPC status message. +func (s *ClientStream) finalizeNonGRPCStatus() *status.Status { + msg := fmt.Sprintf("%s\ndata: %q", s.nonGRPCStatus.Message(), s.nonGRPCDataBuf) + return status.New(s.nonGRPCStatus.Code(), msg) +} + +// handleNonGRPCData collects non-gRPC body from the given data frame. +// It returns non-nil value when the stream should be closed with it. +func (s *ClientStream) handleNonGRPCData(f *parsedDataFrame) *status.Status { + n := min(f.data.Len(), nonGRPCDataMaxLen-len(s.nonGRPCDataBuf)) + s.nonGRPCDataBuf = append(s.nonGRPCDataBuf, f.data.ReadOnlyData()[0:n]...) + if len(s.nonGRPCDataBuf) >= nonGRPCDataMaxLen || f.StreamEnded() { + return s.finalizeNonGRPCStatus() + } + return nil +} + // Read reads an n byte message from the input stream. func (s *ClientStream) Read(n int) (mem.BufferSlice, error) { b, err := s.Stream.read(n) diff --git a/vendor/google.golang.org/grpc/internal/transport/controlbuf.go b/vendor/google.golang.org/grpc/internal/transport/controlbuf.go index 7efa52478..b9bae0249 100644 --- a/vendor/google.golang.org/grpc/internal/transport/controlbuf.go +++ b/vendor/google.golang.org/grpc/internal/transport/controlbuf.go @@ -29,6 +29,7 @@ import ( "golang.org/x/net/http2" "golang.org/x/net/http2/hpack" + "google.golang.org/grpc/internal/envconfig" "google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/mem" ) @@ -96,61 +97,70 @@ func (il *itemList) isEmpty() bool { return il.head == nil } -// The following defines various control items which could flow through -// the control buffer of transport. They represent different aspects of -// control tasks, e.g., flow control, settings, streaming resetting, etc. - -// maxQueuedTransportResponseFrames is the most queued "transport response" -// frames we will buffer before preventing new reads from occurring on the -// transport. These are control frames sent in response to client requests, -// such as RST_STREAM due to bad headers or settings acks. -const maxQueuedTransportResponseFrames = 50 +// maxQueuedControlBufferItems is the maximum number of frames (other than +// HEADERS and DATA) that we will buffer before preventing new reads from +// occurring on the transport. These are control frames sent in response to +// client requests, or frames that result in work being scheduled, such as +// RST_STREAM due to bad headers or settings acks. +var maxQueuedControlBufferItems = int(envconfig.ControlBufferThrottleLimit) type cbItem interface { - isTransportResponseFrame() bool + isThrottled() bool } +// throttledItem represents every item in the controlBuffer to which the overall +// throttling limit applies, other than outgoing HEADERS and DATA frames. +type throttledItem struct{} + +func (throttledItem) isThrottled() bool { return true } + +// The following defines various control items which could flow through +// the control buffer of transport. They represent different aspects of +// control tasks, e.g., flow control, settings, streaming resetting, etc. + // registerStream is used to register an incoming stream with loopy writer. type registerStream struct { + throttledItem streamID uint32 wq *writeQuota } -func (*registerStream) isTransportResponseFrame() bool { return false } - -// headerFrame is also used to register stream on the client-side. -type headerFrame struct { +type clientHeaders struct { streamID uint32 hf []hpack.HeaderField - endStream bool // Valid on server side. - initStream func(uint32) error // Used only on the client side. + initStream func(uint32) error onWrite func() - wq *writeQuota // write quota for the stream created. - cleanup *cleanupStream // Valid on the server side. - onOrphaned func(error) // Valid on client-side + wq *writeQuota + onOrphaned func(error) } -func (h *headerFrame) isTransportResponseFrame() bool { - return h.cleanup != nil && h.cleanup.rst // Results in a RST_STREAM +func (*clientHeaders) isThrottled() bool { return false } + +type serverHeaders struct { + streamID uint32 + hf []hpack.HeaderField + endStream bool + onWrite func() + cleanup *cleanupStream } +func (h *serverHeaders) isThrottled() bool { return false } + type cleanupStream struct { + throttledItem streamID uint32 rst bool rstCode http2.ErrCode onWrite func() } -func (c *cleanupStream) isTransportResponseFrame() bool { return c.rst } // Results in a RST_STREAM - type earlyAbortStream struct { + throttledItem streamID uint32 rst bool hf []hpack.HeaderField // Pre-built header fields } -func (*earlyAbortStream) isTransportResponseFrame() bool { return false } - type dataFrame struct { streamID uint32 endStream bool @@ -162,70 +172,60 @@ type dataFrame struct { onEachWrite func() } -func (*dataFrame) isTransportResponseFrame() bool { return false } +func (*dataFrame) isThrottled() bool { return false } type incomingWindowUpdate struct { + throttledItem streamID uint32 increment uint32 } -func (*incomingWindowUpdate) isTransportResponseFrame() bool { return false } - type outgoingWindowUpdate struct { + throttledItem streamID uint32 increment uint32 } -func (*outgoingWindowUpdate) isTransportResponseFrame() bool { - return false // window updates are throttled by thresholds -} - type incomingSettings struct { + throttledItem ss []http2.Setting } -func (*incomingSettings) isTransportResponseFrame() bool { return true } // Results in a settings ACK - type outgoingSettings struct { + throttledItem ss []http2.Setting } -func (*outgoingSettings) isTransportResponseFrame() bool { return false } - type incomingGoAway struct { + throttledItem } -func (*incomingGoAway) isTransportResponseFrame() bool { return false } - type goAway struct { + throttledItem code http2.ErrCode debugData []byte headsUp bool closeConn error // if set, loopyWriter will exit with this error } -func (*goAway) isTransportResponseFrame() bool { return false } - type ping struct { + throttledItem ack bool data [8]byte } -func (*ping) isTransportResponseFrame() bool { return true } - type outFlowControlSizeRequest struct { + throttledItem resp chan uint32 } -func (*outFlowControlSizeRequest) isTransportResponseFrame() bool { return false } - // closeConnection is an instruction to tell the loopy writer to flush the // framer and exit, which will cause the transport's connection to be closed // (by the client or server). The transport itself will close after the reader // encounters the EOF caused by the connection closure. -type closeConnection struct{} - -func (closeConnection) isTransportResponseFrame() bool { return false } +type closeConnection struct { + throttledItem +} type outStreamState int @@ -379,9 +379,9 @@ func (c *controlBuffer) executeAndPut(f func() bool, it cbItem) (bool, error) { c.consumerWaiting = false } c.list.enqueue(it) - if it.isTransportResponseFrame() { + if it.isThrottled() { c.transportResponseFrames++ - if c.transportResponseFrames == maxQueuedTransportResponseFrames { + if c.transportResponseFrames == maxQueuedControlBufferItems { // We are adding the frame that puts us over the threshold; create // a throttling channel. ch := make(chan struct{}) @@ -436,8 +436,8 @@ func (c *controlBuffer) getOnceLocked() (any, error) { return nil, nil } h := c.list.dequeue().(cbItem) - if h.isTransportResponseFrame() { - if c.transportResponseFrames == maxQueuedTransportResponseFrames { + if h.isThrottled() { + if c.transportResponseFrames == maxQueuedControlBufferItems { // We are removing the frame that put us over the // threshold; close and clear the throttling channel. ch := c.trfChan.Swap(nil) @@ -464,10 +464,8 @@ func (c *controlBuffer) finish() { // is still not aware of these yet. for head := c.list.dequeueAll(); head != nil; head = head.next { switch v := head.it.(type) { - case *headerFrame: - if v.onOrphaned != nil { // It will be nil on the server-side. - v.onOrphaned(ErrConnClosing) - } + case *clientHeaders: + v.onOrphaned(ErrConnClosing) case *dataFrame: if !v.processing { v.data.Free() @@ -680,42 +678,38 @@ func (l *loopyWriter) registerStreamHandler(h *registerStream) { l.estdStreams[h.streamID] = str } -func (l *loopyWriter) headerHandler(h *headerFrame) error { - if l.side == serverSide { - str, ok := l.estdStreams[h.streamID] - if !ok { - if l.logger.V(logLevel) { - l.logger.Infof("Unrecognized streamID %d in loopyWriter", h.streamID) - } - return nil - } - // Case 1.A: Server is responding back with headers. - if !h.endStream { - return l.writeHeader(h.streamID, h.endStream, h.hf, h.onWrite) +func (l *loopyWriter) serverHeaderHandler(hdr *serverHeaders) error { + str, ok := l.estdStreams[hdr.streamID] + if !ok { + if l.logger.V(logLevel) { + l.logger.Infof("Unrecognized streamID %d in loopyWriter", hdr.streamID) } - // else: Case 1.B: Server wants to close stream. + return nil + } - if str.state != empty { // either active or waiting on stream quota. - // add it str's list of items. - str.itl.enqueue(h) - return nil - } - if err := l.writeHeader(h.streamID, h.endStream, h.hf, h.onWrite); err != nil { - return err - } - return l.cleanupStreamHandler(h.cleanup) + // Case 1: Server is responding back with headers. + if !hdr.endStream { + return l.writeHeader(hdr.streamID, hdr.endStream, hdr.hf, hdr.onWrite) + } + + // Case 2: Server is closing the stream. + if str.state != empty { // either active or waiting on stream quota. + str.itl.enqueue(hdr) + return nil + } + if err := l.writeHeader(hdr.streamID, hdr.endStream, hdr.hf, hdr.onWrite); err != nil { + return err } - // Case 2: Client wants to originate stream. + return l.cleanupStreamHandler(hdr.cleanup) +} + +func (l *loopyWriter) clientHeaderHandler(hdr *clientHeaders) error { str := &outStream{ - id: h.streamID, + id: hdr.streamID, state: empty, itl: &itemList{}, - wq: h.wq, + wq: hdr.wq, } - return l.originateStream(str, h) -} - -func (l *loopyWriter) originateStream(str *outStream, hdr *headerFrame) error { // l.draining is set when handling GoAway. In which case, we want to avoid // creating new streams. if l.draining { @@ -726,7 +720,7 @@ func (l *loopyWriter) originateStream(str *outStream, hdr *headerFrame) error { if err := hdr.initStream(str.id); err != nil { return err } - if err := l.writeHeader(str.id, hdr.endStream, hdr.hf, hdr.onWrite); err != nil { + if err := l.writeHeader(str.id, false, hdr.hf, hdr.onWrite); err != nil { return err } l.estdStreams[str.id] = str @@ -882,8 +876,10 @@ func (l *loopyWriter) handle(i any) error { return l.incomingSettingsHandler(i) case *outgoingSettings: return l.outgoingSettingsHandler(i) - case *headerFrame: - return l.headerHandler(i) + case *clientHeaders: + return l.clientHeaderHandler(i) + case *serverHeaders: + return l.serverHeaderHandler(i) case *registerStream: l.registerStreamHandler(i) case *cleanupStream: @@ -956,39 +952,16 @@ func (l *loopyWriter) processData() (bool, error) { // from data is copied to h to make as big as the maximum possible HTTP2 frame // size. - if len(dataItem.h) == 0 && reader.Remaining() == 0 { // Empty data frame - // Client sends out empty data frame with endStream = true - if err := l.framer.writeData(dataItem.streamID, dataItem.endStream, nil); err != nil { - return false, err - } - str.itl.dequeue() // remove the empty data item from stream - reader.Close() - if str.itl.isEmpty() { - str.state = empty - } else if trailer, ok := str.itl.peek().(*headerFrame); ok { // the next item is trailers. - if err := l.writeHeader(trailer.streamID, trailer.endStream, trailer.hf, trailer.onWrite); err != nil { - return false, err - } - if err := l.cleanupStreamHandler(trailer.cleanup); err != nil { - return false, err - } - } else { - l.activeStreams.enqueue(str) - } - return false, nil - } - + isEmpty := len(dataItem.h) == 0 && reader.Remaining() == 0 // Figure out the maximum size we can send maxSize := http2MaxFrameLen - if strQuota := int(l.oiws) - str.bytesOutStanding; strQuota <= 0 { // stream-level flow control. + strQuota := int(l.oiws) - str.bytesOutStanding + if strQuota <= 0 && !isEmpty { // stream-level flow control. str.state = waitingOnStreamQuota return false, nil - } else if maxSize > strQuota { - maxSize = strQuota - } - if maxSize > int(l.sendQuota) { // connection-level flow control. - maxSize = int(l.sendQuota) } + maxSize = min(maxSize, max(strQuota, 0)) + maxSize = min(maxSize, int(l.sendQuota)) // connection-level flow control. // Compute how much of the header and data we can send within quota and max frame length hSize := min(maxSize, len(dataItem.h)) dSize := min(maxSize-hSize, reader.Remaining()) @@ -1039,19 +1012,23 @@ func (l *loopyWriter) processData() (bool, error) { reader.Close() str.itl.dequeue() } + return false, l.updateStreamAfterWrite(str) +} + +func (l *loopyWriter) updateStreamAfterWrite(str *outStream) error { if str.itl.isEmpty() { str.state = empty - } else if trailer, ok := str.itl.peek().(*headerFrame); ok { // The next item is trailers. + } else if trailer, ok := str.itl.peek().(*serverHeaders); ok { // the next item is trailers. if err := l.writeHeader(trailer.streamID, trailer.endStream, trailer.hf, trailer.onWrite); err != nil { - return false, err + return err } if err := l.cleanupStreamHandler(trailer.cleanup); err != nil { - return false, err + return err } } else if int(l.oiws)-str.bytesOutStanding <= 0 { // Ran out of stream quota. str.state = waitingOnStreamQuota } else { // Otherwise add it back to the list of active streams. l.activeStreams.enqueue(str) } - return false, nil + return nil } diff --git a/vendor/google.golang.org/grpc/internal/transport/flowcontrol.go b/vendor/google.golang.org/grpc/internal/transport/flowcontrol.go index 7cfbc9637..98cef9ec2 100644 --- a/vendor/google.golang.org/grpc/internal/transport/flowcontrol.go +++ b/vendor/google.golang.org/grpc/internal/transport/flowcontrol.go @@ -115,7 +115,6 @@ func (f *trInFlow) getSize() uint32 { return atomic.LoadUint32(&f.effectiveWindowSize) } -// TODO(mmukhi): Simplify this code. // inFlow deals with inbound flow control type inFlow struct { mu sync.Mutex @@ -174,14 +173,14 @@ func (f *inFlow) maybeAdjust(n uint32) uint32 { // onData is invoked when some data frame is received. It updates pendingData. func (f *inFlow) onData(n uint32) error { f.mu.Lock() + defer f.mu.Unlock() + f.pendingData += n if f.pendingData+f.pendingUpdate > f.limit+f.delta { limit := f.limit rcvd := f.pendingData + f.pendingUpdate - f.mu.Unlock() return fmt.Errorf("received %d-bytes data exceeding the limit %d bytes", rcvd, limit) } - f.mu.Unlock() return nil } @@ -189,8 +188,9 @@ func (f *inFlow) onData(n uint32) error { // to be sent to the peer. func (f *inFlow) onRead(n uint32) uint32 { f.mu.Lock() + defer f.mu.Unlock() + if f.pendingData == 0 { - f.mu.Unlock() return 0 } f.pendingData -= n @@ -205,9 +205,7 @@ func (f *inFlow) onRead(n uint32) uint32 { if f.pendingUpdate >= f.limit/4 { wu := f.pendingUpdate f.pendingUpdate = 0 - f.mu.Unlock() return wu } - f.mu.Unlock() return 0 } diff --git a/vendor/google.golang.org/grpc/internal/transport/handler_server.go b/vendor/google.golang.org/grpc/internal/transport/handler_server.go index 7ab3422b8..a8356c9ad 100644 --- a/vendor/google.golang.org/grpc/internal/transport/handler_server.go +++ b/vendor/google.golang.org/grpc/internal/transport/handler_server.go @@ -479,8 +479,8 @@ func (ht *serverHandlerTransport) runStream() { func (ht *serverHandlerTransport) incrMsgRecv() {} -func (ht *serverHandlerTransport) Drain(string) { - panic("Drain() is not implemented") +func (ht *serverHandlerTransport) Drain(s string) { + ht.Close(errors.New(s)) } // mapRecvMsgError returns the non-nil err into the appropriate diff --git a/vendor/google.golang.org/grpc/internal/transport/http2_client.go b/vendor/google.golang.org/grpc/internal/transport/http2_client.go index d6bc6a6cc..822c09ba6 100644 --- a/vendor/google.golang.org/grpc/internal/transport/http2_client.go +++ b/vendor/google.golang.org/grpc/internal/transport/http2_client.go @@ -39,6 +39,7 @@ import ( "google.golang.org/grpc/internal" "google.golang.org/grpc/internal/channelz" icredentials "google.golang.org/grpc/internal/credentials" + "google.golang.org/grpc/internal/envconfig" "google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/internal/grpcsync" "google.golang.org/grpc/internal/grpcutil" @@ -318,7 +319,13 @@ func NewHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts } writeBufSize := opts.WriteBufferSize readBufSize := opts.ReadBufferSize + // The default header list size is moving from 16MB to 8KB. The 8KB limit + // is only used if Enable8KBDefaultHeaderListSize is true; otherwise, the + // old 16MB default is used. User-specified options always take precedence. maxHeaderListSize := defaultClientMaxHeaderListSize + if envconfig.Enable8KBDefaultHeaderListSize { + maxHeaderListSize = upcomingDefaultHeaderListSize + } if opts.MaxHeaderListSize != nil { maxHeaderListSize = *opts.MaxHeaderListSize } @@ -799,9 +806,8 @@ func (t *http2Client) NewStream(ctx context.Context, callHdr *CallHdr, handler s close(s.headerChan) } } - hdr := &headerFrame{ - hf: headerFields, - endStream: false, + hdr := &clientHeaders{ + hf: headerFields, initStream: func(uint32) error { t.mu.Lock() // TODO: handle transport closure in loopy instead and remove this @@ -879,8 +885,8 @@ func (t *http2Client) NewStream(ctx context.Context, callHdr *CallHdr, handler s return false } } - if sz > int64(upcomingDefaultHeaderListSize) { - t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In a future release, this will be restricted to %d bytes.", sz, upcomingDefaultHeaderListSize, upcomingDefaultHeaderListSize) + if !envconfig.Enable8KBDefaultHeaderListSize && sz > int64(upcomingDefaultHeaderListSize) { + t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In release v1.82.0, GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE will be enabled by default, enforcing this limit.", sz, upcomingDefaultHeaderListSize) } return true } @@ -1224,10 +1230,30 @@ func (t *http2Client) handleData(f *parsedDataFrame) { t.closeStream(s, io.EOF, true, http2.ErrCodeFlowControl, status.New(codes.Internal, err.Error()), nil, false) return } + + if s.nonGRPCStatus != nil { + // The frame should be handled as a non-gRPC response body + st := s.handleNonGRPCData(f) + if st != nil { + t.closeStream(s, st.Err(), true, http2.ErrCodeProtocol, st, nil, true) + return + } + if w := s.fc.onRead(size); w > 0 { + t.controlBuf.put(&outgoingWindowUpdate{ + streamID: s.id, + increment: w, + }) + } + return + } + dataLen := f.data.Len() if f.Header().Flags.Has(http2.FlagDataPadded) { if w := s.fc.onRead(size - uint32(dataLen)); w > 0 { - t.controlBuf.put(&outgoingWindowUpdate{s.id, w}) + t.controlBuf.put(&outgoingWindowUpdate{ + streamID: s.id, + increment: w, + }) } } if dataLen > 0 { @@ -1468,6 +1494,17 @@ func (t *http2Client) operateHeaders(frame *http2.MetaHeadersFrame) { return } + // If we are collecting non-gRPC response data and receive a trailing + // HEADERS frame with END_STREAM, finalize the buffered data and close + // the stream. + if s.nonGRPCStatus != nil { + if endStream { + st := s.finalizeNonGRPCStatus() + t.closeStream(s, st.Err(), true, http2.ErrCodeProtocol, st, nil, true) + } + return + } + var ( // If a gRPC Response-Headers has already been received, then it means // that the peer is speaking gRPC and we are in gRPC mode. @@ -1568,7 +1605,12 @@ func (t *http2Client) operateHeaders(frame *http2.MetaHeadersFrame) { } se := status.New(grpcErrorCode, strings.Join(errs, "; ")) - t.closeStream(s, se.Err(), true, http2.ErrCodeProtocol, se, nil, endStream) + if endStream { + t.closeStream(s, se.Err(), true, http2.ErrCodeProtocol, se, nil, true) + return + } + + s.startNonGRPCDataCollection(se) return } @@ -1839,7 +1881,7 @@ func (t *http2Client) getOutFlowWindow() int64 { resp := make(chan uint32, 1) timer := time.NewTimer(time.Second) defer timer.Stop() - t.controlBuf.put(&outFlowControlSizeRequest{resp}) + t.controlBuf.put(&outFlowControlSizeRequest{resp: resp}) select { case sz := <-resp: return int64(sz) diff --git a/vendor/google.golang.org/grpc/internal/transport/http2_server.go b/vendor/google.golang.org/grpc/internal/transport/http2_server.go index 3a8c36e4f..be8ae9f9c 100644 --- a/vendor/google.golang.org/grpc/internal/transport/http2_server.go +++ b/vendor/google.golang.org/grpc/internal/transport/http2_server.go @@ -38,11 +38,13 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/grpc/internal" + "google.golang.org/grpc/internal/envconfig" "google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/internal/grpcutil" "google.golang.org/grpc/internal/pretty" istatus "google.golang.org/grpc/internal/status" "google.golang.org/grpc/internal/syscall" + transportinternal "google.golang.org/grpc/internal/transport/internal" "google.golang.org/grpc/mem" "google.golang.org/grpc/codes" @@ -165,7 +167,13 @@ func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport, } writeBufSize := config.WriteBufferSize readBufSize := config.ReadBufferSize + // The default header list size is moving from 16MB to 8KB. The 8KB limit + // is only used if Enable8KBDefaultHeaderListSize is true; otherwise, the + // old 16MB default is used. User-specified options always take precedence. maxHeaderListSize := defaultServerMaxHeaderListSize + if envconfig.Enable8KBDefaultHeaderListSize { + maxHeaderListSize = upcomingDefaultHeaderListSize + } if config.MaxHeaderListSize != nil { maxHeaderListSize = *config.MaxHeaderListSize } @@ -802,7 +810,10 @@ func (t *http2Server) handleData(f *parsedDataFrame) { dataLen := f.data.Len() if f.Header().Flags.Has(http2.FlagDataPadded) { if w := s.fc.onRead(size - uint32(dataLen)); w > 0 { - t.controlBuf.put(&outgoingWindowUpdate{s.id, w}) + t.controlBuf.put(&outgoingWindowUpdate{ + streamID: s.id, + increment: w, + }) } } if dataLen > 0 { @@ -948,8 +959,8 @@ func (t *http2Server) checkForHeaderListSize(hf []hpack.HeaderField) bool { return false } } - if sz > int64(upcomingDefaultHeaderListSize) { - t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In a future release, this will be restricted to %d bytes.", sz, upcomingDefaultHeaderListSize, upcomingDefaultHeaderListSize) + if !envconfig.Enable8KBDefaultHeaderListSize && sz > int64(upcomingDefaultHeaderListSize) { + t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In release v1.82.0, GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE will be enabled by default, enforcing this limit.", sz, upcomingDefaultHeaderListSize) } return true } @@ -1039,7 +1050,7 @@ func (t *http2Server) writeHeaderLocked(s *ServerStream) error { headerFields = append(headerFields, hpack.HeaderField{Name: "grpc-encoding", Value: s.sendCompress}) } headerFields = appendHeaderFieldsFromMD(headerFields, s.header) - hf := &headerFrame{ + hf := &serverHeaders{ streamID: s.id, hf: headerFields, endStream: false, @@ -1107,7 +1118,7 @@ func (t *http2Server) writeStatus(s *ServerStream, st *status.Status) error { // Attach the trailer metadata. headerFields = appendHeaderFieldsFromMD(headerFields, s.trailer) - trailingHeader := &headerFrame{ + trailingHeader := &serverHeaders{ streamID: s.id, hf: headerFields, endStream: true, @@ -1317,7 +1328,7 @@ func (t *http2Server) deleteStream(s *ServerStream, eosReceived bool) { } // finishStream closes the stream and puts the trailing headerFrame into controlbuf. -func (t *http2Server) finishStream(s *ServerStream, rst bool, rstCode http2.ErrCode, hdr *headerFrame, eosReceived bool) { +func (t *http2Server) finishStream(s *ServerStream, rst bool, rstCode http2.ErrCode, hdr *serverHeaders, eosReceived bool) { // In case stream sending and receiving are invoked in separate // goroutines (e.g., bi-directional streaming), cancel needs to be // called to interrupt the potential blocking on other goroutines. @@ -1441,14 +1452,14 @@ func (t *http2Server) socketMetrics() *channelz.EphemeralSocketMetrics { func (t *http2Server) incrMsgSent() { if channelz.IsOn() { t.channelz.SocketMetrics.MessagesSent.Add(1) - t.channelz.SocketMetrics.LastMessageSentTimestamp.Add(1) + t.channelz.SocketMetrics.LastMessageSentTimestamp.Store(transportinternal.TimeNowFunc()) } } func (t *http2Server) incrMsgRecv() { if channelz.IsOn() { t.channelz.SocketMetrics.MessagesReceived.Add(1) - t.channelz.SocketMetrics.LastMessageReceivedTimestamp.Add(1) + t.channelz.SocketMetrics.LastMessageReceivedTimestamp.Store(transportinternal.TimeNowFunc()) } } @@ -1456,7 +1467,7 @@ func (t *http2Server) getOutFlowWindow() int64 { resp := make(chan uint32, 1) timer := time.NewTimer(time.Second) defer timer.Stop() - t.controlBuf.put(&outFlowControlSizeRequest{resp}) + t.controlBuf.put(&outFlowControlSizeRequest{resp: resp}) select { case sz := <-resp: return int64(sz) diff --git a/vendor/google.golang.org/grpc/internal/grpcutil/regex.go b/vendor/google.golang.org/grpc/internal/transport/internal/internal.go similarity index 59% rename from vendor/google.golang.org/grpc/internal/grpcutil/regex.go rename to vendor/google.golang.org/grpc/internal/transport/internal/internal.go index 7a092b2b8..a7c7c7d5a 100644 --- a/vendor/google.golang.org/grpc/internal/grpcutil/regex.go +++ b/vendor/google.golang.org/grpc/internal/transport/internal/internal.go @@ -1,6 +1,6 @@ /* * - * Copyright 2021 gRPC authors. + * Copyright 2026 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,10 @@ * */ -package grpcutil +// Package internal contains functionality internal to the transport package. +package internal -import "regexp" - -// FullMatchWithRegex returns whether the full text matches the regex provided. -func FullMatchWithRegex(re *regexp.Regexp, text string) bool { - if len(text) == 0 { - return re.MatchString(text) - } - re.Longest() - rem := re.FindString(text) - return len(rem) == len(text) -} +// TimeNowFunc is a variable that can be set to override the default behavior of +// getting the current time in nanoseconds. It is used in transport code to set +// channelz timestamps, and is exposed here for testing purposes. +var TimeNowFunc func() int64 diff --git a/vendor/google.golang.org/grpc/internal/transport/transport.go b/vendor/google.golang.org/grpc/internal/transport/transport.go index 1e224576e..6dfae3984 100644 --- a/vendor/google.golang.org/grpc/internal/transport/transport.go +++ b/vendor/google.golang.org/grpc/internal/transport/transport.go @@ -35,6 +35,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/internal/channelz" + "google.golang.org/grpc/internal/transport/internal" "google.golang.org/grpc/keepalive" "google.golang.org/grpc/mem" "google.golang.org/grpc/metadata" @@ -46,6 +47,10 @@ import ( const logLevel = 2 +func init() { + internal.TimeNowFunc = func() int64 { return time.Now().UnixNano() } +} + // recvMsg represents the received msg from the transport. All transport // protocol specific info has been removed. type recvMsg struct { diff --git a/vendor/google.golang.org/grpc/internal/xds/balancer/cdsbalancer/configbuilder.go b/vendor/google.golang.org/grpc/internal/xds/balancer/cdsbalancer/configbuilder.go index 54bede893..336d24000 100644 --- a/vendor/google.golang.org/grpc/internal/xds/balancer/cdsbalancer/configbuilder.go +++ b/vendor/google.golang.org/grpc/internal/xds/balancer/cdsbalancer/configbuilder.go @@ -24,7 +24,8 @@ import ( "maps" "slices" - "google.golang.org/grpc/internal/balancer/weight" + "google.golang.org/grpc/experimental/balancer/hostname" + "google.golang.org/grpc/experimental/balancer/weight" "google.golang.org/grpc/internal/envconfig" "google.golang.org/grpc/internal/hierarchy" internalserviceconfig "google.golang.org/grpc/internal/serviceconfig" @@ -181,7 +182,7 @@ func buildClusterImplConfigForDNS(g *nameGenerator, config *xdsresource.ClusterC // LB policies that rely on locality information (like weighted_target) // continue to work. localityStr := xdsinternal.LocalityString(clients.Locality{}) - retEndpoint = xdsresource.SetHostname(hierarchy.SetInEndpoint(retEndpoint, []string{pName, localityStr}), clusterUpdate.DNSHostName) + retEndpoint = hostname.Set(hierarchy.SetInEndpoint(retEndpoint, []string{pName, localityStr}), clusterUpdate.DNSHostName) // Set the locality weight to 1. This is required because the child policy // like weighted_target which relies on locality weights to distribute // traffic. These policies may drop traffic if the weight is 0. diff --git a/vendor/google.golang.org/grpc/internal/xds/balancer/clusterimpl/clusterimpl.go b/vendor/google.golang.org/grpc/internal/xds/balancer/clusterimpl/clusterimpl.go index 8c8f00d1d..73247d4d0 100644 --- a/vendor/google.golang.org/grpc/internal/xds/balancer/clusterimpl/clusterimpl.go +++ b/vendor/google.golang.org/grpc/internal/xds/balancer/clusterimpl/clusterimpl.go @@ -575,18 +575,18 @@ func (b *clusterImplBalancer) NewSubConn(addrs []resolver.Address, opts balancer newAddrs[i] = xdsinternal.SetXDSHandshakeClusterName(addr, clusterName) newAddrs[i] = xds.SetHandshakeInfo(newAddrs[i], &b.xdsHIPtr) - hostname := xdsresource.Hostname(addr) + host := xdsresource.Hostname(addr) // If the hostname contains a port, strip it. Per [RFC 6066, Section // 3](https://www.rfc-editor.org/rfc/rfc6066.html#section-3), the SNI // may only contain a qualified DNS hostname, which excludes port // numbers. - h, _, err := net.SplitHostPort(hostname) + h, _, err := net.SplitHostPort(host) if err == nil { - hostname = h + host = h } // Store hostname in the address attributes, so that it can be used in // the client handshake. - newAddrs[i] = xds.SetAddressHostname(newAddrs[i], hostname) + newAddrs[i] = xds.SetAddressHostname(newAddrs[i], host) } var sc balancer.SubConn scw := &scWrapper{} diff --git a/vendor/google.golang.org/grpc/internal/xds/balancer/clusterimpl/picker.go b/vendor/google.golang.org/grpc/internal/xds/balancer/clusterimpl/picker.go index b808f6dd7..00ed6da3a 100644 --- a/vendor/google.golang.org/grpc/internal/xds/balancer/clusterimpl/picker.go +++ b/vendor/google.golang.org/grpc/internal/xds/balancer/clusterimpl/picker.go @@ -20,7 +20,6 @@ package clusterimpl import ( "context" - "maps" v3orcapb "github.com/cncf/xds/go/xds/data/orca/v3" "google.golang.org/grpc/balancer" @@ -95,24 +94,10 @@ type picker struct { metrics *xdsresource.LRSReportEndpointMetricsConfig } -func telemetryLabels(ctx context.Context) map[string]string { - if ctx == nil { - return nil - } - labels := stats.GetLabels(ctx) - if labels == nil { - return nil - } - return labels.TelemetryLabels -} - func (d *picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { - // Unconditionally set labels if present, even dropped or queued RPC's can + // Unconditionally update labels if present, even dropped or queued RPC's can // use these labels. - labels := telemetryLabels(info.Ctx) - if labels != nil { - maps.Copy(labels, d.telemetryLabels) - } + stats.UpdateLabels(info.Ctx, d.telemetryLabels) // Don't drop unless the inner picker is READY. Similar to // https://github.com/grpc/grpc-go/issues/2622. @@ -167,10 +152,13 @@ func (d *picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { return pr, err } - if labels != nil { - labels["grpc.lb.locality"] = xdsinternal.LocalityString(lID) - labels["grpc.lb.backend_service"] = d.clusterName - } + stats.UpdateLabels( + info.Ctx, + map[string]string{ + "grpc.lb.locality": xdsinternal.LocalityString(lID), + "grpc.lb.backend_service": d.clusterName, + }, + ) if d.loadStore != nil { locality := clients.Locality{Region: lID.Region, Zone: lID.Zone, SubZone: lID.SubZone} diff --git a/vendor/google.golang.org/grpc/internal/xds/httpfilter/extconfig.go b/vendor/google.golang.org/grpc/internal/xds/httpfilter/extconfig.go new file mode 100644 index 000000000..f93ab5bfa --- /dev/null +++ b/vendor/google.golang.org/grpc/internal/xds/httpfilter/extconfig.go @@ -0,0 +1,88 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package httpfilter + +import ( + "fmt" + "regexp" + + "google.golang.org/grpc/internal/xds/matcher" + + v3mutationpb "github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3" + v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" +) + +// HeaderMutationRules specifies the rules for what modifications an external +// processing server may make to headers sent on the data plane RPC. +type HeaderMutationRules struct { + // AllowExpr specifies a regular expression that matches the headers that can + // be mutated. + AllowExpr *regexp.Regexp + // DisallowExpr specifies a regular expression that matches the headers that + // cannot be mutated. This overrides the above allowExpr if a header matches + // both. + DisallowExpr *regexp.Regexp + // DisallowAll specifies that no header mutations are allowed. This overrides + // all other settings. + DisallowAll bool + // DisallowIsError specifies whether to return an error if a header mutation + // is disallowed. If true, the data plane RPC will be failed with a grpc + // status code of Unknown. + DisallowIsError bool +} + +// ConvertStringMatchers converts a slice of protobuf StringMatcher messages to +// a slice of matcher.StringMatcher. +func ConvertStringMatchers(patterns []*v3matcherpb.StringMatcher) ([]matcher.StringMatcher, error) { + matchers := make([]matcher.StringMatcher, 0, len(patterns)) + for _, p := range patterns { + sm, err := matcher.StringMatcherFromProto(p) + if err != nil { + return nil, err + } + matchers = append(matchers, sm) + } + return matchers, nil +} + +// HeaderMutationRulesFromProto converts a protobuf HeaderMutationRules proto +// message to a HeaderMutationRules struct. +func HeaderMutationRulesFromProto(mr *v3mutationpb.HeaderMutationRules) (HeaderMutationRules, error) { + var rules HeaderMutationRules + if mr == nil { + return rules, nil + } + if allowExpr := mr.GetAllowExpression(); allowExpr != nil { + re, err := matcher.CompileSafeRegex(allowExpr.GetRegex()) + if err != nil { + return rules, fmt.Errorf("httpfilter: %v", err) + } + rules.AllowExpr = re + } + if disallowExpr := mr.GetDisallowExpression(); disallowExpr != nil { + re, err := matcher.CompileSafeRegex(disallowExpr.GetRegex()) + if err != nil { + return rules, fmt.Errorf("httpfilter: %v", err) + } + rules.DisallowExpr = re + } + rules.DisallowAll = mr.GetDisallowAll().GetValue() + rules.DisallowIsError = mr.GetDisallowIsError().GetValue() + return rules, nil +} diff --git a/vendor/google.golang.org/grpc/internal/xds/httpfilter/httpfilter.go b/vendor/google.golang.org/grpc/internal/xds/httpfilter/httpfilter.go index 0b004f5c3..b37ed9d15 100644 --- a/vendor/google.golang.org/grpc/internal/xds/httpfilter/httpfilter.go +++ b/vendor/google.golang.org/grpc/internal/xds/httpfilter/httpfilter.go @@ -31,6 +31,16 @@ type FilterConfig interface { isFilterConfig() } +// DisabledFilterConfig represents a disabled filter override. It implements the +// FilterConfig interface and can be returned by ParseFilterConfigOverride to +// indicate that the filter should be disabled. It is not used as a config for +// any filter, and is only used as a marker in the override configuration. For +// more information, see +// envoyproxy.io/docs/envoy/latest/intro/arch_overview/http/http_filters#route-based-filter-chain +type DisabledFilterConfig struct{} + +func (DisabledFilterConfig) isFilterConfig() {} + // Builder defines the parsing functionality of an HTTP filter. A Builder may // optionally implement either ClientFilterBuilder or ServerFilterBuilder or // both, indicating it is capable of working on the client side or server side diff --git a/vendor/google.golang.org/grpc/internal/xds/matcher/matcher_header.go b/vendor/google.golang.org/grpc/internal/xds/matcher/matcher_header.go index 780257ec3..efda7f8a9 100644 --- a/vendor/google.golang.org/grpc/internal/xds/matcher/matcher_header.go +++ b/vendor/google.golang.org/grpc/internal/xds/matcher/matcher_header.go @@ -24,7 +24,6 @@ import ( "strconv" "strings" - "google.golang.org/grpc/internal/grpcutil" "google.golang.org/grpc/metadata" ) @@ -94,7 +93,7 @@ func (hrm *HeaderRegexMatcher) Match(md metadata.MD) bool { if !ok { return false } - return grpcutil.FullMatchWithRegex(hrm.re, v) != hrm.invert + return hrm.re.MatchString(v) != hrm.invert } func (hrm *HeaderRegexMatcher) String() string { diff --git a/vendor/google.golang.org/grpc/internal/xds/matcher/string_matcher.go b/vendor/google.golang.org/grpc/internal/xds/matcher/string_matcher.go index a6cf05dd3..edcc36c83 100644 --- a/vendor/google.golang.org/grpc/internal/xds/matcher/string_matcher.go +++ b/vendor/google.golang.org/grpc/internal/xds/matcher/string_matcher.go @@ -27,7 +27,6 @@ import ( "strings" v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" - "google.golang.org/grpc/internal/grpcutil" ) // StringMatcher contains match criteria for matching a string, and is an @@ -70,7 +69,7 @@ func (sm StringMatcher) Match(input string) bool { } return strings.Contains(input, *sm.containsMatch) case sm.regexMatch != nil: - return grpcutil.FullMatchWithRegex(sm.regexMatch, input) + return sm.regexMatch.MatchString(input) } return false } @@ -116,11 +115,11 @@ func StringMatcherFromProto(matcherProto *v3matcherpb.StringMatcher) (StringMatc matcher.suffixMatch = newStrPtr(&mt.Suffix, matcher.ignoreCase) case *v3matcherpb.StringMatcher_SafeRegex: regex := matcherProto.GetSafeRegex().GetRegex() - re, err := regexp.Compile(regex) + re, err := CompileSafeRegex(regex) if err != nil { return StringMatcher{}, fmt.Errorf("safe_regex matcher %q is invalid", regex) } - matcher.regexMatch = re + matcher = NewRegexStringMatcher(re) case *v3matcherpb.StringMatcher_Contains: if matcherProto.GetContains() == "" { return StringMatcher{}, errors.New("empty contains is not allowed in StringMatcher") @@ -217,3 +216,14 @@ func (sm StringMatcher) Equal(other StringMatcher) bool { } return true } + +// CompileSafeRegex attempts to compile the provided pattern as a regular +// expression. It first compiles the unanchored pattern to catch syntax errors +// and then compiles and returns the explicitly anchored pattern to guarantee +// full-string matching. +func CompileSafeRegex(pattern string) (*regexp.Regexp, error) { + if _, err := regexp.Compile(pattern); err != nil { + return nil, err + } + return regexp.Compile(fmt.Sprintf("^(?:%s)$", pattern)) +} diff --git a/vendor/google.golang.org/grpc/internal/xds/rbac/matchers.go b/vendor/google.golang.org/grpc/internal/xds/rbac/matchers.go index fede2fe76..cf6fbcead 100644 --- a/vendor/google.golang.org/grpc/internal/xds/rbac/matchers.go +++ b/vendor/google.golang.org/grpc/internal/xds/rbac/matchers.go @@ -21,7 +21,6 @@ import ( "fmt" "net" "net/netip" - "regexp" v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" @@ -79,7 +78,7 @@ func (pm *policyMatcher) match(data *rpcData) bool { func matchersFromPermissions(permissions []*v3rbacpb.Permission) ([]matcher, error) { var matchers []matcher for _, permission := range permissions { - switch permission.GetRule().(type) { + switch p := permission.GetRule().(type) { case *v3rbacpb.Permission_AndRules: mList, err := matchersFromPermissions(permission.GetAndRules().Rules) if err != nil { @@ -121,16 +120,24 @@ func matchersFromPermissions(permissions []*v3rbacpb.Permission) ([]matcher, err if err != nil { return nil, err } + if len(mList) != 1 { + return nil, fmt.Errorf("NotRule must contain exactly one rule") + } matchers = append(matchers, ¬Matcher{matcherToNot: mList[0]}) case *v3rbacpb.Permission_Metadata: - // Never matches - so no-op if not inverted, always match if - // inverted. - if permission.GetMetadata().GetInvert() { // Test metadata being no-op and also metadata with invert always matching + if permission.GetMetadata().GetInvert() { matchers = append(matchers, &alwaysMatcher{}) + } else { + matchers = append(matchers, &neverMatcher{}) } case *v3rbacpb.Permission_RequestedServerName: - // Not supported in gRPC RBAC currently - a permission typed as - // requested server name in the initial config will be a no-op. + m, err := newRequestedServerNameMatcher(permission.GetRequestedServerName()) + if err != nil { + return nil, err + } + matchers = append(matchers, m) + default: + return nil, fmt.Errorf("unsupported permission rule type: %T", p) } } return matchers, nil @@ -139,7 +146,7 @@ func matchersFromPermissions(permissions []*v3rbacpb.Permission) ([]matcher, err func matchersFromPrincipals(principals []*v3rbacpb.Principal) ([]matcher, error) { var matchers []matcher for _, principal := range principals { - switch principal.GetIdentifier().(type) { + switch p := principal.GetIdentifier().(type) { case *v3rbacpb.Principal_AndIds: mList, err := matchersFromPrincipals(principal.GetAndIds().Ids) if err != nil { @@ -179,16 +186,29 @@ func matchersFromPrincipals(principals []*v3rbacpb.Principal) ([]matcher, error) return nil, err } matchers = append(matchers, m) + case *v3rbacpb.Principal_Metadata: + if principal.GetMetadata().GetInvert() { + matchers = append(matchers, &alwaysMatcher{}) + } else { + matchers = append(matchers, &neverMatcher{}) + } case *v3rbacpb.Principal_NotId: mList, err := matchersFromPrincipals([]*v3rbacpb.Principal{{Identifier: principal.GetNotId().Identifier}}) if err != nil { return nil, err } + if len(mList) != 1 { + return nil, fmt.Errorf("NotId must contain exactly one identifier") + } matchers = append(matchers, ¬Matcher{matcherToNot: mList[0]}) case *v3rbacpb.Principal_SourceIp: - // The source ip principal identifier is deprecated. Thus, a - // principal typed as a source ip in the identifier will be a no-op. - // The config should use DirectRemoteIp instead. + // The source ip principal identifier is deprecated, but gRPC RBAC + // treats it as equivalent to direct_remote_ip as per A41. + m, err := newRemoteIPMatcher(principal.GetSourceIp()) + if err != nil { + return nil, err + } + matchers = append(matchers, m) case *v3rbacpb.Principal_RemoteIp: // RBAC in gRPC treats direct_remote_ip and remote_ip as logically // equivalent, as per A41. @@ -197,9 +217,8 @@ func matchersFromPrincipals(principals []*v3rbacpb.Principal) ([]matcher, error) return nil, err } matchers = append(matchers, m) - case *v3rbacpb.Principal_Metadata: - // Not supported in gRPC RBAC currently - a principal typed as - // Metadata in the initial config will be a no-op. + default: + return nil, fmt.Errorf("unsupported principal identifier type: %T", p) } } return matchers, nil @@ -249,6 +268,16 @@ func (am *alwaysMatcher) match(*rpcData) bool { return true } +// neverMatcher is a matcher that will never match. This logically represents a +// permission or principal that is unsupported in gRPC. neverMatcher implements +// the matcher interface. +type neverMatcher struct { +} + +func (nm *neverMatcher) match(*rpcData) bool { + return false +} + // notMatcher is a matcher that nots an underlying matcher. notMatcher // implements the matcher interface. type notMatcher struct { @@ -271,7 +300,7 @@ func newHeaderMatcher(headerMatcherConfig *v3route_componentspb.HeaderMatcher) ( case *v3route_componentspb.HeaderMatcher_ExactMatch: m = internalmatcher.NewHeaderExactMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetExactMatch(), headerMatcherConfig.InvertMatch) case *v3route_componentspb.HeaderMatcher_SafeRegexMatch: - regex, err := regexp.Compile(headerMatcherConfig.GetSafeRegexMatch().Regex) + regex, err := internalmatcher.CompileSafeRegex(headerMatcherConfig.GetSafeRegexMatch().GetRegex()) if err != nil { return nil, err } @@ -398,6 +427,25 @@ func (pm *portMatcher) match(data *rpcData) bool { return data.destinationPort == pm.destinationPort } +// requestedServerNameMatcher matches on if the given string matcher matches +// on "", as per A41-xds-rbac.md. requestedServerNameMatcher implements +// the matcher interface. +type requestedServerNameMatcher struct { + stringMatcher internalmatcher.StringMatcher +} + +func newRequestedServerNameMatcher(stringMatcherProto *v3matcherpb.StringMatcher) (*requestedServerNameMatcher, error) { + stringMatcher, err := internalmatcher.StringMatcherFromProto(stringMatcherProto) + if err != nil { + return nil, err + } + return &requestedServerNameMatcher{stringMatcher: stringMatcher}, nil +} + +func (r *requestedServerNameMatcher) match(*rpcData) bool { + return r.stringMatcher.Match("") +} + // authenticatedMatcher matches on the name of the Principal. If set, the URI // SAN or DNS SAN in that order is used from the certificate, otherwise the // subject field is used. If unset, it applies to any user that is diff --git a/vendor/google.golang.org/grpc/internal/xds/resolver/serviceconfig.go b/vendor/google.golang.org/grpc/internal/xds/resolver/serviceconfig.go index 563026a6f..b19bb7e37 100644 --- a/vendor/google.golang.org/grpc/internal/xds/resolver/serviceconfig.go +++ b/vendor/google.golang.org/grpc/internal/xds/resolver/serviceconfig.go @@ -24,7 +24,6 @@ import ( "math/bits" rand "math/rand/v2" "strings" - "sync/atomic" "time" xxhash "github.com/cespare/xxhash/v2" @@ -196,16 +195,13 @@ func (cs *configSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*iresolver.RP return nil, annotateErrorWithNodeID(status.Errorf(codes.Internal, "error retrieving cluster for match: %v (%T)", cluster, cluster), cs.xdsNodeID) } - // Add a ref to the selected cluster, as this RPC needs this cluster until - // it is committed. - var ref *int32 + // Add a ref to the selected cluster/plugin, as this RPC needs this + // cluster/plugin until it is committed. if info, ok := cs.clusters[cluster.name]; ok { - ref = &info.refCount + info.refCount.Add(1) + } else if info, ok := cs.plugins[cluster.name]; ok { + info.refCount.Add(1) } - if info, ok := cs.plugins[cluster.name]; ok { - ref = &info.refCount - } - atomic.AddInt32(ref, 1) lbCtx := clustermanager.SetPickedCluster(rpcInfo.Context, cluster.name) lbCtx = xdsresource.NewContextWithXDSConfig(lbCtx, cs.xdsConfig) @@ -221,8 +217,7 @@ func (cs *configSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*iresolver.RP // When the RPC is committed, the cluster is no longer required. // Decrease its ref. if info, ok := cs.clusters[cluster.name]; ok { - ref := &info.refCount - if v := atomic.AddInt32(ref, -1); v == 0 { + if v := info.refCount.Add(-1); v == 0 { // We call unsubscribe rather than sendNewServiceConfig to // prevent redundant updates. If the reference count in the // dependency manager drops to zero, it will automatically @@ -233,8 +228,7 @@ func (cs *configSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*iresolver.RP } } if info, ok := cs.plugins[cluster.name]; ok { - ref := &info.refCount - if v := atomic.AddInt32(ref, -1); v == 0 { + if v := info.refCount.Add(-1); v == 0 { // This entry will be removed from activePlugins when // producing a new service config update. cs.sendNewServiceConfig() @@ -350,12 +344,12 @@ func (cs *configSelector) stop() { // after a new one is active, we must trigger a subsequent update to delete // the now-unused clusters. for _, ci := range cs.clusters { - if v := atomic.AddInt32(&ci.refCount, -1); v == 0 { + if v := ci.refCount.Add(-1); v == 0 { ci.unsubscribe() } } for _, ci := range cs.plugins { - if v := atomic.AddInt32(&ci.refCount, -1); v == 0 { + if v := ci.refCount.Add(-1); v == 0 { cs.sendNewServiceConfig() } } diff --git a/vendor/google.golang.org/grpc/internal/xds/resolver/xds_resolver.go b/vendor/google.golang.org/grpc/internal/xds/resolver/xds_resolver.go index 8de3a14bd..3d4932bc2 100644 --- a/vendor/google.golang.org/grpc/internal/xds/resolver/xds_resolver.go +++ b/vendor/google.golang.org/grpc/internal/xds/resolver/xds_resolver.go @@ -415,9 +415,26 @@ func (r *xdsResolver) newConfigSelector() (_ *configSelector, err error) { for i, rt := range r.xdsConfig.VirtualHost.Routes { clusters := rinternal.NewWRR.(func() wrr.WRR)() interceptors := []iresolver.ClientInterceptor{} + // TODO: Carve out the common logic between the ClusterSpecifierPlugin + // and WeightedClusters. if rt.ClusterSpecifierPlugin != "" { clusterName := clusterSpecifierPluginPrefix + rt.ClusterSpecifierPlugin - clusters.Add(&routeCluster{name: clusterName}, 1) + interceptor, err := r.newInterceptor(r.xdsConfig.Listener.APIListener.HTTPFilters, nil, rt.HTTPFilterConfigOverride, r.xdsConfig.VirtualHost.HTTPFilterConfigOverride) + if err != nil { + // Clean up any interceptors that were successfully built + // for the current route before this error occurred. Note + // that this is not handled by the call to cs.stop() in the + // deferred function. + for _, i := range interceptors { + i.Close() + } + return nil, err + } + clusters.Add(&routeCluster{ + name: clusterName, + interceptor: interceptor, + }, 1) + interceptors = append(interceptors, interceptor) ci := r.addOrGetActiveClusterInfo(clusterName, "") ci.cfg = xdsChildConfig{ChildPolicy: balancerConfig(r.xdsConfig.RouteConfig.ClusterSpecifierPlugins[rt.ClusterSpecifierPlugin])} cs.plugins[clusterName] = ci @@ -464,10 +481,10 @@ func (r *xdsResolver) newConfigSelector() (_ *configSelector, err error) { // errors may occur. Note: cs.clusters are pointers to entries in // activeClusters. for _, ci := range cs.clusters { - atomic.AddInt32(&ci.refCount, 1) + ci.refCount.Add(1) } for _, ci := range cs.plugins { - atomic.AddInt32(&ci.refCount, 1) + ci.refCount.Add(1) } // Cleanup filter instances that are no longer specified in the current @@ -496,13 +513,13 @@ func (r *xdsResolver) newConfigSelector() (_ *configSelector, err error) { // Only executed in the context of a serializer callback. func (r *xdsResolver) pruneActiveClustersAndPlugins() { for cluster, ci := range r.activeClusters { - if atomic.LoadInt32(&ci.refCount) == 0 { + if ci.refCount.Load() == 0 { ci.unsubscribe() delete(r.activeClusters, cluster) } } for cluster, ci := range r.activePlugins { - if atomic.LoadInt32(&ci.refCount) == 0 { + if ci.refCount.Load() == 0 { delete(r.activePlugins, cluster) } } @@ -535,8 +552,8 @@ func (r *xdsResolver) addOrGetActiveClusterInfo(key string, name string) *cluste } type clusterInfo struct { - // number of references to this cluster; accessed atomically - refCount int32 + // refCount is the number of references to this cluster. + refCount atomic.Int32 // cfg is the child configuration for this cluster, containing either the // csp config or the cds cluster config. cfg xdsChildConfig @@ -596,6 +613,23 @@ func (r *xdsResolver) newInterceptor(filters []xdsresource.HTTPFilter, clusterOv if override == nil { override = virtualHostOverride[filter.Name] } + + // Determine the effective disabled state of the filter. The base + // configuration's disabled state is used unless an override is present. + // If an override is present, the filter is disabled if the override is + // a DisabledFilterConfig. + disabled := filter.Disabled + if override != nil { + _, disabled = override.(httpfilter.DisabledFilterConfig) + } + + if disabled { + if r.logger.V(2) { + r.logger.Infof("Filter %q has been disabled.", filter.Name) + } + continue + } + builder, ok := filter.Filter.(httpfilter.ClientFilterBuilder) if !ok { // Should not happen if it passed xdsClient validation. diff --git a/vendor/google.golang.org/grpc/internal/xds/server/filter_chain_manager.go b/vendor/google.golang.org/grpc/internal/xds/server/filter_chain_manager.go index 2a12fd73f..f3d0143c2 100644 --- a/vendor/google.golang.org/grpc/internal/xds/server/filter_chain_manager.go +++ b/vendor/google.golang.org/grpc/internal/xds/server/filter_chain_manager.go @@ -122,17 +122,7 @@ func (fcm *filterChainManager) filterChainFromConfig(config *xdsresource.Network func (fcm *filterChainManager) stop() { for _, fc := range fcm.filterChains { - urc := fc.usableRouteConfiguration.Load() - if urc.err != nil { - continue - } - for _, vh := range urc.vhs { - for _, r := range vh.routes { - if r.interceptor != nil { - r.interceptor.Close() - } - } - } + fc.usableRouteConfiguration.Load().stop() fc.stop() } } @@ -172,11 +162,20 @@ type sourcePrefixEntry struct { // Listener resource. This struct contains the active state of a filter chain, // which includes the usable route configuration. type filterChain struct { - securityCfg *xdsresource.SecurityConfig - httpFilters []xdsresource.HTTPFilter - serverFilters []httpfilter.ServerFilter // Server filters with reference counts, stored for cleanup purposes. - routeConfigName string - inlineRouteConfig *xdsresource.RouteConfigUpdate + // The following fields are set at initialization time, and are not + // updated afterwards. + securityCfg *xdsresource.SecurityConfig + httpFilters []xdsresource.HTTPFilter + routeConfigName string + inlineRouteConfig *xdsresource.RouteConfigUpdate + + // Server filters with reference counts. Is updated when a usable route + // configuration is set, and when the filter chain is stopped. Both these + // operations always happen with the listener wrapper's lock held. + serverFilters []httpfilter.ServerFilter + + // The usable route configuration, is passed to the connWrapper, and is used + // to route incoming RPCs. Therefore, this needs to be accessed atomically. usableRouteConfiguration *atomic.Pointer[usableRouteConfiguration] } @@ -188,6 +187,16 @@ type usableRouteConfiguration struct { nodeID string // For logging purposes. Populated by the listener wrapper. } +func (rc *usableRouteConfiguration) stop() { + for _, vh := range rc.vhs { + for _, r := range vh.routes { + if r.interceptor != nil { + r.interceptor.Close() + } + } + } +} + // virtualHostWithInterceptors captures information present in a VirtualHost // update, and also contains routes with instantiated HTTP Filters. type virtualHostWithInterceptors struct { @@ -397,32 +406,61 @@ func (fc *filterChain) stop() { // state across resource updates. type serverFilterProvider func(filter xdsresource.HTTPFilter) (httpfilter.ServerFilter, error) -// constructUsableRouteConfiguration takes Route Configuration and converts it +// updateUsableRouteConfiguration takes Route Configuration and converts it // into matchable route configuration, with instantiated HTTP Filters per route. -func (fc *filterChain) constructUsableRouteConfiguration(config xdsresource.RouteConfigUpdate, provider serverFilterProvider) *usableRouteConfiguration { - vhs := make([]virtualHostWithInterceptors, 0, len(config.VirtualHosts)) +func (fc *filterChain) updateUsableRouteConfiguration(config *xdsresource.RouteConfigUpdate, updateErr error, provider serverFilterProvider, nodeID string) { + if updateErr != nil { + urc := &usableRouteConfiguration{err: updateErr, nodeID: nodeID} + fc.applyConfiguration(urc, nil) + return + } + var serverFilters []httpfilter.ServerFilter + vhs := make([]virtualHostWithInterceptors, 0, len(config.VirtualHosts)) for _, vh := range config.VirtualHosts { vhwi, sfs, err := fc.convertVirtualHost(vh, provider) if err != nil { for _, sf := range serverFilters { sf.Close() } + // Close interceptors from successfully converted virtual hosts. + for _, v := range vhs { + for _, r := range v.routes { + if r.interceptor != nil { + r.interceptor.Close() + r.interceptor = nil + } + } + } // Non nil if (lds + rds) fails, shouldn't happen since validated by // xDS Client, treat as L7 error but shouldn't happen. - return &usableRouteConfiguration{err: fmt.Errorf("virtual host construction: %v", err)} + urc := &usableRouteConfiguration{err: fmt.Errorf("virtual host construction: %v", err), nodeID: nodeID} + fc.applyConfiguration(urc, nil) + return } vhs = append(vhs, vhwi) serverFilters = append(serverFilters, sfs...) } - // Release references to old server filters before replacing with new ones. - for _, sf := range fc.serverFilters { - sf.Close() - } + urc := &usableRouteConfiguration{vhs: vhs, nodeID: nodeID} + fc.applyConfiguration(urc, serverFilters) +} + +func (fc *filterChain) applyConfiguration(urc *usableRouteConfiguration, serverFilters []httpfilter.ServerFilter) { + // Swap in the new configuration first so new RPCs use it immediately. + oldURC := fc.usableRouteConfiguration.Swap(urc) + oldFilters := fc.serverFilters fc.serverFilters = serverFilters - return &usableRouteConfiguration{vhs: vhs} + // Stop the old interceptors before releasing the filters they might depend on. + if oldURC != nil { + oldURC.stop() + } + + // Release references to old server filters. + for _, sf := range oldFilters { + sf.Close() + } } func (fc *filterChain) convertVirtualHost(virtualHost *xdsresource.VirtualHost, provider serverFilterProvider) (_ virtualHostWithInterceptors, _ []httpfilter.ServerFilter, err error) { @@ -441,6 +479,13 @@ func (fc *filterChain) convertVirtualHost(virtualHost *xdsresource.VirtualHost, rs[i].matcher = xdsresource.RouteToMatcher(r) interceptor, sfs, err := fc.newInterceptor(r.HTTPFilterConfigOverride, virtualHost.HTTPFilterConfigOverride, provider) if err != nil { + // Close interceptors from successfully converted routes. + for _, route := range rs { + if route.interceptor != nil { + route.interceptor.Close() + route.interceptor = nil + } + } return virtualHostWithInterceptors{}, nil, err } serverFilters = append(serverFilters, sfs...) @@ -478,6 +523,14 @@ func (fc *filterChain) newInterceptor(routeOverride, virtualHostOverride map[str override = virtualHostOverride[filter.Name] } + disabled := filter.Disabled + if override != nil { + _, disabled = override.(httpfilter.DisabledFilterConfig) + } + if disabled { + continue + } + serverFilter, err := provider(filter) if err != nil { return nil, nil, err diff --git a/vendor/google.golang.org/grpc/internal/xds/server/listener_wrapper.go b/vendor/google.golang.org/grpc/internal/xds/server/listener_wrapper.go index b7d20b2ba..92b654b60 100644 --- a/vendor/google.golang.org/grpc/internal/xds/server/listener_wrapper.go +++ b/vendor/google.golang.org/grpc/internal/xds/server/listener_wrapper.go @@ -154,7 +154,6 @@ func (l *listenerWrapper) maybeUpdateFilterChains() { } l.mu.Lock() - l.switchModeLocked(connectivity.ServingModeServing, nil) // "Updates to a Listener cause all older connections on that Listener to be // gracefully shut down with a grace period of 10 minutes for long-lived // RPC's, such that clients will reconnect and have the updated @@ -189,6 +188,7 @@ func (l *listenerWrapper) maybeUpdateFilterChains() { delete(l.httpFilters, key) } } + l.switchModeLocked(connectivity.ServingModeServing, nil) l.mu.Unlock() go func() { @@ -210,14 +210,10 @@ func (l *listenerWrapper) handleRDSUpdate(routeName string, rcu rdsWatcherUpdate continue } if rcu.err != nil && rcu.data == nil { // Either NACK before update, or resource not found triggers this conditional. - urc := &usableRouteConfiguration{err: rcu.err} - urc.nodeID = l.xdsNodeID - fc.usableRouteConfiguration.Store(urc) + fc.updateUsableRouteConfiguration(nil, rcu.err, l.getOrCreateServerFilterLocked, l.xdsNodeID) continue } - urc := fc.constructUsableRouteConfiguration(*rcu.data, l.getOrCreateServerFilterLocked) - urc.nodeID = l.xdsNodeID - fc.usableRouteConfiguration.Store(urc) + fc.updateUsableRouteConfiguration(rcu.data, nil, l.getOrCreateServerFilterLocked, l.xdsNodeID) } } l.mu.Unlock() @@ -235,21 +231,15 @@ func (l *listenerWrapper) handleRDSUpdate(routeName string, rcu rdsWatcherUpdate func (l *listenerWrapper) instantiateFilterChainRoutingConfigurationsLocked() { for _, fc := range l.activeFilterChainManager.filterChains { if fc.inlineRouteConfig != nil { - urc := fc.constructUsableRouteConfiguration(*fc.inlineRouteConfig, l.getOrCreateServerFilterLocked) - urc.nodeID = l.xdsNodeID - fc.usableRouteConfiguration.Store(urc) // Can't race with an RPC coming in but no harm making atomic. + fc.updateUsableRouteConfiguration(fc.inlineRouteConfig, nil, l.getOrCreateServerFilterLocked, l.xdsNodeID) continue } // Inline configuration constructed once here, will remain for lifetime of filter chain. rcu := l.rdsHandler.updates[fc.routeConfigName] if rcu.err != nil && rcu.data == nil { - urc := &usableRouteConfiguration{err: rcu.err} - urc.nodeID = l.xdsNodeID - fc.usableRouteConfiguration.Store(urc) + fc.updateUsableRouteConfiguration(nil, rcu.err, l.getOrCreateServerFilterLocked, l.xdsNodeID) continue } - urc := fc.constructUsableRouteConfiguration(*rcu.data, l.getOrCreateServerFilterLocked) - urc.nodeID = l.xdsNodeID - fc.usableRouteConfiguration.Store(urc) // Can't race with an RPC coming in but no harm making atomic. + fc.updateUsableRouteConfiguration(rcu.data, nil, l.getOrCreateServerFilterLocked, l.xdsNodeID) } } diff --git a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/grpc_service.go b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/grpc_service.go new file mode 100644 index 000000000..c4571d090 --- /dev/null +++ b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/grpc_service.go @@ -0,0 +1,49 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xdsresource + +import ( + "time" + + "google.golang.org/grpc/metadata" +) + +// GRPCServiceConfig contains the configuration for an external server. It is +// the parsed configuration for the GrpcService proto message. +// See: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/grpc_service.proto +type GRPCServiceConfig struct { + // TargetURI is the name of the external server. + TargetURI string + // ChannelCredentials specifies the configuration for the transport + // credentials to use to connect to the external server, as a JSON string. + ChannelCredentials string + // CallCredentials specifies the configuration for the per-RPC credentials to + // use when making calls to the external server, as a JSON string. + CallCredentials string + // Timeout is the RPC timeout for the call to the external server. If unset, + // the timeout depends on the usage of this external server. For example, + // cases like ext_authz and ext_proc, where there is a 1:1 mapping between the + // data plane RPC and the external server call, the timeout will be capped by + // the timeout on the data plane RPC. For cases like RLQS where there is a + // side channel to the external server, an unset timeout will result in no + // timeout being applied to the external server call. + Timeout time.Duration + // InitialMetadata is the additional metadata to include in all RPCs sent to + // the external server. + InitialMetadata metadata.MD +} diff --git a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/matcher_path.go b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/matcher_path.go index da487e20c..8627ed3ca 100644 --- a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/matcher_path.go +++ b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/matcher_path.go @@ -20,8 +20,6 @@ package xdsresource import ( "regexp" "strings" - - "google.golang.org/grpc/internal/grpcutil" ) type pathMatcher interface { @@ -94,7 +92,7 @@ func newPathRegexMatcher(re *regexp.Regexp) *pathRegexMatcher { } func (prm *pathRegexMatcher) match(path string) bool { - return grpcutil.FullMatchWithRegex(prm.re, path) + return prm.re.MatchString(path) } func (prm *pathRegexMatcher) String() string { diff --git a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/metadata.go b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/metadata.go index 566f46f8b..4d915c5e4 100644 --- a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/metadata.go +++ b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/metadata.go @@ -21,15 +21,20 @@ import ( "fmt" "net/netip" - v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" "google.golang.org/grpc/internal/envconfig" "google.golang.org/protobuf/types/known/anypb" + + v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + v3gcpauthnpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3" ) func init() { if envconfig.XDSHTTPConnectEnabled { registerMetadataConverter("type.googleapis.com/envoy.config.core.v3.Address", proxyAddressConvertor{}) } + if envconfig.GCPAuthenticationFilterEnabled { + registerMetadataConverter("type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience", audienceConverter{}) + } } var ( @@ -100,3 +105,28 @@ func (proxyAddressConvertor) convert(anyProto *anypb.Any) (any, error) { } return ProxyAddressMetadataValue{Address: parseAddress(socketaddress)}, nil } + +// AudienceMetadataValue holds the audience parsed from the +// envoy.extensions.filters.http.gcp_authn.v3.Audience proto message, as +// specified in gRFC A83. +type AudienceMetadataValue struct { + // Audience is the URL of the receiving service that performs token + // authentication. + Audience string +} + +// audienceConverter implements the metadataConverter interface to +// handle the conversion of envoy.extensions.filters.http.gcp_authn.v3.Audience +// protobuf messages into an internal representation. +type audienceConverter struct{} + +func (audienceConverter) convert(anyProto *anypb.Any) (any, error) { + audienceProto := &v3gcpauthnpb.Audience{} + if err := anyProto.UnmarshalTo(audienceProto); err != nil { + return nil, fmt.Errorf("failed to unmarshal the envoy.extensions.filters.http.gcp_authn.v3.Audience resource from Any proto: %v", err) + } + if audienceProto.GetUrl() == "" { + return nil, fmt.Errorf("empty url field in audience metadata") + } + return AudienceMetadataValue{Audience: audienceProto.GetUrl()}, nil +} diff --git a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/type_cds.go b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/type_cds.go index d4b2d323d..f2063827e 100644 --- a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/type_cds.go +++ b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/type_cds.go @@ -83,6 +83,9 @@ type ClusterUpdate struct { // LRSReportEndpointMetrics specifies the subset of ORCA metrics that // should be propagated to the LRS server. LRSReportEndpointMetrics *LRSReportEndpointMetricsConfig + + // Metadata contains the metadata from the cluster resource. + Metadata map[string]any } // LRSReportEndpointMetricsConfig holds the configuration for propagating ORCA diff --git a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/type_lds.go b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/type_lds.go index 8993c8cf7..a03fd2dba 100644 --- a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/type_lds.go +++ b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/type_lds.go @@ -69,6 +69,9 @@ type HTTPFilter struct { Filter httpfilter.Builder // Config contains the filter's configuration Config httpfilter.FilterConfig + // Disabled specifies if the filter is disabled. For more information, see + // envoyproxy.io/docs/envoy/latest/intro/arch_overview/http/http_filters#route-based-filter-chain + Disabled bool } // InboundListenerConfig contains information about the inbound listener, i.e diff --git a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_cds.go b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_cds.go index 4ff3224bd..151143a16 100644 --- a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_cds.go +++ b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_cds.go @@ -215,6 +215,14 @@ func validateClusterAndConstructClusterUpdate(cluster *v3clusterpb.Cluster, serv } } + var metadata map[string]any + if envconfig.GCPAuthenticationFilterEnabled { + var err error + if metadata, err = validateAndConstructMetadata(cluster.GetMetadata()); err != nil { + return ClusterUpdate{}, err + } + } + ret := ClusterUpdate{ ClusterName: cluster.GetName(), SecurityCfg: sc, @@ -223,6 +231,7 @@ func validateClusterAndConstructClusterUpdate(cluster *v3clusterpb.Cluster, serv OutlierDetection: od, TelemetryLabels: telemetryLabels, LRSReportEndpointMetrics: lrsReportEndpointMetrics, + Metadata: metadata, } if lrs := cluster.GetLrsServer(); lrs != nil { diff --git a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_eds.go b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_eds.go index 93bb3e424..22eb8500d 100644 --- a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_eds.go +++ b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_eds.go @@ -26,6 +26,7 @@ import ( v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "google.golang.org/grpc/experimental/balancer/hostname" "google.golang.org/grpc/internal/envconfig" "google.golang.org/grpc/internal/pretty" xdsinternal "google.golang.org/grpc/internal/xds" @@ -36,27 +37,12 @@ import ( "google.golang.org/protobuf/types/known/anypb" ) -// hostnameKeyType is the key to store the hostname attribute in -// a resolver.Endpoint. -type hostnameKeyType struct{} - -// SetHostname returns a copy of the given endpoint with hostname added -// as an attribute. -func SetHostname(endpoint resolver.Endpoint, hostname string) resolver.Endpoint { - // Only set if non-empty; xds_cluster_impl uses this to trigger :authority - // rewriting. - if hostname == "" { - return endpoint - } - endpoint.Attributes = endpoint.Attributes.WithValue(hostnameKeyType{}, hostname) - return endpoint -} - -// Hostname returns the hostname from the BalancerAttributes of the given -// Address. If this attribute is not set, it returns the empty string. +// Hostname returns the hostname from the BalancerAttributes of the +// given Address. If this attribute is not set, it returns the empty +// string. func Hostname(addr resolver.Address) string { - hostname, _ := addr.BalancerAttributes.Value(hostnameKeyType{}).(string) - return hostname + ep := resolver.Endpoint{Attributes: addr.BalancerAttributes} + return hostname.FromEndpoint(ep) } func unmarshalEndpointsResource(r *anypb.Any) (string, EndpointsUpdate, error) { @@ -166,7 +152,7 @@ func parseEndpoints(lbEndpoints []*v3endpointpb.LbEndpoint, uniqueEndpointAddrs } } endpoint := resolver.Endpoint{Addresses: address} - endpoint = SetHostname(endpoint, lbEndpoint.GetEndpoint().GetHostname()) + endpoint = hostname.Set(endpoint, lbEndpoint.GetEndpoint().GetHostname()) endpoint = ringhash.SetHashKey(endpoint, hashKey) endpoints = append(endpoints, Endpoint{ ResolverEndpoint: endpoint, @@ -268,6 +254,8 @@ func parseEDSRespProto(m *v3endpointpb.ClusterLoadAssignment) (EndpointsUpdate, return ret, nil } +// validateAndConstructMetadata processes the metadata from the xDS resource +// and returns a map of parsed metadata values. func validateAndConstructMetadata(metadataProto *v3corepb.Metadata) (map[string]any, error) { if metadataProto == nil { return nil, nil diff --git a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_lds.go b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_lds.go index 117f5e4b7..e4c8bfd5c 100644 --- a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_lds.go +++ b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_lds.go @@ -30,6 +30,7 @@ import ( v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "google.golang.org/grpc/internal/envconfig" "google.golang.org/grpc/internal/xds/clients/xdsclient" "google.golang.org/grpc/internal/xds/httpfilter" "google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version" @@ -178,6 +179,7 @@ func processHTTPFilterOverrides(cfgs map[string]*anypb.Any) (map[string]httpfilt m := make(map[string]httpfilter.FilterConfig) for name, cfg := range cfgs { optional := false + disabled := false s := new(v3routepb.FilterConfig) if cfg.MessageIs(s) { if err := cfg.UnmarshalTo(s); err != nil { @@ -185,6 +187,14 @@ func processHTTPFilterOverrides(cfgs map[string]*anypb.Any) (map[string]httpfilt } cfg = s.GetConfig() optional = s.GetIsOptional() + if envconfig.XDSClientExtProcEnabled { + disabled = s.GetDisabled() + } + } + + if disabled { + m[name] = httpfilter.DisabledFilterConfig{} + continue } httpFilter, config, err := validateHTTPFilterConfig(cfg, false, optional) @@ -235,8 +245,12 @@ func processHTTPFilters(filters []*v3httppb.HttpFilter, server bool) ([]HTTPFilt return nil, fmt.Errorf("HTTP filter %q not supported client-side", name) } + disabled := false + if envconfig.XDSClientExtProcEnabled { + disabled = filter.GetDisabled() + } // Save name/config - ret = append(ret, HTTPFilter{Name: name, Filter: httpFilter, Config: config}) + ret = append(ret, HTTPFilter{Name: name, Filter: httpFilter, Config: config, Disabled: disabled}) } // "Validation will fail if a terminal filter is not the last filter in the // chain or if a non-terminal filter is the last filter in the chain." - A39 diff --git a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_rds.go b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_rds.go index 09589d6bc..15a8e1036 100644 --- a/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_rds.go +++ b/vendor/google.golang.org/grpc/internal/xds/xdsclient/xdsresource/unmarshal_rds.go @@ -241,7 +241,7 @@ func routesProtoToSlice(routes []*v3routepb.Route, csps map[string]clusterspecif route.Path = &pt.Path case *v3routepb.RouteMatch_SafeRegex: regex := pt.SafeRegex.GetRegex() - re, err := regexp.Compile(regex) + re, err := matcher.CompileSafeRegex(regex) if err != nil { return nil, nil, fmt.Errorf("route %+v contains an invalid regex %q", r, regex) } @@ -261,7 +261,7 @@ func routesProtoToSlice(routes []*v3routepb.Route, csps map[string]clusterspecif header.ExactMatch = &ht.ExactMatch case *v3routepb.HeaderMatcher_SafeRegexMatch: regex := ht.SafeRegexMatch.GetRegex() - re, err := regexp.Compile(regex) + re, err := matcher.CompileSafeRegex(regex) if err != nil { return nil, nil, fmt.Errorf("route %+v contains an invalid regex %q", r, regex) } diff --git a/vendor/google.golang.org/grpc/rpc_util.go b/vendor/google.golang.org/grpc/rpc_util.go index ee7f7dead..52f4ea513 100644 --- a/vendor/google.golang.org/grpc/rpc_util.go +++ b/vendor/google.golang.org/grpc/rpc_util.go @@ -128,6 +128,16 @@ func NewGZIPDecompressor() Decompressor { } func (d *gzipDecompressor) Do(r io.Reader) ([]byte, error) { + return d.doWithMaxSize(r, math.MaxInt64) +} + +// doWithMaxSize behaves like Do but caps the size of the decompressed +// payload at maxMessageSize+1 bytes. The Decompressor interface does not +// allow extra parameters, so callers inside the package type-assert to +// *gzipDecompressor to invoke this method directly. The +1 byte makes it +// possible for the caller to detect that the limit was exceeded and +// return ResourceExhausted instead of materializing an unbounded payload. +func (d *gzipDecompressor) doWithMaxSize(r io.Reader, maxMessageSize int64) ([]byte, error) { var z *gzip.Reader switch maybeZ := d.pool.Get().(type) { case nil: @@ -148,7 +158,11 @@ func (d *gzipDecompressor) Do(r io.Reader) ([]byte, error) { z.Close() d.pool.Put(z) }() - return io.ReadAll(z) + var src io.Reader = z + if maxMessageSize < math.MaxInt64 { + src = io.LimitReader(z, maxMessageSize+1) + } + return io.ReadAll(src) } func (d *gzipDecompressor) Type() string { @@ -830,15 +844,15 @@ func compress(in mem.BufferSlice, cp Compressor, compressor encoding.Compressor, if compressor != nil { z, err := compressor.Compress(w) if err != nil { - return nil, 0, wrapErr(err) + return nil, compressionNone, wrapErr(err) } for _, b := range in { if _, err := z.Write(b.ReadOnlyData()); err != nil { - return nil, 0, wrapErr(err) + return nil, compressionNone, wrapErr(err) } } if err := z.Close(); err != nil { - return nil, 0, wrapErr(err) + return nil, compressionNone, wrapErr(err) } } else { // This is obviously really inefficient since it fully materializes the data, but @@ -848,7 +862,7 @@ func compress(in mem.BufferSlice, cp Compressor, compressor encoding.Compressor, buf := in.MaterializeToBuffer(pool) defer buf.Free() if err := cp.Do(w, buf.ReadOnlyData()); err != nil { - return nil, 0, wrapErr(err) + return nil, compressionNone, wrapErr(err) } } return out, compressionMade, nil @@ -971,7 +985,20 @@ func recvAndDecompress(p *parser, s recvCompressor, dc Decompressor, maxReceiveM func decompress(compressor encoding.Compressor, d mem.BufferSlice, dc Decompressor, maxReceiveMessageSize int, pool mem.BufferPool) (mem.BufferSlice, error) { if dc != nil { r := d.Reader() - uncompressed, err := dc.Do(r) + // For the built-in gzip decompressor, bound the decompressed output + // at maxReceiveMessageSize+1 so that a small but highly compressed + // payload (a "zip bomb") cannot expand to gigabytes in memory before + // the post-decompression size check below has a chance to fire. The + // Decompressor interface does not accept an extra size parameter, + // so we type-assert to invoke a size-aware helper. Third-party + // Decompressor implementations keep the original Do behavior. + var uncompressed []byte + var err error + if gd, ok := dc.(*gzipDecompressor); ok { + uncompressed, err = gd.doWithMaxSize(r, int64(maxReceiveMessageSize)) + } else { + uncompressed, err = dc.Do(r) + } if err != nil { r.Close() // ensure buffers are reused return nil, status.Errorf(codes.Internal, "grpc: failed to decompress the received message: %v", err) @@ -989,6 +1016,9 @@ func decompress(compressor encoding.Compressor, d mem.BufferSlice, dc Decompress r.Close() // ensure buffers are reused return nil, status.Errorf(codes.Internal, "grpc: failed to decompress the message: %v", err) } + if closer, ok := dcReader.(io.Closer); ok { + defer closer.Close() + } // Read at most one byte more than the limit from the decompressor. // Unless the limit is MaxInt64, in which case, that's impossible, so diff --git a/vendor/google.golang.org/grpc/server.go b/vendor/google.golang.org/grpc/server.go index 5229adf71..cf0a20671 100644 --- a/vendor/google.golang.org/grpc/server.go +++ b/vendor/google.golang.org/grpc/server.go @@ -28,6 +28,7 @@ import ( "net/http" "reflect" "runtime" + "runtime/pprof" "strings" "sync" "sync/atomic" @@ -150,8 +151,6 @@ type Server struct { serverWorkerChannel chan func() serverWorkerChannelClose func() - - strictPathCheckingLogEmitted atomic.Bool } type serverOptions struct { @@ -250,10 +249,8 @@ func newJoinServerOption(opts ...ServerOption) ServerOption { // If this option is set to true every connection will release the buffer after // flushing the data on the wire. // -// # Experimental -// -// Notice: This API is EXPERIMENTAL and may be changed or removed in a -// later release. +// Deprecated: shared write buffer is enabled by default. SharedWriteBuffer +// will be removed in a future release. func SharedWriteBuffer(val bool) ServerOption { return newFuncServerOption(func(o *serverOptions) { o.sharedWriteBuffer = val @@ -302,6 +299,14 @@ func InitialConnWindowSize(s int32) ServerOption { // window size to the value provided and disables dynamic flow control. // The lower bound for window size is 64K and any value smaller than that // will be ignored. +// +// Note that this also disables dynamic flow control for the connection, +// falling back to a default static connection-level window of 64KB. To +// use a larger connection-level window, you must also use the +// [StaticConnWindowSize] ServerOption. +// +// Most users should not configure static flow control windows unless +// operating in a memory-constrained environment. func StaticStreamWindowSize(s int32) ServerOption { return newFuncServerOption(func(o *serverOptions) { o.initialWindowSize = s @@ -313,6 +318,14 @@ func StaticStreamWindowSize(s int32) ServerOption { // window size to the value provided and disables dynamic flow control. // The lower bound for window size is 64K and any value smaller than that // will be ignored. +// +// Note that this also disables dynamic flow control for individual streams, +// falling back to a default static connection-level window of 64KB. To +// explicitly configure the stream-level window size, you must also use the +// [StaticStreamWindowSize] ServerOption. +// +// Most users should not configure static flow control windows unless +// operating in a memory-constrained environment. func StaticConnWindowSize(s int32) ServerOption { return newFuncServerOption(func(o *serverOptions) { o.initialConnWindowSize = s @@ -1787,6 +1800,12 @@ func (s *Server) handleMalformedMethodName(stream *transport.ServerStream, ti *t func (s *Server) handleStream(t transport.ServerTransport, stream *transport.ServerStream) { ctx := stream.Context() ctx = contextWithServer(ctx, s) + if envconfig.LabelServerGoroutines&envconfig.GoroutineLabelServerMethod != 0 { + // This method always runs in its own goroutine, so we can set a + // goroutine label without needing to restore a previous context. + ctx = pprof.WithLabels(ctx, pprof.Labels("grpc.method", stream.Method())) + pprof.SetGoroutineLabels(ctx) + } var ti *traceInfo if EnableTracing { tr := newTrace("grpc.Recv."+methodFamily(stream.Method()), stream.Method()) @@ -1803,28 +1822,11 @@ func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Ser } } - sm := stream.Method() - if sm == "" { + sm, found := strings.CutPrefix(stream.Method(), "/") + if !found { s.handleMalformedMethodName(stream, ti) return } - if sm[0] != '/' { - // TODO(easwars): Add a link to the CVE in the below log messages once - // published. - if envconfig.DisableStrictPathChecking { - if old := s.strictPathCheckingLogEmitted.Swap(true); !old { - channelz.Warningf(logger, s.channelz, "grpc: Server.handleStream received malformed method name %q. Allowing it because the environment variable GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING is set to true, but this option will be removed in a future release.", sm) - } - } else { - if old := s.strictPathCheckingLogEmitted.Swap(true); !old { - channelz.Warningf(logger, s.channelz, "grpc: Server.handleStream rejected malformed method name %q. To temporarily allow such requests, set the environment variable GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING to true. Note that this is not recommended as it may allow requests to bypass security policies.", sm) - } - s.handleMalformedMethodName(stream, ti) - return - } - } else { - sm = sm[1:] - } pos := strings.LastIndex(sm, "/") if pos == -1 { s.handleMalformedMethodName(stream, ti) diff --git a/vendor/google.golang.org/grpc/stats/opentelemetry/client_metrics.go b/vendor/google.golang.org/grpc/stats/opentelemetry/client_metrics.go index 91bda9ac7..fb07704ff 100644 --- a/vendor/google.golang.org/grpc/stats/opentelemetry/client_metrics.go +++ b/vendor/google.golang.org/grpc/stats/opentelemetry/client_metrics.go @@ -18,6 +18,7 @@ package opentelemetry import ( "context" + "maps" "sync/atomic" "time" @@ -159,27 +160,26 @@ func (h *clientMetricsHandler) HandleConn(context.Context, stats.ConnStats) {} // TagRPC implements per RPC attempt context management for metrics. func (h *clientMetricsHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context { - // Numerous stats handlers can be used for the same channel. The cluster - // impl balancer which writes to this will only write once, thus have this - // stats handler's per attempt scoped context point to the same optional - // labels map if set. - var labels *istats.Labels - if labels = istats.GetLabels(ctx); labels == nil { - labels = &istats.Labels{ + ctx, ri := getOrCreateClientRPCInfo(ctx) + ai := ri.ai + if ai.xdsLabels == nil { + ai.xdsLabels = map[string]string{ // The defaults for all the per call labels from a plugin that // executes on the callpath that this OpenTelemetry component // currently supports. - TelemetryLabels: map[string]string{ - "grpc.lb.locality": "", - "grpc.lb.backend_service": "", - }, + "grpc.lb.locality": "", + "grpc.lb.backend_service": "", } - ctx = istats.SetLabels(ctx, labels) } - ctx, ri := getOrCreateClientRPCInfo(ctx) - ai := ri.ai + + // Numerous stats handlers can be used for the same channel. This callback + // ensures that all label updates are propagated to the rpc attempt info across + // derived contexts. + ctx = istats.RegisterTelemetryLabelCallback(ctx, func(labels map[string]string) { + maps.Copy(ai.xdsLabels, labels) + }) + ai.startTime = time.Now() - ai.xdsLabels = labels.TelemetryLabels ai.method = removeLeadingSlash(info.FullMethodName) return ctx diff --git a/vendor/google.golang.org/grpc/stats/opentelemetry/client_tracing.go b/vendor/google.golang.org/grpc/stats/opentelemetry/client_tracing.go index 718a634d7..cffdb9697 100644 --- a/vendor/google.golang.org/grpc/stats/opentelemetry/client_tracing.go +++ b/vendor/google.golang.org/grpc/stats/opentelemetry/client_tracing.go @@ -21,6 +21,7 @@ import ( "log" "strings" + "go.opentelemetry.io/otel/attribute" otelcodes "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" @@ -83,7 +84,10 @@ func (h *clientTracingHandler) finishTrace(err error, ts trace.Span) { // It creates a new outgoing carrier which serializes information about this // span into gRPC Metadata, if TextMapPropagator is provided in the trace // options. if TextMapPropagator is not provided, it returns the context as is. -func (h *clientTracingHandler) traceTagRPC(ctx context.Context, ai *attemptInfo, nameResolutionDelayed bool) (context.Context, *attemptInfo) { +// +// Note: The passed attemptInfo pointer (ai) is mutated in-place. Fields such as +// ai.traceSpan are updated directly. No new attemptInfo is returned. +func (h *clientTracingHandler) traceTagRPC(ctx context.Context, ai *attemptInfo, nameResolutionDelayed bool) context.Context { // Add a "Delayed name resolution complete" event to the call span // if there was name resolution delay. In case of multiple retry attempts, // ensure that event is added only once. @@ -98,7 +102,7 @@ func (h *clientTracingHandler) traceTagRPC(ctx context.Context, ai *attemptInfo, carrier := otelinternaltracing.NewOutgoingCarrier(ctx) h.options.TraceOptions.TextMapPropagator.Inject(ctx, carrier) ai.traceSpan = span - return carrier.Context(), ai + return carrier.Context() } // createCallTraceSpan creates a call span to put in the provided context using @@ -121,7 +125,12 @@ func (h *clientTracingHandler) HandleConn(context.Context, stats.ConnStats) {} // TagRPC implements per RPC attempt context management for traces. func (h *clientTracingHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context { ctx, ri := getOrCreateClientRPCInfo(ctx) - ctx, _ = h.traceTagRPC(ctx, ri.ai, info.NameResolutionDelay) + ci := getCallInfo(ctx) + if ci == nil { + logger.Error("context passed into client side stats handler (TagRPC) has no call info") + return ctx + } + ctx = h.traceTagRPC(ctx, ri.ai, info.NameResolutionDelay) return ctx } @@ -132,5 +141,15 @@ func (h *clientTracingHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) logger.Error("ctx passed into client side tracing handler trace event handling has no client attempt data present") return } + + // Client-specific Begin attributes. + if begin, ok := rs.(*stats.Begin); ok { + ci := getCallInfo(ctx) + previousRPCAttempts := ci.previousRPCAttempts.Add(1) - 1 + ri.ai.traceSpan.SetAttributes( + attribute.Int64("previous-rpc-attempts", int64(previousRPCAttempts)), + attribute.Bool("transparent-retry", begin.IsTransparentRetryAttempt), + ) + } populateSpan(rs, ri.ai) } diff --git a/vendor/google.golang.org/grpc/stats/opentelemetry/opentelemetry.go b/vendor/google.golang.org/grpc/stats/opentelemetry/opentelemetry.go index d976e60c9..b9b0c7af5 100644 --- a/vendor/google.golang.org/grpc/stats/opentelemetry/opentelemetry.go +++ b/vendor/google.golang.org/grpc/stats/opentelemetry/opentelemetry.go @@ -179,6 +179,9 @@ type callInfo struct { // nameResolutionEventAdded is set when the resolver delay trace event // is added. Prevents duplicate events, since it is reported per-attempt. nameResolutionEventAdded atomic.Bool + // previousRPCAttempts holds the count of RPC attempts that have happened + // before current attempt. Transparent retries are excluded. + previousRPCAttempts atomic.Uint32 } type callInfoKey struct{} @@ -257,9 +260,8 @@ type attemptInfo struct { // message counters for sent and received messages (used for // generating message IDs), and the number of previous RPC attempts for the // associated call. - countSentMsg uint32 - countRecvMsg uint32 - previousRPCAttempts uint32 + countSentMsg uint32 + countRecvMsg uint32 } type clientMetrics struct { diff --git a/vendor/google.golang.org/grpc/stats/opentelemetry/trace.go b/vendor/google.golang.org/grpc/stats/opentelemetry/trace.go index 40ac7a1b6..3ee66d1e8 100644 --- a/vendor/google.golang.org/grpc/stats/opentelemetry/trace.go +++ b/vendor/google.golang.org/grpc/stats/opentelemetry/trace.go @@ -17,8 +17,6 @@ package opentelemetry import ( - "sync/atomic" - "go.opentelemetry.io/otel/attribute" otelcodes "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" @@ -40,18 +38,6 @@ func populateSpan(rs stats.RPCStats, ai *attemptInfo) { span := ai.traceSpan switch rs := rs.(type) { - case *stats.Begin: - // Note: Go always added Client and FailFast attributes even though they are not - // defined by the OpenCensus gRPC spec. Thus, they are unimportant for - // correctness. - span.SetAttributes( - attribute.Bool("Client", rs.Client), - attribute.Bool("FailFast", rs.FailFast), - attribute.Int64("previous-rpc-attempts", int64(ai.previousRPCAttempts)), - attribute.Bool("transparent-retry", rs.IsTransparentRetryAttempt), - ) - // increment previous rpc attempts applicable for next attempt - atomic.AddUint32(&ai.previousRPCAttempts, 1) case *stats.DelayedPickComplete: span.AddEvent("Delayed LB pick complete") case *stats.InPayload: diff --git a/vendor/google.golang.org/grpc/version.go b/vendor/google.golang.org/grpc/version.go index 3ccfe515f..53c737fee 100644 --- a/vendor/google.golang.org/grpc/version.go +++ b/vendor/google.golang.org/grpc/version.go @@ -19,4 +19,4 @@ package grpc // Version is the current grpc version. -const Version = "1.81.1" +const Version = "1.82.1" diff --git a/vendor/modules.txt b/vendor/modules.txt index 9c4fb9244..e710275fd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -758,6 +758,7 @@ github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/fault/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3 +github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3 @@ -2011,7 +2012,7 @@ google.golang.org/genproto/googleapis/api/monitoredres google.golang.org/genproto/googleapis/rpc/code google.golang.org/genproto/googleapis/rpc/errdetails google.golang.org/genproto/googleapis/rpc/status -# google.golang.org/grpc v1.81.1 +# google.golang.org/grpc v1.82.1 ## explicit; go 1.25.0 google.golang.org/grpc google.golang.org/grpc/attributes @@ -2059,6 +2060,8 @@ google.golang.org/grpc/encoding google.golang.org/grpc/encoding/gzip google.golang.org/grpc/encoding/internal google.golang.org/grpc/encoding/proto +google.golang.org/grpc/experimental/balancer/hostname +google.golang.org/grpc/experimental/balancer/weight google.golang.org/grpc/experimental/opentelemetry google.golang.org/grpc/experimental/stats google.golang.org/grpc/grpclog @@ -2069,7 +2072,6 @@ google.golang.org/grpc/internal/admin google.golang.org/grpc/internal/backoff google.golang.org/grpc/internal/balancer/gracefulswitch google.golang.org/grpc/internal/balancer/nop -google.golang.org/grpc/internal/balancer/weight google.golang.org/grpc/internal/balancergroup google.golang.org/grpc/internal/balancerload google.golang.org/grpc/internal/binarylog @@ -2103,6 +2105,7 @@ google.golang.org/grpc/internal/stats google.golang.org/grpc/internal/status google.golang.org/grpc/internal/syscall google.golang.org/grpc/internal/transport +google.golang.org/grpc/internal/transport/internal google.golang.org/grpc/internal/transport/networktype google.golang.org/grpc/internal/transport/readyreader google.golang.org/grpc/internal/wrr From e3b062da1153d6a87279bbc69287993831c77f2d Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Thu, 30 Jul 2026 14:27:43 +0200 Subject: [PATCH 04/11] Fix lint issue --- pkg/slack/events/workflowSubmissionEvents/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/slack/events/workflowSubmissionEvents/types.go b/pkg/slack/events/workflowSubmissionEvents/types.go index 3bb0d1f0a..8c0cb4465 100644 --- a/pkg/slack/events/workflowSubmissionEvents/types.go +++ b/pkg/slack/events/workflowSubmissionEvents/types.go @@ -134,7 +134,7 @@ func (c *SlackWorkflowClient) postJSON(method string, payload interface{}) error if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var sr slackResponse if err := json.NewDecoder(resp.Body).Decode(&sr); err != nil { From 8fc100cba9913ac935fbaaf4340bf80a9a544c3a Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Thu, 30 Jul 2026 14:40:16 +0200 Subject: [PATCH 05/11] Fix 'ci/prow/fix' issue --- pkg/slack/events/workflowSubmissionEvents/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/slack/events/workflowSubmissionEvents/types.go b/pkg/slack/events/workflowSubmissionEvents/types.go index 8c0cb4465..63a997502 100644 --- a/pkg/slack/events/workflowSubmissionEvents/types.go +++ b/pkg/slack/events/workflowSubmissionEvents/types.go @@ -118,7 +118,7 @@ func (c *SlackWorkflowClient) SaveWorkflowStepConfiguration(workflowStepEditID s return c.postJSON("workflows.updateStep", r) } -func (c *SlackWorkflowClient) postJSON(method string, payload interface{}) error { +func (c *SlackWorkflowClient) postJSON(method string, payload any) error { body, err := json.Marshal(payload) if err != nil { return err From 11ab6bb4a00bee379ab0be984c07da678586f084 Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Fri, 31 Jul 2026 11:43:23 +0200 Subject: [PATCH 06/11] Addressing coderabbit findings Co-Authored-By: Claude Opus 4.6 --- .../events/workflowSubmissionEvents/types.go | 25 +++++++++++-------- .../workflow_handler.go | 6 ++--- pkg/slack/interactions/router/router.go | 4 +-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/pkg/slack/events/workflowSubmissionEvents/types.go b/pkg/slack/events/workflowSubmissionEvents/types.go index 63a997502..9fd945e68 100644 --- a/pkg/slack/events/workflowSubmissionEvents/types.go +++ b/pkg/slack/events/workflowSubmissionEvents/types.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "text/template" + "time" ) type workflowSubmit interface { @@ -34,12 +35,12 @@ type workflowStepExecuteEvent struct { } type eventWorkflowStep struct { - WorkflowStepExecuteID string `json:"workflow_step_execute_id"` - WorkflowID string `json:"workflow_id"` - WorkflowInstanceID string `json:"workflow_instance_id"` - StepID string `json:"step_id"` - Inputs *WorkflowStepInputs `json:"inputs,omitempty"` - Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` + WorkflowStepExecuteID string `json:"workflow_step_execute_id"` + WorkflowID string `json:"workflow_id"` + WorkflowInstanceID string `json:"workflow_instance_id"` + StepID string `json:"step_id"` + Inputs WorkflowStepInputs `json:"inputs,omitempty"` + Outputs []WorkflowStepOutput `json:"outputs,omitempty"` } type WorkflowStepInputElement struct { @@ -71,9 +72,9 @@ func NewSlackWorkflowClient(token string) *SlackWorkflowClient { } type workflowUpdateStepRequest struct { - WorkflowStepEditID string `json:"workflow_step_edit_id"` - Inputs *WorkflowStepInputs `json:"inputs,omitempty"` - Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` + WorkflowStepEditID string `json:"workflow_step_edit_id"` + Inputs WorkflowStepInputs `json:"inputs,omitempty"` + Outputs []WorkflowStepOutput `json:"outputs,omitempty"` } type workflowStepCompletedRequest struct { @@ -109,7 +110,7 @@ func (c *SlackWorkflowClient) WorkflowStepFailed(workflowStepExecuteID string, e return c.postJSON("workflows.stepFailed", r) } -func (c *SlackWorkflowClient) SaveWorkflowStepConfiguration(workflowStepEditID string, inputs *WorkflowStepInputs, outputs *[]WorkflowStepOutput) error { +func (c *SlackWorkflowClient) SaveWorkflowStepConfiguration(workflowStepEditID string, inputs WorkflowStepInputs, outputs []WorkflowStepOutput) error { r := &workflowUpdateStepRequest{ WorkflowStepEditID: workflowStepEditID, Inputs: inputs, @@ -123,7 +124,9 @@ func (c *SlackWorkflowClient) postJSON(method string, payload any) error { if err != nil { return err } - req, err := http.NewRequestWithContext(context.Background(), "POST", c.endpoint+method, bytes.NewReader(body)) + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, "POST", c.endpoint+method, bytes.NewReader(body)) if err != nil { return err } diff --git a/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go b/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go index 1dfd3c4ab..7f413b186 100644 --- a/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go +++ b/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go @@ -56,7 +56,7 @@ func Handler(token string, filer jira.IssueFiler) events.PartialHandler { } func checkTicketType(event *workflowStepExecuteEvent) (ticketType string, supported bool) { - for key, output := range *event.WorkflowStep.Inputs { + for key, output := range event.WorkflowStep.Inputs { if key == "ticket_type" { switch output.Value { case string(BugIdentifier): @@ -100,7 +100,7 @@ func handleJiraStep(client workflowSubmit, event *workflowStepExecuteEvent, file } } outgoingOutputs := make(map[string]string) - for _, incomingOutputs := range *event.WorkflowStep.Outputs { + for _, incomingOutputs := range event.WorkflowStep.Outputs { switch incomingOutputs.Name { case "issue.key": outgoingOutputs[incomingOutputs.Name] = issue.Key @@ -119,7 +119,7 @@ func fileTicket(event *workflowStepExecuteEvent, parameters JiraIssueParameters, title := "not_defined" reporter := "not_defined" data := make(map[string]string) - for key, inputs := range *event.WorkflowStep.Inputs { + for key, inputs := range event.WorkflowStep.Inputs { if key == TicketTitle { title = inputs.Value continue diff --git a/pkg/slack/interactions/router/router.go b/pkg/slack/interactions/router/router.go index 5405e1aed..3b5498e8b 100644 --- a/pkg/slack/interactions/router/router.go +++ b/pkg/slack/interactions/router/router.go @@ -96,7 +96,7 @@ func (r *modalRouter) Handle(callback *slack.InteractionCallback, logger *logrus case slack.InteractionTypeViewSubmission: if metadataToIdentifier(callback.View.PrivateMetadata, logger) == string(slack.InteractionTypeWorkflowStepEdit) { input, output := stepsFromApp.StepFromAppSubmit(callback) - return nil, r.slackClient.SaveWorkflowStepConfiguration(callback.Value, &input, &output) + return nil, r.slackClient.SaveWorkflowStepConfiguration(callback.Value, input, output) } return r.delegate(callback, logger) default: @@ -123,7 +123,7 @@ func isMessageButtonPress(callback *slack.InteractionCallback) bool { type slackClient interface { OpenView(triggerID string, view slack.ModalViewRequest) (*slack.ViewResponse, error) - SaveWorkflowStepConfiguration(workflowStepEditID string, inputs *workflowSubmissionEvents.WorkflowStepInputs, outputs *[]workflowSubmissionEvents.WorkflowStepOutput) error + SaveWorkflowStepConfiguration(workflowStepEditID string, inputs workflowSubmissionEvents.WorkflowStepInputs, outputs []workflowSubmissionEvents.WorkflowStepOutput) error } type combinedSlackClient struct { From 8600e10a2abfb7f6bfbcd6f59cfd459dc7019a77 Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Tue, 4 Aug 2026 14:14:24 +0200 Subject: [PATCH 07/11] Fix workflow_step_execute handling after slack-go v0.23 upgrade Re-register workflow_step_execute in EventsAPIInnerEventMapping since the slack-go library removed it, and add a nil guard for the Jira filer to prevent panics when Jira is not configured. Add tests covering the workflow handler, event parsing, and slack-go serialization changes (RichTextPreformatted flattening, *bool Emoji field). Co-Authored-By: Claude Opus 4.6 --- .../workflow_handler.go | 12 + .../workflow_handler_test.go | 293 ++++++++++++++++++ pkg/slack/modals/common/simple_modals_test.go | 78 +++++ 3 files changed, 383 insertions(+) create mode 100644 pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go diff --git a/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go b/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go index 7f413b186..114763d8c 100644 --- a/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go +++ b/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go @@ -12,6 +12,15 @@ import ( "github.com/slack-go/slack/slackevents" ) +func init() { + // Re-register workflow_step_execute in the event mapping. + // The slack-go v0.23+ library removed WorkflowStepExecuteEvent but Slack + // still sends this event type for deprecated Steps from Apps integrations. + // Without this registration, slackevents.ParseEvent rejects the event + // before our handler ever sees it. + slackevents.EventsAPIInnerEventMapping[slackevents.WorkflowStepExecute] = workflowStepExecuteEvent{} +} + const ( // BlockIdTitle is the block identifier to use for inputs // that should be used as the title of a Jira issue @@ -74,6 +83,9 @@ func checkTicketType(event *workflowStepExecuteEvent) (ticketType string, suppor } func handleJiraStep(client workflowSubmit, event *workflowStepExecuteEvent, filer jira.IssueFiler) error { + if filer == nil { + return fmt.Errorf("jira client is not configured") + } ticketType, isSupported := checkTicketType(event) if !isSupported { diff --git a/pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go b/pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go new file mode 100644 index 000000000..272d513f7 --- /dev/null +++ b/pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go @@ -0,0 +1,293 @@ +package workflowSubmissionEvents + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/slack-go/slack/slackevents" +) + +func TestSlackWorkflowClientSerialization(t *testing.T) { + t.Run("stepCompleted sends correct JSON", func(t *testing.T) { + var gotBody map[string]interface{} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + if err := json.Unmarshal(body, &gotBody); err != nil { + t.Errorf("failed to unmarshal request body: %v", err) + } + + if r.Header.Get("Content-Type") != "application/json; charset=utf-8" { + t.Errorf("Content-Type = %q, want application/json", r.Header.Get("Content-Type")) + } + if r.Header.Get("Authorization") != "Bearer test-token" { + t.Errorf("Authorization = %q, want Bearer test-token", r.Header.Get("Authorization")) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"ok":true}`) + })) + defer srv.Close() + + client := &SlackWorkflowClient{token: "test-token", endpoint: srv.URL + "/"} + outputs := map[string]string{"issue.key": "PROJ-123", "issue.link": "https://issues.redhat.com/browse/PROJ-123"} + err := client.WorkflowStepCompleted("exec-id-1", outputs) + if err != nil { + t.Fatalf("WorkflowStepCompleted returned error: %v", err) + } + + if gotBody["workflow_step_execute_id"] != "exec-id-1" { + t.Errorf("workflow_step_execute_id = %v, want exec-id-1", gotBody["workflow_step_execute_id"]) + } + outputsMap := gotBody["outputs"].(map[string]interface{}) + if outputsMap["issue.key"] != "PROJ-123" { + t.Errorf("outputs[issue.key] = %v, want PROJ-123", outputsMap["issue.key"]) + } + }) + + t.Run("stepFailed sends correct JSON", func(t *testing.T) { + var gotBody map[string]interface{} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + if err := json.Unmarshal(body, &gotBody); err != nil { + t.Errorf("failed to unmarshal request body: %v", err) + } + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"ok":true}`) + })) + defer srv.Close() + + client := &SlackWorkflowClient{token: "test-token", endpoint: srv.URL + "/"} + err := client.WorkflowStepFailed("exec-id-2", "something broke") + if err != nil { + t.Fatalf("WorkflowStepFailed returned error: %v", err) + } + + if gotBody["workflow_step_execute_id"] != "exec-id-2" { + t.Errorf("workflow_step_execute_id = %v, want exec-id-2", gotBody["workflow_step_execute_id"]) + } + errorObj := gotBody["error"].(map[string]interface{}) + if errorObj["message"] != "something broke" { + t.Errorf("error.message = %v, want 'something broke'", errorObj["message"]) + } + }) + + t.Run("saveWorkflowStepConfiguration sends correct JSON", func(t *testing.T) { + var gotBody map[string]interface{} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + if err := json.Unmarshal(body, &gotBody); err != nil { + t.Errorf("failed to unmarshal request body: %v", err) + } + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"ok":true}`) + })) + defer srv.Close() + + client := &SlackWorkflowClient{token: "test-token", endpoint: srv.URL + "/"} + inputs := WorkflowStepInputs{ + "ticket_type": {Value: "bug", SkipVariableReplacement: false}, + "ticket_title": {Value: "Test bug"}, + } + outputs := []WorkflowStepOutput{ + {Name: "issue.key", Type: "text", Label: "Issue Key"}, + } + err := client.SaveWorkflowStepConfiguration("edit-id-1", inputs, outputs) + if err != nil { + t.Fatalf("SaveWorkflowStepConfiguration returned error: %v", err) + } + + if gotBody["workflow_step_edit_id"] != "edit-id-1" { + t.Errorf("workflow_step_edit_id = %v, want edit-id-1", gotBody["workflow_step_edit_id"]) + } + inputsMap := gotBody["inputs"].(map[string]interface{}) + ticketType := inputsMap["ticket_type"].(map[string]interface{}) + if ticketType["value"] != "bug" { + t.Errorf("inputs.ticket_type.value = %v, want bug", ticketType["value"]) + } + }) + + t.Run("API error is returned", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"ok":false,"error":"invalid_auth"}`) + })) + defer srv.Close() + + client := &SlackWorkflowClient{token: "bad-token", endpoint: srv.URL + "/"} + err := client.WorkflowStepCompleted("exec-id", nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if err.Error() != "slack API workflows.stepCompleted: invalid_auth" { + t.Errorf("error = %q, want slack API error", err.Error()) + } + }) +} + +func TestWorkflowStepExecuteEventParsing(t *testing.T) { + rawEvent := `{ + "type": "workflow_step_execute", + "callback_id": "jira_ticket", + "workflow_step": { + "workflow_step_execute_id": "exec-123", + "workflow_id": "wf-456", + "workflow_instance_id": "inst-789", + "step_id": "step-1", + "inputs": { + "ticket_type": {"value": "bug", "skip_variable_replacement": false}, + "ticket_title": {"value": "Test Bug Title"} + }, + "outputs": [ + {"name": "issue.key", "type": "text", "label": "Issue Key"}, + {"name": "issue.link", "type": "text", "label": "Issue Link"} + ] + }, + "event_ts": "1234567890.123456" + }` + + // Simulate the marshal/unmarshal round-trip the handler does + var innerData interface{} + if err := json.Unmarshal([]byte(rawEvent), &innerData); err != nil { + t.Fatalf("failed to unmarshal raw event: %v", err) + } + + raw, err := json.Marshal(innerData) + if err != nil { + t.Fatalf("failed to re-marshal: %v", err) + } + + var event workflowStepExecuteEvent + if err := json.Unmarshal(raw, &event); err != nil { + t.Fatalf("failed to unmarshal into workflowStepExecuteEvent: %v", err) + } + + if event.Type != "workflow_step_execute" { + t.Errorf("Type = %q, want workflow_step_execute", event.Type) + } + if event.CallbackID != "jira_ticket" { + t.Errorf("CallbackID = %q, want jira_ticket", event.CallbackID) + } + if event.WorkflowStep.WorkflowStepExecuteID != "exec-123" { + t.Errorf("WorkflowStepExecuteID = %q, want exec-123", event.WorkflowStep.WorkflowStepExecuteID) + } + if len(event.WorkflowStep.Inputs) != 2 { + t.Errorf("Inputs count = %d, want 2", len(event.WorkflowStep.Inputs)) + } + if event.WorkflowStep.Inputs["ticket_type"].Value != "bug" { + t.Errorf("ticket_type = %q, want bug", event.WorkflowStep.Inputs["ticket_type"].Value) + } + if len(event.WorkflowStep.Outputs) != 2 { + t.Errorf("Outputs count = %d, want 2", len(event.WorkflowStep.Outputs)) + } + if event.WorkflowStep.Outputs[0].Name != "issue.key" { + t.Errorf("first output name = %q, want issue.key", event.WorkflowStep.Outputs[0].Name) + } +} + +func TestHandlerRoutesWorkflowStepExecute(t *testing.T) { + handler := Handler("fake-token", nil) + + t.Run("ignores non-callback events", func(t *testing.T) { + event := &slackevents.EventsAPIEvent{ + Type: slackevents.URLVerification, + } + handled, err := handler.Handle(event, nil) + if handled || err != nil { + t.Errorf("expected (false, nil), got (%v, %v)", handled, err) + } + }) + + t.Run("ignores non-workflow_step_execute inner events", func(t *testing.T) { + event := &slackevents.EventsAPIEvent{ + Type: slackevents.CallbackEvent, + InnerEvent: slackevents.EventsAPIInnerEvent{ + Data: &slackevents.MessageEvent{}, + }, + } + handled, err := handler.Handle(event, nil) + if handled { + t.Error("expected handled=false for MessageEvent") + } + _ = err + }) +} + +func TestHandlerEndToEndJiraTicket(t *testing.T) { + // Simulate the exact JSON that Slack sends for a workflow_step_execute event, + // parse it through slackevents.ParseEvent (which requires the init() + // registration), then through the handler's marshal/unmarshal round-trip. + + rawPayload := []byte(`{ + "token": "fake", + "team_id": "T0001", + "api_app_id": "A0001", + "event": { + "type": "workflow_step_execute", + "callback_id": "jira_ticket", + "workflow_step": { + "workflow_step_execute_id": "exec-test-123", + "workflow_id": "wf-001", + "workflow_instance_id": "inst-001", + "step_id": "step-001", + "inputs": { + "ticket_type": {"value": "bug", "skip_variable_replacement": false}, + "ticket_title": {"value": "Test Bug"}, + "user_details": {"value": "U12345"} + }, + "outputs": [ + {"name": "issue.key", "type": "text", "label": "Issue Key"}, + {"name": "issue.link", "type": "text", "label": "Issue Link"} + ] + }, + "event_ts": "1234567890.123456" + }, + "type": "event_callback", + "event_id": "Ev0001", + "event_time": 1234567890 + }`) + + // Parse the same way the bot's handleEvent does (slackevents.ParseEvent) + event, err := slackevents.ParseEvent(rawPayload, slackevents.OptionNoVerifyToken()) + if err != nil { + t.Fatalf("ParseEvent failed (init() registration may be missing): %v", err) + } + + if event.Type != slackevents.CallbackEvent { + t.Fatalf("event.Type = %q, want %q", event.Type, slackevents.CallbackEvent) + } + + // Now do the same marshal/unmarshal the handler does + raw, err := json.Marshal(event.InnerEvent.Data) + if err != nil { + t.Fatalf("failed to marshal InnerEvent.Data: %v", err) + } + + var wsEvent workflowStepExecuteEvent + if err := json.Unmarshal(raw, &wsEvent); err != nil { + t.Fatalf("failed to unmarshal workflowStepExecuteEvent: %v", err) + } + + if wsEvent.Type != "workflow_step_execute" { + t.Errorf("event type = %q, want workflow_step_execute", wsEvent.Type) + } + if wsEvent.CallbackID != "jira_ticket" { + t.Errorf("callback_id = %q, want jira_ticket", wsEvent.CallbackID) + } + if wsEvent.WorkflowStep.WorkflowStepExecuteID != "exec-test-123" { + t.Errorf("execute_id = %q, want exec-test-123", wsEvent.WorkflowStep.WorkflowStepExecuteID) + } + if wsEvent.WorkflowStep.Inputs["ticket_type"].Value != "bug" { + t.Errorf("ticket_type = %q, want bug", wsEvent.WorkflowStep.Inputs["ticket_type"].Value) + } + if wsEvent.WorkflowStep.Inputs["ticket_title"].Value != "Test Bug" { + t.Errorf("ticket_title = %q, want 'Test Bug'", wsEvent.WorkflowStep.Inputs["ticket_title"].Value) + } + if len(wsEvent.WorkflowStep.Outputs) != 2 { + t.Errorf("outputs count = %d, want 2", len(wsEvent.WorkflowStep.Outputs)) + } + +} diff --git a/pkg/slack/modals/common/simple_modals_test.go b/pkg/slack/modals/common/simple_modals_test.go index 19d6d349a..6e8dbdebb 100644 --- a/pkg/slack/modals/common/simple_modals_test.go +++ b/pkg/slack/modals/common/simple_modals_test.go @@ -149,6 +149,84 @@ func TestAppendKubeconfigBlock(t *testing.T) { } } +func TestAppendKubeconfigBlockJSON(t *testing.T) { + view := &slack.ModalViewRequest{ + Blocks: slack.Blocks{BlockSet: []slack.Block{}}, + } + AppendKubeconfigBlock(view, "apiVersion: v1\nclusters: []", "Kubeconfig") + + data, err := json.Marshal(view) + if err != nil { + t.Fatalf("failed to marshal view: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal(data, &parsed); err != nil { + t.Fatalf("failed to unmarshal JSON: %v", err) + } + + blocks := parsed["blocks"].([]interface{}) + richTextBlock := blocks[2].(map[string]interface{}) + if richTextBlock["type"] != "rich_text" { + t.Errorf("rich text block type = %q, want %q", richTextBlock["type"], "rich_text") + } + elements := richTextBlock["elements"].([]interface{}) + preformatted := elements[0].(map[string]interface{}) + if preformatted["type"] != "rich_text_preformatted" { + t.Errorf("preformatted type = %q, want %q", preformatted["type"], "rich_text_preformatted") + } + innerElements := preformatted["elements"].([]interface{}) + textEl := innerElements[0].(map[string]interface{}) + if textEl["type"] != "text" { + t.Errorf("text element type = %q, want %q", textEl["type"], "text") + } + if textEl["text"] != "apiVersion: v1\nclusters: []" { + t.Errorf("text content = %q, want kubeconfig content", textEl["text"]) + } +} + +func TestEmojiFieldSerialization(t *testing.T) { + boolPtr := new(bool) + textObj := &slack.TextBlockObject{ + Type: slack.PlainTextType, + Text: "Test", + Emoji: boolPtr, + } + + data, err := json.Marshal(textObj) + if err != nil { + t.Fatalf("failed to marshal TextBlockObject: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal(data, &parsed); err != nil { + t.Fatalf("failed to unmarshal JSON: %v", err) + } + + // With *bool set to new(bool) (false), emoji should be present and false + emoji, exists := parsed["emoji"] + if !exists { + t.Fatal("emoji field missing from serialized JSON") + } + if emoji != false { + t.Errorf("emoji = %v, want false", emoji) + } + + // With nil *bool, emoji should be omitted + textObj.Emoji = nil + data, err = json.Marshal(textObj) + if err != nil { + t.Fatalf("failed to marshal TextBlockObject with nil emoji: %v", err) + } + var parsed2 map[string]interface{} + if err := json.Unmarshal(data, &parsed2); err != nil { + t.Fatalf("failed to unmarshal JSON: %v", err) + } + if _, exists := parsed2["emoji"]; exists { + t.Error("emoji field should be omitted when nil") + } +} + func TestBuildListResultModal(t *testing.T) { tests := []struct { name string From 50719102dfaadf7be18f2759139abf4aa1362942 Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Tue, 4 Aug 2026 14:29:33 +0200 Subject: [PATCH 08/11] Fix 'ci/prow/fix' issue Replace interface{} with any in test files to satisfy go fix, and improve TestHandlerEndToEndJiraTicket to exercise handler.Handle() with mock dependencies. Co-Authored-By: Claude Opus 4.6 --- .../workflow_handler.go | 5 +- .../workflow_handler_test.go | 216 ++++++++++++++---- pkg/slack/modals/common/simple_modals_test.go | 18 +- 3 files changed, 181 insertions(+), 58 deletions(-) diff --git a/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go b/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go index 114763d8c..6c491e12b 100644 --- a/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go +++ b/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go @@ -33,7 +33,10 @@ const ( ) func Handler(token string, filer jira.IssueFiler) events.PartialHandler { - wc := NewSlackWorkflowClient(token) + return newHandler(NewSlackWorkflowClient(token), filer) +} + +func newHandler(wc workflowSubmit, filer jira.IssueFiler) events.PartialHandler { return events.PartialHandlerFunc("workflow-execution-event", func(callback *slackevents.EventsAPIEvent, logger *logrus.Entry) (handled bool, err error) { if callback.Type != slackevents.CallbackEvent { return false, nil diff --git a/pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go b/pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go index 272d513f7..b16d8ab3e 100644 --- a/pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go +++ b/pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go @@ -8,12 +8,64 @@ import ( "net/http/httptest" "testing" + jiraAPI "github.com/andygrunwald/go-jira" + "github.com/sirupsen/logrus" "github.com/slack-go/slack/slackevents" ) +type mockIssueFiler struct { + issue *jiraAPI.Issue + err error + calls []mockFileIssueCall +} + +type mockFileIssueCall struct { + IssueType string + Title string + Description string + Reporter string +} + +func (m *mockIssueFiler) FileIssue(issueType, title, description, reporter string, _ *logrus.Entry) (*jiraAPI.Issue, error) { + m.calls = append(m.calls, mockFileIssueCall{ + IssueType: issueType, + Title: title, + Description: description, + Reporter: reporter, + }) + return m.issue, m.err +} + +type mockWorkflowClient struct { + completedCalls []mockCompletedCall + failedCalls []mockFailedCall + completeErr error + failErr error +} + +type mockCompletedCall struct { + ExecuteID string + Outputs map[string]string +} + +type mockFailedCall struct { + ExecuteID string + Message string +} + +func (m *mockWorkflowClient) WorkflowStepCompleted(execID string, outputs map[string]string) error { + m.completedCalls = append(m.completedCalls, mockCompletedCall{ExecuteID: execID, Outputs: outputs}) + return m.completeErr +} + +func (m *mockWorkflowClient) WorkflowStepFailed(execID string, msg string) error { + m.failedCalls = append(m.failedCalls, mockFailedCall{ExecuteID: execID, Message: msg}) + return m.failErr +} + func TestSlackWorkflowClientSerialization(t *testing.T) { t.Run("stepCompleted sends correct JSON", func(t *testing.T) { - var gotBody map[string]interface{} + var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &gotBody); err != nil { @@ -42,14 +94,14 @@ func TestSlackWorkflowClientSerialization(t *testing.T) { if gotBody["workflow_step_execute_id"] != "exec-id-1" { t.Errorf("workflow_step_execute_id = %v, want exec-id-1", gotBody["workflow_step_execute_id"]) } - outputsMap := gotBody["outputs"].(map[string]interface{}) + outputsMap := gotBody["outputs"].(map[string]any) if outputsMap["issue.key"] != "PROJ-123" { t.Errorf("outputs[issue.key] = %v, want PROJ-123", outputsMap["issue.key"]) } }) t.Run("stepFailed sends correct JSON", func(t *testing.T) { - var gotBody map[string]interface{} + var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &gotBody); err != nil { @@ -69,14 +121,14 @@ func TestSlackWorkflowClientSerialization(t *testing.T) { if gotBody["workflow_step_execute_id"] != "exec-id-2" { t.Errorf("workflow_step_execute_id = %v, want exec-id-2", gotBody["workflow_step_execute_id"]) } - errorObj := gotBody["error"].(map[string]interface{}) + errorObj := gotBody["error"].(map[string]any) if errorObj["message"] != "something broke" { t.Errorf("error.message = %v, want 'something broke'", errorObj["message"]) } }) t.Run("saveWorkflowStepConfiguration sends correct JSON", func(t *testing.T) { - var gotBody map[string]interface{} + var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &gotBody); err != nil { @@ -103,8 +155,8 @@ func TestSlackWorkflowClientSerialization(t *testing.T) { if gotBody["workflow_step_edit_id"] != "edit-id-1" { t.Errorf("workflow_step_edit_id = %v, want edit-id-1", gotBody["workflow_step_edit_id"]) } - inputsMap := gotBody["inputs"].(map[string]interface{}) - ticketType := inputsMap["ticket_type"].(map[string]interface{}) + inputsMap := gotBody["inputs"].(map[string]any) + ticketType := inputsMap["ticket_type"].(map[string]any) if ticketType["value"] != "bug" { t.Errorf("inputs.ticket_type.value = %v, want bug", ticketType["value"]) } @@ -150,7 +202,7 @@ func TestWorkflowStepExecuteEventParsing(t *testing.T) { }` // Simulate the marshal/unmarshal round-trip the handler does - var innerData interface{} + var innerData any if err := json.Unmarshal([]byte(rawEvent), &innerData); err != nil { t.Fatalf("failed to unmarshal raw event: %v", err) } @@ -217,10 +269,6 @@ func TestHandlerRoutesWorkflowStepExecute(t *testing.T) { } func TestHandlerEndToEndJiraTicket(t *testing.T) { - // Simulate the exact JSON that Slack sends for a workflow_step_execute event, - // parse it through slackevents.ParseEvent (which requires the init() - // registration), then through the handler's marshal/unmarshal round-trip. - rawPayload := []byte(`{ "token": "fake", "team_id": "T0001", @@ -236,7 +284,12 @@ func TestHandlerEndToEndJiraTicket(t *testing.T) { "inputs": { "ticket_type": {"value": "bug", "skip_variable_replacement": false}, "ticket_title": {"value": "Test Bug"}, - "user_details": {"value": "U12345"} + "user_details": {"value": "U12345"}, + "incorrect_behaviour": {"value": "it breaks"}, + "expected_behaviour": {"value": "it works"}, + "impact": {"value": "high"}, + "affected_component": {"value": "Testing"}, + "is_reproducible": {"value": "yes"} }, "outputs": [ {"name": "issue.key", "type": "text", "label": "Issue Key"}, @@ -250,44 +303,111 @@ func TestHandlerEndToEndJiraTicket(t *testing.T) { "event_time": 1234567890 }`) - // Parse the same way the bot's handleEvent does (slackevents.ParseEvent) - event, err := slackevents.ParseEvent(rawPayload, slackevents.OptionNoVerifyToken()) - if err != nil { - t.Fatalf("ParseEvent failed (init() registration may be missing): %v", err) - } + t.Run("successful jira filing calls WorkflowStepCompleted", func(t *testing.T) { + event, err := slackevents.ParseEvent(rawPayload, slackevents.OptionNoVerifyToken()) + if err != nil { + t.Fatalf("ParseEvent failed (init() registration may be missing): %v", err) + } - if event.Type != slackevents.CallbackEvent { - t.Fatalf("event.Type = %q, want %q", event.Type, slackevents.CallbackEvent) - } + filer := &mockIssueFiler{issue: &jiraAPI.Issue{Key: "OCPCRT-999"}} + wc := &mockWorkflowClient{} + handler := newHandler(wc, filer) + logger := logrus.NewEntry(logrus.New()) - // Now do the same marshal/unmarshal the handler does - raw, err := json.Marshal(event.InnerEvent.Data) - if err != nil { - t.Fatalf("failed to marshal InnerEvent.Data: %v", err) - } + handled, err := handler.Handle(&event, logger) + if err != nil { + t.Fatalf("Handle returned error: %v", err) + } + if !handled { + t.Fatal("expected handled=true for jira_ticket callback") + } - var wsEvent workflowStepExecuteEvent - if err := json.Unmarshal(raw, &wsEvent); err != nil { - t.Fatalf("failed to unmarshal workflowStepExecuteEvent: %v", err) - } + if len(filer.calls) != 1 { + t.Fatalf("FileIssue called %d times, want 1", len(filer.calls)) + } + if filer.calls[0].IssueType != "Bug" { + t.Errorf("IssueType = %q, want Bug", filer.calls[0].IssueType) + } + if filer.calls[0].Title != "Test Bug" { + t.Errorf("Title = %q, want 'Test Bug'", filer.calls[0].Title) + } + if filer.calls[0].Reporter != "U12345" { + t.Errorf("Reporter = %q, want U12345", filer.calls[0].Reporter) + } - if wsEvent.Type != "workflow_step_execute" { - t.Errorf("event type = %q, want workflow_step_execute", wsEvent.Type) - } - if wsEvent.CallbackID != "jira_ticket" { - t.Errorf("callback_id = %q, want jira_ticket", wsEvent.CallbackID) - } - if wsEvent.WorkflowStep.WorkflowStepExecuteID != "exec-test-123" { - t.Errorf("execute_id = %q, want exec-test-123", wsEvent.WorkflowStep.WorkflowStepExecuteID) - } - if wsEvent.WorkflowStep.Inputs["ticket_type"].Value != "bug" { - t.Errorf("ticket_type = %q, want bug", wsEvent.WorkflowStep.Inputs["ticket_type"].Value) - } - if wsEvent.WorkflowStep.Inputs["ticket_title"].Value != "Test Bug" { - t.Errorf("ticket_title = %q, want 'Test Bug'", wsEvent.WorkflowStep.Inputs["ticket_title"].Value) - } - if len(wsEvent.WorkflowStep.Outputs) != 2 { - t.Errorf("outputs count = %d, want 2", len(wsEvent.WorkflowStep.Outputs)) - } + if len(wc.completedCalls) != 1 { + t.Fatalf("WorkflowStepCompleted called %d times, want 1", len(wc.completedCalls)) + } + if wc.completedCalls[0].ExecuteID != "exec-test-123" { + t.Errorf("ExecuteID = %q, want exec-test-123", wc.completedCalls[0].ExecuteID) + } + if wc.completedCalls[0].Outputs["issue.key"] != "OCPCRT-999" { + t.Errorf("issue.key = %q, want OCPCRT-999", wc.completedCalls[0].Outputs["issue.key"]) + } + if wc.completedCalls[0].Outputs["issue.link"] != "https://issues.redhat.com/browse/OCPCRT-999" { + t.Errorf("issue.link = %q, want OCPCRT-999 browse URL", wc.completedCalls[0].Outputs["issue.link"]) + } + if len(wc.failedCalls) != 0 { + t.Errorf("WorkflowStepFailed called %d times, want 0", len(wc.failedCalls)) + } + }) + + t.Run("jira filing error calls WorkflowStepFailed", func(t *testing.T) { + event, err := slackevents.ParseEvent(rawPayload, slackevents.OptionNoVerifyToken()) + if err != nil { + t.Fatalf("ParseEvent failed: %v", err) + } + + filer := &mockIssueFiler{err: fmt.Errorf("jira unavailable")} + wc := &mockWorkflowClient{} + handler := newHandler(wc, filer) + logger := logrus.NewEntry(logrus.New()) + + handled, err := handler.Handle(&event, logger) + if handled { + t.Error("expected handled=false when jira filing fails") + } + if err == nil || err.Error() != "jira unavailable" { + t.Errorf("err = %v, want 'jira unavailable'", err) + } + if len(wc.failedCalls) != 1 { + t.Fatalf("WorkflowStepFailed called %d times, want 1", len(wc.failedCalls)) + } + if wc.failedCalls[0].ExecuteID != "exec-test-123" { + t.Errorf("ExecuteID = %q, want exec-test-123", wc.failedCalls[0].ExecuteID) + } + if wc.failedCalls[0].Message != "jira unavailable" { + t.Errorf("Message = %q, want 'jira unavailable'", wc.failedCalls[0].Message) + } + if len(wc.completedCalls) != 0 { + t.Errorf("WorkflowStepCompleted called %d times, want 0", len(wc.completedCalls)) + } + }) + + t.Run("nil filer calls WorkflowStepFailed", func(t *testing.T) { + event, err := slackevents.ParseEvent(rawPayload, slackevents.OptionNoVerifyToken()) + if err != nil { + t.Fatalf("ParseEvent failed: %v", err) + } + + wc := &mockWorkflowClient{} + handler := newHandler(wc, nil) + logger := logrus.NewEntry(logrus.New()) + + handled, err := handler.Handle(&event, logger) + if handled { + t.Error("expected handled=false when filer is nil") + } + if err == nil || err.Error() != "jira client is not configured" { + t.Errorf("err = %v, want 'jira client is not configured'", err) + } + + if len(wc.failedCalls) != 1 { + t.Fatalf("WorkflowStepFailed called %d times, want 1", len(wc.failedCalls)) + } + if wc.failedCalls[0].Message != "jira client is not configured" { + t.Errorf("Message = %q, want 'jira client is not configured'", wc.failedCalls[0].Message) + } + }) } diff --git a/pkg/slack/modals/common/simple_modals_test.go b/pkg/slack/modals/common/simple_modals_test.go index 6e8dbdebb..09dec1b91 100644 --- a/pkg/slack/modals/common/simple_modals_test.go +++ b/pkg/slack/modals/common/simple_modals_test.go @@ -160,23 +160,23 @@ func TestAppendKubeconfigBlockJSON(t *testing.T) { t.Fatalf("failed to marshal view: %v", err) } - var parsed map[string]interface{} + var parsed map[string]any if err := json.Unmarshal(data, &parsed); err != nil { t.Fatalf("failed to unmarshal JSON: %v", err) } - blocks := parsed["blocks"].([]interface{}) - richTextBlock := blocks[2].(map[string]interface{}) + blocks := parsed["blocks"].([]any) + richTextBlock := blocks[2].(map[string]any) if richTextBlock["type"] != "rich_text" { t.Errorf("rich text block type = %q, want %q", richTextBlock["type"], "rich_text") } - elements := richTextBlock["elements"].([]interface{}) - preformatted := elements[0].(map[string]interface{}) + elements := richTextBlock["elements"].([]any) + preformatted := elements[0].(map[string]any) if preformatted["type"] != "rich_text_preformatted" { t.Errorf("preformatted type = %q, want %q", preformatted["type"], "rich_text_preformatted") } - innerElements := preformatted["elements"].([]interface{}) - textEl := innerElements[0].(map[string]interface{}) + innerElements := preformatted["elements"].([]any) + textEl := innerElements[0].(map[string]any) if textEl["type"] != "text" { t.Errorf("text element type = %q, want %q", textEl["type"], "text") } @@ -198,7 +198,7 @@ func TestEmojiFieldSerialization(t *testing.T) { t.Fatalf("failed to marshal TextBlockObject: %v", err) } - var parsed map[string]interface{} + var parsed map[string]any if err := json.Unmarshal(data, &parsed); err != nil { t.Fatalf("failed to unmarshal JSON: %v", err) } @@ -218,7 +218,7 @@ func TestEmojiFieldSerialization(t *testing.T) { if err != nil { t.Fatalf("failed to marshal TextBlockObject with nil emoji: %v", err) } - var parsed2 map[string]interface{} + var parsed2 map[string]any if err := json.Unmarshal(data, &parsed2); err != nil { t.Fatalf("failed to unmarshal JSON: %v", err) } From d4db39465cd96d58811443b79e762f275608b657 Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Thu, 6 Aug 2026 10:58:20 +0200 Subject: [PATCH 09/11] Remove deprecated Steps from Apps and Jira integration Slack deprecated Steps from Apps and removed the UI to create new workflow steps. The entire Jira integration existed solely to support this feature and has no remaining callers. Co-Authored-By: Claude Opus 4.6 --- cmd/ci-chat-bot/main.go | 13 +- cmd/ci-chat-bot/slack.go | 28 +- pkg/jira/jira.go | 143 ------ pkg/slack/events/router/router.go | 5 +- .../events/workflowSubmissionEvents/bug.go | 40 -- .../workflowSubmissionEvents/consultation.go | 39 -- .../workflowSubmissionEvents/enhancement.go | 39 -- .../events/workflowSubmissionEvents/types.go | 150 ------- .../workflow_handler.go | 159 ------- .../workflow_handler_test.go | 413 ------------------ pkg/slack/interactions/router/router.go | 30 +- pkg/slack/modals/handlers.go | 190 +------- pkg/slack/modals/stepsFromApp/jira_step.go | 179 -------- .../modals/stepsFromApp/workflow_submit.go | 37 -- 14 files changed, 9 insertions(+), 1456 deletions(-) delete mode 100644 pkg/jira/jira.go delete mode 100644 pkg/slack/events/workflowSubmissionEvents/bug.go delete mode 100644 pkg/slack/events/workflowSubmissionEvents/consultation.go delete mode 100644 pkg/slack/events/workflowSubmissionEvents/enhancement.go delete mode 100644 pkg/slack/events/workflowSubmissionEvents/types.go delete mode 100644 pkg/slack/events/workflowSubmissionEvents/workflow_handler.go delete mode 100644 pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go delete mode 100644 pkg/slack/modals/stepsFromApp/jira_step.go delete mode 100644 pkg/slack/modals/stepsFromApp/workflow_submit.go diff --git a/cmd/ci-chat-bot/main.go b/cmd/ci-chat-bot/main.go index 3a092e6c1..5e2ea4631 100644 --- a/cmd/ci-chat-bot/main.go +++ b/cmd/ci-chat-bot/main.go @@ -89,8 +89,6 @@ type options struct { rosaBillingAccount string overrideLaunchLabel string overrideRosaSecretName string - - jiraOptions flagutil.JiraOptions } func (o *options) Validate() error { @@ -152,7 +150,6 @@ func run() error { opt.GitHubOptions.AddFlags(emptyFlags) opt.KubernetesOptions.AddFlags(emptyFlags) opt.InstrumentationOptions.AddFlags(emptyFlags) - opt.jiraOptions.AddFlags(emptyFlags) pflag.CommandLine.AddGoFlagSet(emptyFlags) pflag.Parse() klog.SetOutput(os.Stderr) @@ -417,16 +414,10 @@ func run() error { } bot := slack.NewBot(botToken, botSigningSecret, opt.GracePeriod, opt.Port, &workflows) - jiraclient, err := opt.jiraOptions.Client() httpClient := &http.Client{Timeout: 60 * time.Second} - if err != nil { - klog.Errorf("failed to load the Jira Client: %s", err) - Start(bot, nil, jobManager, httpClient, health, opt.InstrumentationOptions, clusterBotMetrics) - } else { - Start(bot, jiraclient.JiraClient(), jobManager, httpClient, health, opt.InstrumentationOptions, clusterBotMetrics) - } + Start(bot, jobManager, httpClient, health, opt.InstrumentationOptions, clusterBotMetrics) - return err + return nil } func processKubeConfigs(kubeConfigs map[string]rest.Config) (utils.BuildClusterClientConfigMap, error) { diff --git a/cmd/ci-chat-bot/slack.go b/cmd/ci-chat-bot/slack.go index 2766e0f7d..715e9cab1 100644 --- a/cmd/ci-chat-bot/slack.go +++ b/cmd/ci-chat-bot/slack.go @@ -6,8 +6,6 @@ import ( "strconv" "time" - jiraClient "github.com/andygrunwald/go-jira" - "github.com/openshift/ci-chat-bot/pkg/jira" "github.com/openshift/ci-chat-bot/pkg/manager" "github.com/openshift/ci-chat-bot/pkg/slack" eventhandler "github.com/openshift/ci-chat-bot/pkg/slack/events" @@ -31,21 +29,11 @@ func l(fragment string, children ...simplifypath.Node) simplifypath.Node { return simplifypath.L(fragment, children...) } -func Start(bot *slack.Bot, jiraclient *jiraClient.Client, jobManager manager.JobManager, httpclient *http.Client, health *pjutil.Health, iOpts prowflagutil.InstrumentationOptions, clusterBotMetrics *metrics.Metrics) { +func Start(bot *slack.Bot, jobManager manager.JobManager, httpclient *http.Client, health *pjutil.Health, iOpts prowflagutil.InstrumentationOptions, clusterBotMetrics *metrics.Metrics) { slackclient := slackClient.New(bot.BotToken) jobManager.SetNotifier(bot.JobResponder(slackclient)) jobManager.SetRosaNotifier(bot.RosaResponder(slackclient)) jobManager.SetMceNotifier(bot.MceResponder(slackclient)) - var issueFiler jira.IssueFiler - if jiraclient != nil { - var err error - issueFiler, err = jira.NewIssueFiler(slackclient, jiraclient) - if err != nil { - klog.Errorf(" Could not initialize Jira issue filer: %s", err) - } - } else { - issueFiler = nil - } metrics.ExposeMetrics("ci-chat-bot", config.PushGateway{}, iOpts.MetricsPort) simplifier := simplifypath.NewSimplifier(l("", // shadow element mimicking the root @@ -61,8 +49,8 @@ func Start(bot *slack.Bot, jiraclient *jiraClient.Client, jobManager manager.Job // handle the root to allow for a simple uptime probe mux.Handle("/", handler(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusOK) }))) mux.Handle("/readyz", handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))) // report ready once the server is up and responding - mux.Handle("/slack/events-endpoint", handler(handleEvent(bot.BotSigningSecret, eventrouter.ForEvents(slackclient, jobManager, bot.SupportedCommands(), issueFiler, bot.BotToken)))) - mux.Handle("/slack/interactive-endpoint", handler(handleInteraction(bot.BotSigningSecret, interactionrouter.ForModals(slackclient, jobManager, httpclient, bot.BotToken)))) + mux.Handle("/slack/events-endpoint", handler(handleEvent(bot.BotSigningSecret, eventrouter.ForEvents(slackclient, jobManager, bot.SupportedCommands())))) + mux.Handle("/slack/interactive-endpoint", handler(handleInteraction(bot.BotSigningSecret, interactionrouter.ForModals(slackclient, jobManager, httpclient)))) server := &http.Server{Addr: ":" + strconv.Itoa(bot.Port), Handler: mux, ReadHeaderTimeout: 10 * time.Second} health.ServeReady(func() bool { resp, err := http.DefaultClient.Get("http://127.0.0.1:" + strconv.Itoa(bot.Port) + "/readyz") @@ -132,16 +120,6 @@ func handleInteraction(signingSecret string, handler interactionhandler.Handler) writer.WriteHeader(http.StatusInternalServerError) return } - // Extract workflow_step_edit_id from raw JSON for deprecated Steps from Apps. - // The WorkflowStep field was removed from InteractionCallback in slack-go v0.23. - var wsData struct { - WorkflowStep struct { - WorkflowStepEditID string `json:"workflow_step_edit_id"` - } `json:"workflow_step"` - } - if err := json.Unmarshal([]byte(payload), &wsData); err == nil && wsData.WorkflowStep.WorkflowStepEditID != "" { - callback.Value = wsData.WorkflowStep.WorkflowStepEditID - } logger.WithField("interaction", callback).Trace("Read an interaction payload.") logger = logger.WithFields(fieldsFor(&callback)) diff --git a/pkg/jira/jira.go b/pkg/jira/jira.go deleted file mode 100644 index 5df52fae6..000000000 --- a/pkg/jira/jira.go +++ /dev/null @@ -1,143 +0,0 @@ -package jira - -import ( - "fmt" - "net/url" - - "github.com/andygrunwald/go-jira" - "github.com/sirupsen/logrus" - "github.com/slack-go/slack" - - jirautil "sigs.k8s.io/prow/pkg/jira" -) - -const ( - ProjectCRT = "OCPCRT" - - IssueTypeBug = "Bug" - IssueTypeStory = "Story" - IssueTypeTask = "Task" -) - -// IssueFiler knows how to file an issue in Jira -type IssueFiler interface { - FileIssue(issueType, title, description, reporter string, logger *logrus.Entry) (*jira.Issue, error) -} - -type slackClient interface { - GetUserInfo(user string) (*slack.User, error) -} - -// this adapter is needed since none of the upstream types -// are interfaces and they hold mutually ambiguous methods -type jiraAdapter struct { - delegate *jira.Client -} - -func (a *jiraAdapter) FindUser(property string) ([]jira.User, *jira.Response, error) { - // the JIRA API does not work as documented, and we can therefore not use the jira.Client.User.Find function here. - // That function supplies a query parameter to the search endpoint which will result in a 400 as you are required to pass the - // username parameter and this parameter behaves as the query parameter should. - req, _ := a.delegate.NewRequest("GET", fmt.Sprintf("rest/api/2/user/search?username=%s", url.QueryEscape(property)), nil) - var users []jira.User - response, err := a.delegate.Do(req, &users) - - return users, response, err -} - -func (a *jiraAdapter) CreateIssue(issue *jira.Issue) (*jira.Issue, *jira.Response, error) { - return a.delegate.Issue.Create(issue) -} - -type jiraClient interface { - FindUser(property string) ([]jira.User, *jira.Response, error) - CreateIssue(issue *jira.Issue) (*jira.Issue, *jira.Response, error) -} - -// filer caches information from Jira to make filing issues easier -type filer struct { - slackClient slackClient - jiraClient jiraClient - // project caches metadata for the Jira project we create - // issues under - this will never change so we can read it - // once at startup and reuse it forever - project jira.Project - // issueTypesByName caches Jira issue types by their given - // names - these will never change so we can read them once - // at startup and reuse them forever - issueTypesByName map[string]jira.IssueType - // botUser caches the bot's Jira user metadata for use as a - // back-stop when no requester can be found to match the - // Slack user that is interacting with us - botUser *jira.User -} - -// FileIssue files an issue, closing over a number of Jira-specific API -// quirks like how issue types and projects are provided, as well as -// transforming the Slack reporter ID to a Jira user, when possible. -func (f *filer) FileIssue(issueType, title, description, reporter string, logger *logrus.Entry) (*jira.Issue, error) { - requester := f.resolveRequester(reporter, logger) - description = fmt.Sprintf("%s\n\nThis issue was filed by %s (%s)", description, reporter, requester.EmailAddress) - logger.WithFields(logrus.Fields{ - "title": title, - "reporter": requester.Name, - "type": issueType, - }).Debug("Filing Jira issue.") - toCreate := &jira.Issue{Fields: &jira.IssueFields{ - Project: f.project, - Reporter: requester, - Type: f.issueTypesByName[issueType], - Summary: title, - Description: description, - }} - issue, response, err := f.jiraClient.CreateIssue(toCreate) - return issue, jirautil.HandleJiraError(response, err) -} - -// resolveRequester attempts to get more information about the Slack -// user that requested the Jira issue, doing everything best-effort -func (f *filer) resolveRequester(reporter string, logger *logrus.Entry) *jira.User { - var requester *jira.User - jiraUsers, response, err := f.jiraClient.FindUser(reporter) - if err := jirautil.HandleJiraError(response, err); err != nil { - logger.WithError(err).Warn("could not search Jira for requester") - } - if len(jiraUsers) != 0 { - requester = &jiraUsers[0] - } - if requester == nil { - logger.Infof("Could not find a Jira user for Slack user %q, defaulting to bot user.", reporter) - requester = f.botUser - } - return requester -} - -func NewIssueFiler(slackClient *slack.Client, jiraClient *jira.Client) (IssueFiler, error) { - filer := &filer{ - slackClient: slackClient, - jiraClient: &jiraAdapter{delegate: jiraClient}, - issueTypesByName: map[string]jira.IssueType{}, - } - - project, response, err := jiraClient.Project.Get(ProjectCRT) - if err := jirautil.HandleJiraError(response, err); err != nil { - return nil, fmt.Errorf("could not find Jira project %s: %w", ProjectCRT, err) - } - filer.project = *project - for _, t := range project.IssueTypes { - filer.issueTypesByName[t.Name] = t - } - for _, name := range []string{IssueTypeStory, IssueTypeBug, IssueTypeTask} { - if _, found := filer.issueTypesByName[name]; !found { - return nil, fmt.Errorf("could not find issue type %s in Jira for project %s", name, ProjectCRT) - } - } - - botUser, response, err := jiraClient.User.GetSelf() - if err := jirautil.HandleJiraError(response, err); err != nil { - return nil, fmt.Errorf("could not resolve Jira bot user: %w", err) - } - filer.botUser = botUser - - return filer, nil -} diff --git a/pkg/slack/events/router/router.go b/pkg/slack/events/router/router.go index 5e4d956af..281701b23 100644 --- a/pkg/slack/events/router/router.go +++ b/pkg/slack/events/router/router.go @@ -1,10 +1,8 @@ package router import ( - "github.com/openshift/ci-chat-bot/pkg/jira" "github.com/openshift/ci-chat-bot/pkg/manager" "github.com/openshift/ci-chat-bot/pkg/slack/events/apphome" - "github.com/openshift/ci-chat-bot/pkg/slack/events/workflowSubmissionEvents" "github.com/openshift/ci-chat-bot/pkg/slack/mention" slackCommandParser "github.com/openshift/ci-chat-bot/pkg/slack/parser" "github.com/slack-go/slack" @@ -15,11 +13,10 @@ import ( // ForEvents returns a Handler that appropriately routes // event callbacks for the handlers we know about -func ForEvents(client *slack.Client, manager manager.JobManager, botCommands []slackCommandParser.BotCommand, filer jira.IssueFiler, botToken string) events.Handler { +func ForEvents(client *slack.Client, manager manager.JobManager, botCommands []slackCommandParser.BotCommand) events.Handler { return events.MultiHandler( messages.Handle(client, manager, botCommands), mention.Handler(client), - workflowSubmissionEvents.Handler(botToken, filer), apphome.Handler(client, manager), ) } diff --git a/pkg/slack/events/workflowSubmissionEvents/bug.go b/pkg/slack/events/workflowSubmissionEvents/bug.go deleted file mode 100644 index 9b566a76b..000000000 --- a/pkg/slack/events/workflowSubmissionEvents/bug.go +++ /dev/null @@ -1,40 +0,0 @@ -package workflowSubmissionEvents - -import ( - "text/template" - - "github.com/openshift/ci-chat-bot/pkg/jira" -) - -const BugIdentifier Identifier = "bug" - -const ( - AffectedComponent = "affected_component" - OtherComponent = "other_component_not_listed" - IncorrectBehaviour = "incorrect_behaviour" - ExpectedBehaviour = "expected_behaviour" - Impact = "impact" - IsReproducible = "is_reproducible" -) - -func BugParameters() JiraIssueParameters { - return JiraIssueParameters{ - Id: BugIdentifier, - IssueType: jira.IssueTypeBug, - Template: template.Must(template.New(string(BugIdentifier)).Parse(`h3. Symptomatic Behavior -{{ .` + IncorrectBehaviour + ` }} - -h3. Expected Behavior -{{ .` + ExpectedBehaviour + ` }} - -h3. Impact -{{ .` + Impact + ` }} - -h3. Category -{{if eq .` + AffectedComponent + ` "Other"}}{{ .` + OtherComponent + ` }}{{else}}{{ .` + AffectedComponent + ` }}{{end}} - -h3. How to Reproduce -{{ .` + IsReproducible + ` }}`)), - Fields: []string{BlockIdTitle, AffectedComponent, OtherComponent, IncorrectBehaviour, ExpectedBehaviour, Impact, IsReproducible}, - } -} diff --git a/pkg/slack/events/workflowSubmissionEvents/consultation.go b/pkg/slack/events/workflowSubmissionEvents/consultation.go deleted file mode 100644 index 7355ce5c5..000000000 --- a/pkg/slack/events/workflowSubmissionEvents/consultation.go +++ /dev/null @@ -1,39 +0,0 @@ -package workflowSubmissionEvents - -import ( - "text/template" - - "github.com/openshift/ci-chat-bot/pkg/jira" - "github.com/openshift/ci-chat-bot/pkg/slack/modals" -) - -const ConsultationIdentifier Identifier = "consultation" - -const ( - BlockIdRequirement = "consultation_requirement" - BlockIdPrevious = "consultation_previous" - BlockIdAcceptanceCriteria = "consultation_acceptance_criteria" - BlockIdAdditional = "consultation_additional" -) - -func consultationParameters() JiraIssueParameters { - return JiraIssueParameters{ - Id: ConsultationIdentifier, - IssueType: jira.IssueTypeStory, - Template: template.Must(template.New(string(ConsultationIdentifier)).Funcs(modals.BulletListFunc()).Parse(`h3. Requirement -{{ .` + BlockIdRequirement + ` }} - -h3. Previous Efforts -{{ .` + BlockIdPrevious + ` }} - -h3. Acceptance Criteria -{{ toBulletList .` + BlockIdAcceptanceCriteria + ` }} - -{{- if .` + BlockIdAdditional + ` }} - -h3. Additional Details -{{ .` + BlockIdAdditional + ` }} -{{- end }}`)), - Fields: []string{modals.BlockIdTitle, BlockIdRequirement, BlockIdPrevious, BlockIdAcceptanceCriteria, BlockIdAdditional}, - } -} diff --git a/pkg/slack/events/workflowSubmissionEvents/enhancement.go b/pkg/slack/events/workflowSubmissionEvents/enhancement.go deleted file mode 100644 index 5cc70d28f..000000000 --- a/pkg/slack/events/workflowSubmissionEvents/enhancement.go +++ /dev/null @@ -1,39 +0,0 @@ -package workflowSubmissionEvents - -import ( - "text/template" - - "github.com/openshift/ci-chat-bot/pkg/jira" - "github.com/openshift/ci-chat-bot/pkg/slack/modals" -) - -const EnhancementIdentifier Identifier = "enhancement" - -const ( - BlockIdAsA = "as_a" - BlockIdIWant = "i_want" - BlockIdSoThat = "so_that" - BlockIdSummary = "enhancement_summary" - BlockIdImplementation = "enhancement_implementation" -) - -func enhancementParameters() JiraIssueParameters { - return JiraIssueParameters{ - Id: EnhancementIdentifier, - IssueType: jira.IssueTypeStory, - Template: template.Must(template.New(string(EnhancementIdentifier)).Funcs(modals.BulletListFunc()).Parse(`h3. Overview -As a {{ .` + BlockIdAsA + ` }} -I want {{ .` + BlockIdIWant + ` }} -So that {{ .` + BlockIdSoThat + ` }} - -h3. Summary -{{ .` + BlockIdSummary + ` }} - -{{- if .` + BlockIdImplementation + ` }} - -h3. Implementation Details -{{ .` + BlockIdImplementation + ` }} -{{- end }}`)), - Fields: []string{modals.BlockIdTitle, BlockIdAsA, BlockIdIWant, BlockIdSoThat, BlockIdSummary, BlockIdImplementation}, - } -} diff --git a/pkg/slack/events/workflowSubmissionEvents/types.go b/pkg/slack/events/workflowSubmissionEvents/types.go deleted file mode 100644 index 9fd945e68..000000000 --- a/pkg/slack/events/workflowSubmissionEvents/types.go +++ /dev/null @@ -1,150 +0,0 @@ -package workflowSubmissionEvents - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "text/template" - "time" -) - -type workflowSubmit interface { - WorkflowStepCompleted(workflowStepExecuteID string, outputs map[string]string) error - WorkflowStepFailed(workflowStepExecuteID string, errorMessage string) error -} - -type Identifier string - -// JiraIssueParameters holds the metadata used to create a Jira issue -type JiraIssueParameters struct { - Id Identifier - IssueType string - Template *template.Template - Fields []string -} - -// workflowStepExecuteEvent mirrors the deprecated slackevents.WorkflowStepExecuteEvent -// which was removed from slack-go/slack v0.23.0+. -type workflowStepExecuteEvent struct { - Type string `json:"type"` - CallbackID string `json:"callback_id"` - WorkflowStep eventWorkflowStep `json:"workflow_step"` - EventTimestamp string `json:"event_ts"` -} - -type eventWorkflowStep struct { - WorkflowStepExecuteID string `json:"workflow_step_execute_id"` - WorkflowID string `json:"workflow_id"` - WorkflowInstanceID string `json:"workflow_instance_id"` - StepID string `json:"step_id"` - Inputs WorkflowStepInputs `json:"inputs,omitempty"` - Outputs []WorkflowStepOutput `json:"outputs,omitempty"` -} - -type WorkflowStepInputElement struct { - Value string `json:"value"` - SkipVariableReplacement bool `json:"skip_variable_replacement"` -} - -type WorkflowStepInputs map[string]WorkflowStepInputElement - -type WorkflowStepOutput struct { - Name string `json:"name"` - Type string `json:"type"` - Label string `json:"label"` -} - -// SlackWorkflowClient wraps a tokenized HTTP client to call the deprecated -// Slack workflow step API methods (workflows.stepCompleted, workflows.stepFailed, -// workflows.updateStep). -type SlackWorkflowClient struct { - token string - endpoint string -} - -func NewSlackWorkflowClient(token string) *SlackWorkflowClient { - return &SlackWorkflowClient{ - token: token, - endpoint: "https://slack.com/api/", - } -} - -type workflowUpdateStepRequest struct { - WorkflowStepEditID string `json:"workflow_step_edit_id"` - Inputs WorkflowStepInputs `json:"inputs,omitempty"` - Outputs []WorkflowStepOutput `json:"outputs,omitempty"` -} - -type workflowStepCompletedRequest struct { - WorkflowStepExecuteID string `json:"workflow_step_execute_id"` - Outputs map[string]string `json:"outputs,omitempty"` -} - -type workflowStepFailedRequest struct { - WorkflowStepExecuteID string `json:"workflow_step_execute_id"` - Error struct { - Message string `json:"message"` - } `json:"error"` -} - -type slackResponse struct { - OK bool `json:"ok"` - Error string `json:"error,omitempty"` -} - -func (c *SlackWorkflowClient) WorkflowStepCompleted(workflowStepExecuteID string, outputs map[string]string) error { - r := &workflowStepCompletedRequest{ - WorkflowStepExecuteID: workflowStepExecuteID, - Outputs: outputs, - } - return c.postJSON("workflows.stepCompleted", r) -} - -func (c *SlackWorkflowClient) WorkflowStepFailed(workflowStepExecuteID string, errorMessage string) error { - r := &workflowStepFailedRequest{ - WorkflowStepExecuteID: workflowStepExecuteID, - } - r.Error.Message = errorMessage - return c.postJSON("workflows.stepFailed", r) -} - -func (c *SlackWorkflowClient) SaveWorkflowStepConfiguration(workflowStepEditID string, inputs WorkflowStepInputs, outputs []WorkflowStepOutput) error { - r := &workflowUpdateStepRequest{ - WorkflowStepEditID: workflowStepEditID, - Inputs: inputs, - Outputs: outputs, - } - return c.postJSON("workflows.updateStep", r) -} - -func (c *SlackWorkflowClient) postJSON(method string, payload any) error { - body, err := json.Marshal(payload) - if err != nil { - return err - } - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - req, err := http.NewRequestWithContext(ctx, "POST", c.endpoint+method, bytes.NewReader(body)) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json; charset=utf-8") - req.Header.Set("Authorization", "Bearer "+c.token) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() - - var sr slackResponse - if err := json.NewDecoder(resp.Body).Decode(&sr); err != nil { - return err - } - if !sr.OK { - return fmt.Errorf("slack API %s: %s", method, sr.Error) - } - return nil -} diff --git a/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go b/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go deleted file mode 100644 index 6c491e12b..000000000 --- a/pkg/slack/events/workflowSubmissionEvents/workflow_handler.go +++ /dev/null @@ -1,159 +0,0 @@ -package workflowSubmissionEvents - -import ( - "bytes" - "encoding/json" - "fmt" - - jiraClient "github.com/andygrunwald/go-jira" - "github.com/openshift/ci-chat-bot/pkg/jira" - "github.com/openshift/ci-chat-bot/pkg/slack/events" - "github.com/sirupsen/logrus" - "github.com/slack-go/slack/slackevents" -) - -func init() { - // Re-register workflow_step_execute in the event mapping. - // The slack-go v0.23+ library removed WorkflowStepExecuteEvent but Slack - // still sends this event type for deprecated Steps from Apps integrations. - // Without this registration, slackevents.ParseEvent rejects the event - // before our handler ever sees it. - slackevents.EventsAPIInnerEventMapping[slackevents.WorkflowStepExecute] = workflowStepExecuteEvent{} -} - -const ( - // BlockIdTitle is the block identifier to use for inputs - // that should be used as the title of a Jira issue - BlockIdTitle = "title" -) - -const ( - TicketTitle = "ticket_title" - UserDetails = "user_details" -) - -func Handler(token string, filer jira.IssueFiler) events.PartialHandler { - return newHandler(NewSlackWorkflowClient(token), filer) -} - -func newHandler(wc workflowSubmit, filer jira.IssueFiler) events.PartialHandler { - return events.PartialHandlerFunc("workflow-execution-event", func(callback *slackevents.EventsAPIEvent, logger *logrus.Entry) (handled bool, err error) { - if callback.Type != slackevents.CallbackEvent { - return false, nil - } - raw, err := json.Marshal(callback.InnerEvent.Data) - if err != nil { - return false, nil - } - var event workflowStepExecuteEvent - if err := json.Unmarshal(raw, &event); err != nil { - return false, nil - } - if event.Type != "workflow_step_execute" { - return false, nil - } - switch event.CallbackID { - case "jira_ticket": - err := handleJiraStep(wc, &event, filer) - if err != nil { - error := wc.WorkflowStepFailed(event.WorkflowStep.WorkflowStepExecuteID, err.Error()) - if error != nil { - return false, error - } - return false, err - } - } - return true, err - }) -} - -func checkTicketType(event *workflowStepExecuteEvent) (ticketType string, supported bool) { - for key, output := range event.WorkflowStep.Inputs { - if key == "ticket_type" { - switch output.Value { - case string(BugIdentifier): - return string(BugIdentifier), true - case string(ConsultationIdentifier): - return string(ConsultationIdentifier), true - case string(EnhancementIdentifier): - return string(EnhancementIdentifier), true - default: - return output.Value, false - } - } - } - return "unsupported_ticket_type", false -} - -func handleJiraStep(client workflowSubmit, event *workflowStepExecuteEvent, filer jira.IssueFiler) error { - if filer == nil { - return fmt.Errorf("jira client is not configured") - } - ticketType, isSupported := checkTicketType(event) - - if !isSupported { - return fmt.Errorf("unsuported ticket type %s", ticketType) - } - var issue *jiraClient.Issue - var err error - - switch ticketType { - case string(BugIdentifier): - issue, err = fileTicket(event, BugParameters(), filer) - if err != nil { - return err - } - case string(EnhancementIdentifier): - issue, err = fileTicket(event, enhancementParameters(), filer) - if err != nil { - return err - } - case string(ConsultationIdentifier): - issue, err = fileTicket(event, consultationParameters(), filer) - if err != nil { - return err - } - } - outgoingOutputs := make(map[string]string) - for _, incomingOutputs := range event.WorkflowStep.Outputs { - switch incomingOutputs.Name { - case "issue.key": - outgoingOutputs[incomingOutputs.Name] = issue.Key - case "issue.link": - outgoingOutputs[incomingOutputs.Name] = fmt.Sprintf("https://issues.redhat.com/browse/%s", issue.Key) - } - } - err = client.WorkflowStepCompleted(event.WorkflowStep.WorkflowStepExecuteID, outgoingOutputs) - if err != nil { - return err - } - return nil -} - -func fileTicket(event *workflowStepExecuteEvent, parameters JiraIssueParameters, filer jira.IssueFiler) (*jiraClient.Issue, error) { - title := "not_defined" - reporter := "not_defined" - data := make(map[string]string) - for key, inputs := range event.WorkflowStep.Inputs { - if key == TicketTitle { - title = inputs.Value - continue - } - if key == UserDetails { - reporter = inputs.Value - continue - } - data[key] = inputs.Value - } - body := &bytes.Buffer{} - if err := parameters.Template.Execute(body, data); err != nil { - return nil, fmt.Errorf("failed to render %s template: %w", parameters.Id, err) - } - logger := logrus.WithField("api", "events") - issue, err := filer.FileIssue(parameters.IssueType, title, body.String(), reporter, logger) - if err != nil { - logger.WithError(err).Errorf("Failed to create %s Jira.", parameters.Id) - return nil, err - } - return issue, nil -} diff --git a/pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go b/pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go deleted file mode 100644 index b16d8ab3e..000000000 --- a/pkg/slack/events/workflowSubmissionEvents/workflow_handler_test.go +++ /dev/null @@ -1,413 +0,0 @@ -package workflowSubmissionEvents - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "testing" - - jiraAPI "github.com/andygrunwald/go-jira" - "github.com/sirupsen/logrus" - "github.com/slack-go/slack/slackevents" -) - -type mockIssueFiler struct { - issue *jiraAPI.Issue - err error - calls []mockFileIssueCall -} - -type mockFileIssueCall struct { - IssueType string - Title string - Description string - Reporter string -} - -func (m *mockIssueFiler) FileIssue(issueType, title, description, reporter string, _ *logrus.Entry) (*jiraAPI.Issue, error) { - m.calls = append(m.calls, mockFileIssueCall{ - IssueType: issueType, - Title: title, - Description: description, - Reporter: reporter, - }) - return m.issue, m.err -} - -type mockWorkflowClient struct { - completedCalls []mockCompletedCall - failedCalls []mockFailedCall - completeErr error - failErr error -} - -type mockCompletedCall struct { - ExecuteID string - Outputs map[string]string -} - -type mockFailedCall struct { - ExecuteID string - Message string -} - -func (m *mockWorkflowClient) WorkflowStepCompleted(execID string, outputs map[string]string) error { - m.completedCalls = append(m.completedCalls, mockCompletedCall{ExecuteID: execID, Outputs: outputs}) - return m.completeErr -} - -func (m *mockWorkflowClient) WorkflowStepFailed(execID string, msg string) error { - m.failedCalls = append(m.failedCalls, mockFailedCall{ExecuteID: execID, Message: msg}) - return m.failErr -} - -func TestSlackWorkflowClientSerialization(t *testing.T) { - t.Run("stepCompleted sends correct JSON", func(t *testing.T) { - var gotBody map[string]any - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, _ := io.ReadAll(r.Body) - if err := json.Unmarshal(body, &gotBody); err != nil { - t.Errorf("failed to unmarshal request body: %v", err) - } - - if r.Header.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type = %q, want application/json", r.Header.Get("Content-Type")) - } - if r.Header.Get("Authorization") != "Bearer test-token" { - t.Errorf("Authorization = %q, want Bearer test-token", r.Header.Get("Authorization")) - } - - w.Header().Set("Content-Type", "application/json") - _, _ = fmt.Fprint(w, `{"ok":true}`) - })) - defer srv.Close() - - client := &SlackWorkflowClient{token: "test-token", endpoint: srv.URL + "/"} - outputs := map[string]string{"issue.key": "PROJ-123", "issue.link": "https://issues.redhat.com/browse/PROJ-123"} - err := client.WorkflowStepCompleted("exec-id-1", outputs) - if err != nil { - t.Fatalf("WorkflowStepCompleted returned error: %v", err) - } - - if gotBody["workflow_step_execute_id"] != "exec-id-1" { - t.Errorf("workflow_step_execute_id = %v, want exec-id-1", gotBody["workflow_step_execute_id"]) - } - outputsMap := gotBody["outputs"].(map[string]any) - if outputsMap["issue.key"] != "PROJ-123" { - t.Errorf("outputs[issue.key] = %v, want PROJ-123", outputsMap["issue.key"]) - } - }) - - t.Run("stepFailed sends correct JSON", func(t *testing.T) { - var gotBody map[string]any - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, _ := io.ReadAll(r.Body) - if err := json.Unmarshal(body, &gotBody); err != nil { - t.Errorf("failed to unmarshal request body: %v", err) - } - w.Header().Set("Content-Type", "application/json") - _, _ = fmt.Fprint(w, `{"ok":true}`) - })) - defer srv.Close() - - client := &SlackWorkflowClient{token: "test-token", endpoint: srv.URL + "/"} - err := client.WorkflowStepFailed("exec-id-2", "something broke") - if err != nil { - t.Fatalf("WorkflowStepFailed returned error: %v", err) - } - - if gotBody["workflow_step_execute_id"] != "exec-id-2" { - t.Errorf("workflow_step_execute_id = %v, want exec-id-2", gotBody["workflow_step_execute_id"]) - } - errorObj := gotBody["error"].(map[string]any) - if errorObj["message"] != "something broke" { - t.Errorf("error.message = %v, want 'something broke'", errorObj["message"]) - } - }) - - t.Run("saveWorkflowStepConfiguration sends correct JSON", func(t *testing.T) { - var gotBody map[string]any - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, _ := io.ReadAll(r.Body) - if err := json.Unmarshal(body, &gotBody); err != nil { - t.Errorf("failed to unmarshal request body: %v", err) - } - w.Header().Set("Content-Type", "application/json") - _, _ = fmt.Fprint(w, `{"ok":true}`) - })) - defer srv.Close() - - client := &SlackWorkflowClient{token: "test-token", endpoint: srv.URL + "/"} - inputs := WorkflowStepInputs{ - "ticket_type": {Value: "bug", SkipVariableReplacement: false}, - "ticket_title": {Value: "Test bug"}, - } - outputs := []WorkflowStepOutput{ - {Name: "issue.key", Type: "text", Label: "Issue Key"}, - } - err := client.SaveWorkflowStepConfiguration("edit-id-1", inputs, outputs) - if err != nil { - t.Fatalf("SaveWorkflowStepConfiguration returned error: %v", err) - } - - if gotBody["workflow_step_edit_id"] != "edit-id-1" { - t.Errorf("workflow_step_edit_id = %v, want edit-id-1", gotBody["workflow_step_edit_id"]) - } - inputsMap := gotBody["inputs"].(map[string]any) - ticketType := inputsMap["ticket_type"].(map[string]any) - if ticketType["value"] != "bug" { - t.Errorf("inputs.ticket_type.value = %v, want bug", ticketType["value"]) - } - }) - - t.Run("API error is returned", func(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - _, _ = fmt.Fprint(w, `{"ok":false,"error":"invalid_auth"}`) - })) - defer srv.Close() - - client := &SlackWorkflowClient{token: "bad-token", endpoint: srv.URL + "/"} - err := client.WorkflowStepCompleted("exec-id", nil) - if err == nil { - t.Fatal("expected error, got nil") - } - if err.Error() != "slack API workflows.stepCompleted: invalid_auth" { - t.Errorf("error = %q, want slack API error", err.Error()) - } - }) -} - -func TestWorkflowStepExecuteEventParsing(t *testing.T) { - rawEvent := `{ - "type": "workflow_step_execute", - "callback_id": "jira_ticket", - "workflow_step": { - "workflow_step_execute_id": "exec-123", - "workflow_id": "wf-456", - "workflow_instance_id": "inst-789", - "step_id": "step-1", - "inputs": { - "ticket_type": {"value": "bug", "skip_variable_replacement": false}, - "ticket_title": {"value": "Test Bug Title"} - }, - "outputs": [ - {"name": "issue.key", "type": "text", "label": "Issue Key"}, - {"name": "issue.link", "type": "text", "label": "Issue Link"} - ] - }, - "event_ts": "1234567890.123456" - }` - - // Simulate the marshal/unmarshal round-trip the handler does - var innerData any - if err := json.Unmarshal([]byte(rawEvent), &innerData); err != nil { - t.Fatalf("failed to unmarshal raw event: %v", err) - } - - raw, err := json.Marshal(innerData) - if err != nil { - t.Fatalf("failed to re-marshal: %v", err) - } - - var event workflowStepExecuteEvent - if err := json.Unmarshal(raw, &event); err != nil { - t.Fatalf("failed to unmarshal into workflowStepExecuteEvent: %v", err) - } - - if event.Type != "workflow_step_execute" { - t.Errorf("Type = %q, want workflow_step_execute", event.Type) - } - if event.CallbackID != "jira_ticket" { - t.Errorf("CallbackID = %q, want jira_ticket", event.CallbackID) - } - if event.WorkflowStep.WorkflowStepExecuteID != "exec-123" { - t.Errorf("WorkflowStepExecuteID = %q, want exec-123", event.WorkflowStep.WorkflowStepExecuteID) - } - if len(event.WorkflowStep.Inputs) != 2 { - t.Errorf("Inputs count = %d, want 2", len(event.WorkflowStep.Inputs)) - } - if event.WorkflowStep.Inputs["ticket_type"].Value != "bug" { - t.Errorf("ticket_type = %q, want bug", event.WorkflowStep.Inputs["ticket_type"].Value) - } - if len(event.WorkflowStep.Outputs) != 2 { - t.Errorf("Outputs count = %d, want 2", len(event.WorkflowStep.Outputs)) - } - if event.WorkflowStep.Outputs[0].Name != "issue.key" { - t.Errorf("first output name = %q, want issue.key", event.WorkflowStep.Outputs[0].Name) - } -} - -func TestHandlerRoutesWorkflowStepExecute(t *testing.T) { - handler := Handler("fake-token", nil) - - t.Run("ignores non-callback events", func(t *testing.T) { - event := &slackevents.EventsAPIEvent{ - Type: slackevents.URLVerification, - } - handled, err := handler.Handle(event, nil) - if handled || err != nil { - t.Errorf("expected (false, nil), got (%v, %v)", handled, err) - } - }) - - t.Run("ignores non-workflow_step_execute inner events", func(t *testing.T) { - event := &slackevents.EventsAPIEvent{ - Type: slackevents.CallbackEvent, - InnerEvent: slackevents.EventsAPIInnerEvent{ - Data: &slackevents.MessageEvent{}, - }, - } - handled, err := handler.Handle(event, nil) - if handled { - t.Error("expected handled=false for MessageEvent") - } - _ = err - }) -} - -func TestHandlerEndToEndJiraTicket(t *testing.T) { - rawPayload := []byte(`{ - "token": "fake", - "team_id": "T0001", - "api_app_id": "A0001", - "event": { - "type": "workflow_step_execute", - "callback_id": "jira_ticket", - "workflow_step": { - "workflow_step_execute_id": "exec-test-123", - "workflow_id": "wf-001", - "workflow_instance_id": "inst-001", - "step_id": "step-001", - "inputs": { - "ticket_type": {"value": "bug", "skip_variable_replacement": false}, - "ticket_title": {"value": "Test Bug"}, - "user_details": {"value": "U12345"}, - "incorrect_behaviour": {"value": "it breaks"}, - "expected_behaviour": {"value": "it works"}, - "impact": {"value": "high"}, - "affected_component": {"value": "Testing"}, - "is_reproducible": {"value": "yes"} - }, - "outputs": [ - {"name": "issue.key", "type": "text", "label": "Issue Key"}, - {"name": "issue.link", "type": "text", "label": "Issue Link"} - ] - }, - "event_ts": "1234567890.123456" - }, - "type": "event_callback", - "event_id": "Ev0001", - "event_time": 1234567890 - }`) - - t.Run("successful jira filing calls WorkflowStepCompleted", func(t *testing.T) { - event, err := slackevents.ParseEvent(rawPayload, slackevents.OptionNoVerifyToken()) - if err != nil { - t.Fatalf("ParseEvent failed (init() registration may be missing): %v", err) - } - - filer := &mockIssueFiler{issue: &jiraAPI.Issue{Key: "OCPCRT-999"}} - wc := &mockWorkflowClient{} - handler := newHandler(wc, filer) - logger := logrus.NewEntry(logrus.New()) - - handled, err := handler.Handle(&event, logger) - if err != nil { - t.Fatalf("Handle returned error: %v", err) - } - if !handled { - t.Fatal("expected handled=true for jira_ticket callback") - } - - if len(filer.calls) != 1 { - t.Fatalf("FileIssue called %d times, want 1", len(filer.calls)) - } - if filer.calls[0].IssueType != "Bug" { - t.Errorf("IssueType = %q, want Bug", filer.calls[0].IssueType) - } - if filer.calls[0].Title != "Test Bug" { - t.Errorf("Title = %q, want 'Test Bug'", filer.calls[0].Title) - } - if filer.calls[0].Reporter != "U12345" { - t.Errorf("Reporter = %q, want U12345", filer.calls[0].Reporter) - } - - if len(wc.completedCalls) != 1 { - t.Fatalf("WorkflowStepCompleted called %d times, want 1", len(wc.completedCalls)) - } - if wc.completedCalls[0].ExecuteID != "exec-test-123" { - t.Errorf("ExecuteID = %q, want exec-test-123", wc.completedCalls[0].ExecuteID) - } - if wc.completedCalls[0].Outputs["issue.key"] != "OCPCRT-999" { - t.Errorf("issue.key = %q, want OCPCRT-999", wc.completedCalls[0].Outputs["issue.key"]) - } - if wc.completedCalls[0].Outputs["issue.link"] != "https://issues.redhat.com/browse/OCPCRT-999" { - t.Errorf("issue.link = %q, want OCPCRT-999 browse URL", wc.completedCalls[0].Outputs["issue.link"]) - } - if len(wc.failedCalls) != 0 { - t.Errorf("WorkflowStepFailed called %d times, want 0", len(wc.failedCalls)) - } - }) - - t.Run("jira filing error calls WorkflowStepFailed", func(t *testing.T) { - event, err := slackevents.ParseEvent(rawPayload, slackevents.OptionNoVerifyToken()) - if err != nil { - t.Fatalf("ParseEvent failed: %v", err) - } - - filer := &mockIssueFiler{err: fmt.Errorf("jira unavailable")} - wc := &mockWorkflowClient{} - handler := newHandler(wc, filer) - logger := logrus.NewEntry(logrus.New()) - - handled, err := handler.Handle(&event, logger) - if handled { - t.Error("expected handled=false when jira filing fails") - } - if err == nil || err.Error() != "jira unavailable" { - t.Errorf("err = %v, want 'jira unavailable'", err) - } - - if len(wc.failedCalls) != 1 { - t.Fatalf("WorkflowStepFailed called %d times, want 1", len(wc.failedCalls)) - } - if wc.failedCalls[0].ExecuteID != "exec-test-123" { - t.Errorf("ExecuteID = %q, want exec-test-123", wc.failedCalls[0].ExecuteID) - } - if wc.failedCalls[0].Message != "jira unavailable" { - t.Errorf("Message = %q, want 'jira unavailable'", wc.failedCalls[0].Message) - } - if len(wc.completedCalls) != 0 { - t.Errorf("WorkflowStepCompleted called %d times, want 0", len(wc.completedCalls)) - } - }) - - t.Run("nil filer calls WorkflowStepFailed", func(t *testing.T) { - event, err := slackevents.ParseEvent(rawPayload, slackevents.OptionNoVerifyToken()) - if err != nil { - t.Fatalf("ParseEvent failed: %v", err) - } - - wc := &mockWorkflowClient{} - handler := newHandler(wc, nil) - logger := logrus.NewEntry(logrus.New()) - - handled, err := handler.Handle(&event, logger) - if handled { - t.Error("expected handled=false when filer is nil") - } - if err == nil || err.Error() != "jira client is not configured" { - t.Errorf("err = %v, want 'jira client is not configured'", err) - } - - if len(wc.failedCalls) != 1 { - t.Fatalf("WorkflowStepFailed called %d times, want 1", len(wc.failedCalls)) - } - if wc.failedCalls[0].Message != "jira client is not configured" { - t.Errorf("Message = %q, want 'jira client is not configured'", wc.failedCalls[0].Message) - } - }) -} diff --git a/pkg/slack/interactions/router/router.go b/pkg/slack/interactions/router/router.go index 3b5498e8b..875222dfc 100644 --- a/pkg/slack/interactions/router/router.go +++ b/pkg/slack/interactions/router/router.go @@ -5,7 +5,6 @@ import ( "net/http" "github.com/openshift/ci-chat-bot/pkg/manager" - "github.com/openshift/ci-chat-bot/pkg/slack/events/workflowSubmissionEvents" "github.com/openshift/ci-chat-bot/pkg/slack/interactions" "github.com/openshift/ci-chat-bot/pkg/slack/modals" "github.com/openshift/ci-chat-bot/pkg/slack/modals/auth" @@ -18,19 +17,15 @@ import ( mcelist "github.com/openshift/ci-chat-bot/pkg/slack/modals/mce/list" mcelookup "github.com/openshift/ci-chat-bot/pkg/slack/modals/mce/lookup" "github.com/openshift/ci-chat-bot/pkg/slack/modals/refresh" - "github.com/openshift/ci-chat-bot/pkg/slack/modals/stepsFromApp" "github.com/sirupsen/logrus" "github.com/slack-go/slack" ) // ForModals returns a Handler that appropriately routes // interaction callbacks for the modals we know about -func ForModals(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client, botToken string) interactions.Handler { +func ForModals(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { router := &modalRouter{ - slackClient: &combinedSlackClient{ - Client: client, - SlackWorkflowClient: workflowSubmissionEvents.NewSlackWorkflowClient(botToken), - }, + slackClient: client, viewsByID: map[modals.Identifier]slack.ModalViewRequest{}, handlersByIDAndType: map[modals.Identifier]map[slack.InteractionType]interactions.Handler{}, } @@ -84,8 +79,6 @@ type modalRouter struct { // Handle routes the interaction callback to the appropriate handler func (r *modalRouter) Handle(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { switch callback.Type { - case slack.InteractionTypeWorkflowStepEdit: - return nil, r.viewForApplicationStep(callback, logger) case slack.InteractionTypeShortcut: return nil, r.viewForShortcut(callback, logger) case slack.InteractionTypeBlockActions: @@ -94,10 +87,6 @@ func (r *modalRouter) Handle(callback *slack.InteractionCallback, logger *logrus } return r.delegate(callback, logger) case slack.InteractionTypeViewSubmission: - if metadataToIdentifier(callback.View.PrivateMetadata, logger) == string(slack.InteractionTypeWorkflowStepEdit) { - input, output := stepsFromApp.StepFromAppSubmit(callback) - return nil, r.slackClient.SaveWorkflowStepConfiguration(callback.Value, input, output) - } return r.delegate(callback, logger) default: return r.delegate(callback, logger) @@ -123,12 +112,6 @@ func isMessageButtonPress(callback *slack.InteractionCallback) bool { type slackClient interface { OpenView(triggerID string, view slack.ModalViewRequest) (*slack.ViewResponse, error) - SaveWorkflowStepConfiguration(workflowStepEditID string, inputs workflowSubmissionEvents.WorkflowStepInputs, outputs []workflowSubmissionEvents.WorkflowStepOutput) error -} - -type combinedSlackClient struct { - *slack.Client - *workflowSubmissionEvents.SlackWorkflowClient } // viewForShortcut reacts to the original shortcut action from the user @@ -138,15 +121,6 @@ func (r *modalRouter) viewForShortcut(callback *slack.InteractionCallback, logge return r.openModal(id, callback.TriggerID, logger) } -func (r *modalRouter) viewForApplicationStep(callback *slack.InteractionCallback, logger *logrus.Entry) error { - response, err := r.slackClient.OpenView(callback.TriggerID, stepsFromApp.WorkflowStepEditView(callback)) - if err != nil { - logger.WithError(err).Warn("Failed to open the workflow_step_edit view.") - } - logger.WithField("response", response).Trace("Received a workflow_step_edit request") - return err -} - // viewForButton reacts to the a user pressing a button in a bot message // to open the a modal view for them func (r *modalRouter) viewForButton(callback *slack.InteractionCallback, logger *logrus.Entry) error { diff --git a/pkg/slack/modals/handlers.go b/pkg/slack/modals/handlers.go index 436e3eaf4..0352a54cd 100644 --- a/pkg/slack/modals/handlers.go +++ b/pkg/slack/modals/handlers.go @@ -1,11 +1,8 @@ package modals import ( - "bytes" "encoding/json" "fmt" - "strings" - "text/template" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog" @@ -15,18 +12,12 @@ import ( "maps" - "github.com/openshift/ci-chat-bot/pkg/jira" "github.com/openshift/ci-chat-bot/pkg/manager" "github.com/openshift/ci-chat-bot/pkg/slack/interactions" ) const ( - // BlockIdTitle is the block identifier to use for inputs - // that should be used as the title of a Jira issue - BlockIdTitle = "title" - IdentifierJira Identifier = "jira" - IdentifierJiraPending Identifier = "jira_pending" - IdentifierError Identifier = "error" + IdentifierError Identifier = "error" ) // ViewUpdater is a subset of the Slack client @@ -55,139 +46,6 @@ func UpdateViewForButtonPress(identifier, buttonID string, updater ViewUpdater, }) } -// JiraIssueParameters holds the metadata used to create a Jira issue -type JiraIssueParameters struct { - Id Identifier - IssueType string - Template *template.Template - Fields []string -} - -// Process processes the interaction callback data to render the Jira issue title and body -func (p *JiraIssueParameters) Process(callback *slack.InteractionCallback) (string, string, error) { - data := valuesFor(callback, p.Fields...) - body := &bytes.Buffer{} - if err := p.Template.Execute(body, data); err != nil { - return "", "", fmt.Errorf("failed to render %s template: %w", p.Id, err) - } - return data[BlockIdTitle], body.String(), nil -} - -// ToJiraIssue responds to the user with a confirmation screen and files -// a Jira issue behind the scenes, updating the View once the operation -// has finished. We need this asynchronous response mechanism as the API -// calls needed to file the issue often take longer than the 3sec TTL on -// responding to the interaction payload we have. -func ToJiraIssue(parameters JiraIssueParameters, filer jira.IssueFiler, updater ViewUpdater) interactions.Handler { - return interactions.HandlerFunc(string(parameters.Id)+".jira", func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - logger.Infof("Submitting new %s to Jira.", parameters.Id) - if filer == nil { - // respond to the HTTP payload from Slack with a submission response - response, err := json.Marshal(&slack.ViewSubmissionResponse{ - ResponseAction: slack.RAUpdate, - View: NotEnabledView(), - }) - if err != nil { - logger.WithError(err).Error("Failed to marshal View update submission response.") - return nil, err - } - return response, nil - } - go func() { - overwriteView := func(view slack.ModalViewRequest) { - // don't pass a hash so we overwrite the View always - response, err := updater.UpdateView(view, "", "", callback.View.ID) - if err != nil { - logger.WithError(err).Warn("Failed to update a modal View.") - } - logger.WithField("response", response).Trace("Got a modal response.") - } - title, body, err := parameters.Process(callback) - if err != nil { - logger.WithError(err).Warnf("Failed to render %s template.", parameters.Id) - overwriteView(ErrorView(fmt.Sprintf("render %s template", parameters.Id), err)) - return - } - - issue, err := filer.FileIssue(parameters.IssueType, title, body, callback.User.ID, logger) - if err != nil { - logger.WithError(err).Errorf("Failed to create %s Jira.", parameters.Id) - overwriteView(ErrorView(fmt.Sprintf("create %s Jira issue", parameters.Id), err)) - return - } - overwriteView(JiraView(issue.Key)) - }() - - // respond to the HTTP payload from Slack with a submission response - response, err := json.Marshal(&slack.ViewSubmissionResponse{ - ResponseAction: slack.RAUpdate, - View: PendingJiraView(), - }) - if err != nil { - logger.WithError(err).Error("Failed to marshal View update submission response.") - return nil, err - } - return response, nil - }) -} - -// PendingJiraView is a placeholder modal View for the user -// to know we are working on publishing a Jira issue -func PendingJiraView() *slack.ModalViewRequest { - return &slack.ModalViewRequest{ - Type: slack.VTModal, - PrivateMetadata: string(IdentifierJiraPending), - Title: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Creating Jira Issue..."}, - Blocks: slack.Blocks{BlockSet: []slack.Block{ - &slack.SectionBlock{ - Type: slack.MBTSection, - Text: &slack.TextBlockObject{ - Type: slack.MarkdownType, - Text: "A Jira issue is being filed, please do not close this window...", - }, - }, - }}, - } -} - -// NotEnabledView is a placeholder modal View when a jira filer is not defined (disabled) -func NotEnabledView() *slack.ModalViewRequest { - return &slack.ModalViewRequest{ - Type: slack.VTModal, - PrivateMetadata: string(IdentifierJiraPending), - Title: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Creating Jira Issue..."}, - Blocks: slack.Blocks{BlockSet: []slack.Block{ - &slack.SectionBlock{ - Type: slack.MBTSection, - Text: &slack.TextBlockObject{ - Type: slack.MarkdownType, - Text: "This feature is not implemented. Please contact #forum-ocp-crt for more details", - }, - }, - }}, - } -} - -// JiraView is a modal View to show the user the -// Jira issue we just created for them -func JiraView(key string) slack.ModalViewRequest { - return slack.ModalViewRequest{ - Type: slack.VTModal, - PrivateMetadata: string(IdentifierJira), - Title: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Jira Issue Created"}, - Close: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "OK"}, - Blocks: slack.Blocks{BlockSet: []slack.Block{ - &slack.SectionBlock{ - Type: slack.MBTSection, - Text: &slack.TextBlockObject{ - Type: slack.MarkdownType, - Text: fmt.Sprintf("A Jira issue was filed: ", key, key), - }, - }, - }}, - } -} - func SubmitPrepare(title, modalName string, logger *logrus.Entry) ([]byte, error) { response, err := json.Marshal(&slack.ViewSubmissionResponse{ ResponseAction: slack.RAUpdate, @@ -265,52 +123,6 @@ func OverwriteView(client ViewUpdater, view slack.ModalViewRequest, callback *sl logger.WithField("response", response).Trace("Got a modal response.") } -// valuesFor extracts values identified by the block IDs and exposes them -// in a map by their IDs. We bias towards plain text inputs where the block -// Identifier that we set when creating the modal View can fully identify the data, -// and concatenate the block identifier with the input type in other scenarios -func valuesFor(callback *slack.InteractionCallback, blockIds ...string) map[string]string { - values := map[string]string{} - for _, id := range blockIds { - for _, action := range callback.View.State.Values[id] { - switch string(action.Type) { - case string(slack.METPlainTextInput): - // the most common input type is just a plain text, for which - // there will only ever be one action per block and for which - // we can identify this data with the block Identifier - values[id] = action.Value - // in all other cases - like selectors, etc - we can have more - // than one input block per block and need to identify each of - // them with a concatenation of the block Identifier and the type - case slack.OptTypeChannels: - values[fmt.Sprintf("%s_%s", id, slack.OptTypeChannels)] = action.SelectedChannel - case slack.OptTypeConversations: - values[fmt.Sprintf("%s_%s", id, slack.OptTypeConversations)] = action.SelectedConversation - case slack.OptTypeUser: - values[fmt.Sprintf("%s_%s", id, slack.OptTypeUser)] = action.SelectedUser - case slack.OptTypeStatic: - values[fmt.Sprintf("%s_%s", id, slack.OptTypeStatic)] = action.SelectedOption.Value - } - } - } - return values -} - -// BulletListFunc exposes a function to turn lines into a bullet list -func BulletListFunc() template.FuncMap { - return template.FuncMap{ - "toBulletList": func(input string) string { - var output []string - for line := range strings.SplitSeq(input, "\n") { - if trimmed := strings.TrimSpace(line); trimmed != "" { - output = append(output, fmt.Sprintf("* %s", trimmed)) - } - } - return strings.Join(output, "\n") - }, - } -} - func CallbackSelection(callback *slack.InteractionCallback) map[string]string { selectionValues := make(map[string]string) for key, value := range callback.View.State.Values { diff --git a/pkg/slack/modals/stepsFromApp/jira_step.go b/pkg/slack/modals/stepsFromApp/jira_step.go deleted file mode 100644 index 71cec6430..000000000 --- a/pkg/slack/modals/stepsFromApp/jira_step.go +++ /dev/null @@ -1,179 +0,0 @@ -package stepsFromApp - -import ( - "github.com/openshift/ci-chat-bot/pkg/slack/events/workflowSubmissionEvents" - "github.com/openshift/ci-chat-bot/pkg/slack/modals" - "github.com/slack-go/slack" -) - -const Identifier modals.Identifier = "workflow_step_edit" - -const ( - TicketTitle = "ticket_title" - ticketType = "ticket_type" - UserDetails = "user_details" -) - -func WorkflowStepEditView(callback *slack.InteractionCallback) slack.ModalViewRequest { - return slack.ModalViewRequest{ - Type: "workflow_step", - PrivateMetadata: string(Identifier), - CallbackID: callback.CallbackID, - Blocks: slack.Blocks{BlockSet: []slack.Block{ - // using a multi-select does not seem to work here (the selected option is always empty in the input, for - // some reason). Using a text-block instead, with a hint on currently supported options. If the input is not - // supported, it is detected and handled when handling the workflow_step_execute event - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: ticketType, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Supported ticket type (currently supported: bug, enhancement, consultation)"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: UserDetails, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Include the user details (the one who initiates the workflow)"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: TicketTitle, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Ticket Title (one-line summary)"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.DividerBlock{ - Type: slack.MBTDivider, - BlockID: "bug_divider", - }, - &slack.HeaderBlock{ - Type: slack.MBTHeader, - BlockID: "bug_divider_header", - Text: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "These are bug specific configurations:"}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.AffectedComponent, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Bugs: Affected component"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.OtherComponent, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Bugs: Other component if not listed"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.IncorrectBehaviour, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Bugs: Incorrect behaviour"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.ExpectedBehaviour, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Bugs: Expected behaviour"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.Impact, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Bugs: Impact"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.IsReproducible, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Bugs: Is it reproducible"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.DividerBlock{ - Type: slack.MBTDivider, - BlockID: "consultation_divider", - }, - &slack.HeaderBlock{ - Type: slack.MBTHeader, - BlockID: "consultation_divider_header", - Text: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "These are consultation specific configurations:"}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.BlockIdRequirement, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Consultation: Requirements"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.BlockIdPrevious, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Consultation: Previous"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.BlockIdAcceptanceCriteria, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Consultation: Acceptance Criteria"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.BlockIdAdditional, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Consultation: Additional"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.DividerBlock{ - Type: slack.MBTDivider, - BlockID: "enhancement_divider", - }, - &slack.HeaderBlock{ - Type: slack.MBTHeader, - BlockID: "enhancement_divider_header", - Text: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "These are enhancement specific configurations:"}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.BlockIdAsA, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Enhancement: As as..."}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.BlockIdIWant, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Enhancement: I want..."}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.BlockIdSoThat, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Enhancement: So that..."}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.BlockIdSummary, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Enhancement: Summary"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - &slack.InputBlock{ - Type: slack.MBTInput, - BlockID: workflowSubmissionEvents.BlockIdImplementation, - Optional: true, - Label: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Enhancement: Implementation"}, - Element: &slack.PlainTextInputBlockElement{Type: slack.METPlainTextInput}, - }, - }, - }, - } -} diff --git a/pkg/slack/modals/stepsFromApp/workflow_submit.go b/pkg/slack/modals/stepsFromApp/workflow_submit.go deleted file mode 100644 index 01bcd38c9..000000000 --- a/pkg/slack/modals/stepsFromApp/workflow_submit.go +++ /dev/null @@ -1,37 +0,0 @@ -package stepsFromApp - -import ( - "github.com/openshift/ci-chat-bot/pkg/slack/events/workflowSubmissionEvents" - "github.com/slack-go/slack" -) - -func StepFromAppSubmit(callback *slack.InteractionCallback) (workflowSubmissionEvents.WorkflowStepInputs, []workflowSubmissionEvents.WorkflowStepOutput) { - inputMap := make(workflowSubmissionEvents.WorkflowStepInputs) - outputMap := []workflowSubmissionEvents.WorkflowStepOutput{ - { - Name: "issue.key", - Type: "text", - Label: "Issue Key", - }, - { - Name: "issue.link", - Type: "text", - Label: "Issue Link", - }, - { - Name: "issue.summary", - Type: "text", - Label: "Issue Summary Field", - }, - } - for key, text := range callback.View.State.Values { - var input workflowSubmissionEvents.WorkflowStepInputElement - for _, subInput := range text { - // we expect a single value here. If the array has multiple arrays, the last element proceeds. - input.Value = subInput.Value - input.SkipVariableReplacement = false - } - inputMap[key] = input - } - return inputMap, outputMap -} From e8fa088f8df5ee3f6084832c40579f1865432a32 Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Thu, 6 Aug 2026 11:30:17 +0200 Subject: [PATCH 10/11] Update go.mod: go-jira is now an indirect dependency Co-Authored-By: Claude Opus 4.6 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 17514252f..6254b0cc6 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,6 @@ replace ( require ( cloud.google.com/go/bigquery v1.77.0 github.com/adrg/xdg v0.5.3 - github.com/andygrunwald/go-jira v1.17.0 github.com/blang/semver v3.5.1+incompatible github.com/onsi/ginkgo/v2 v2.27.1 github.com/onsi/gomega v1.38.2 @@ -81,6 +80,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/PaesslerAG/gval v1.0.0 // indirect github.com/PaesslerAG/jsonpath v0.1.1 // indirect + github.com/andygrunwald/go-jira v1.17.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect From 9791040fce97785227156db5c5c88785dbf9adeb Mon Sep 17 00:00:00 2001 From: thiagoalessio Date: Fri, 7 Aug 2026 15:48:01 +0200 Subject: [PATCH 11/11] Keep deprecated --jira-* flags for backward-compatible deployment The deployment config in openshift/release still passes --jira-endpoint, --jira-username, and --jira-password-file. Retain the flags as no-ops with a deprecation warning so this PR can merge without a coordinated deployment change. Co-Authored-By: Claude Opus 4.6 --- cmd/ci-chat-bot/main.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cmd/ci-chat-bot/main.go b/cmd/ci-chat-bot/main.go index 5e2ea4631..ff0fcedb2 100644 --- a/cmd/ci-chat-bot/main.go +++ b/cmd/ci-chat-bot/main.go @@ -146,6 +146,13 @@ func run() error { pflag.StringVar(&opt.rosaBillingAccount, "rosa-billingAccount-path", "", "Path to the Billing Account ID.") pflag.BoolVar(&opt.disableRosa, "disable-rosa", false, "Do not load the rosa client") + // Deprecated: Jira integration has been removed. These flags are kept + // for backward compatibility with existing deployment configurations. + var deprecatedJiraEndpoint, deprecatedJiraUsername, deprecatedJiraPasswordFile string + pflag.StringVar(&deprecatedJiraEndpoint, "jira-endpoint", "", "Deprecated: Jira integration has been removed") + pflag.StringVar(&deprecatedJiraUsername, "jira-username", "", "Deprecated: Jira integration has been removed") + pflag.StringVar(&deprecatedJiraPasswordFile, "jira-password-file", "", "Deprecated: Jira integration has been removed") + opt.prowconfig.AddFlags(emptyFlags) opt.GitHubOptions.AddFlags(emptyFlags) opt.KubernetesOptions.AddFlags(emptyFlags) @@ -153,6 +160,11 @@ func run() error { pflag.CommandLine.AddGoFlagSet(emptyFlags) pflag.Parse() klog.SetOutput(os.Stderr) + + if deprecatedJiraEndpoint != "" || deprecatedJiraUsername != "" || deprecatedJiraPasswordFile != "" { + klog.Warning("Jira flags (--jira-endpoint, --jira-username, --jira-password-file) are deprecated and will be removed in a future release. Jira integration has been removed.") + } + // let k8s know that we're alive health := pjutil.NewHealthOnPort(opt.InstrumentationOptions.HealthPort)