Files
Dmitry VerkhoturovandUmputun 80c12a3f10 chore(deps): update Go modules
Bump Go dependencies in both backend/ and backend/_example/memory_store.

Notable updates:
- github.com/go-pkgz/lgr v0.12.1 -> v0.12.3
- github.com/klauspost/compress v1.18.2 -> v1.18.5
- github.com/PuerkitoBio/goquery v1.11.0 -> v1.12.0
- github.com/montanaflynn/stats v0.7.1 -> v0.9.0
- github.com/redis/go-redis/v9 v9.17.2 -> v9.18.0
- github.com/slack-go/slack v0.17.3 -> v0.21.1
- go.mongodb.org/mongo-driver v1.17.6 -> v1.17.9
- golang.org/x/crypto v0.48.0 -> v0.50.0
- golang.org/x/net v0.49.0 -> v0.53.0
- golang.org/x/image v0.36.0 -> v0.39.0
- golang.org/x/sys v0.41.0 -> v0.43.0
- golang.org/x/{oauth2,sync,text} minor bumps

Key markdown/sanitisation libs (bluemonday v1.0.27,
alecthomas/chroma/v2 v2.23.1, russross/blackfriday/v2 v2.1.0,
Depado/bfchroma/v2 v2.0.0) are already at the latest available
versions and were not bumped.

Verified the Chroma span-class allowlist regex in
backend/app/store/comment.go:128-131 is still fully in sync with
chroma/v2 types.go StandardTypes map (86 classes, byte-equal after
sorting). The inline comment references commit c263f6f which is
stale (Chroma is at v2 now), but the class list content is current.

Ran `go mod tidy` + `go mod vendor` + full race test suite on both
modules. All green. Added a reminder in CLAUDE.md that updating
backend/ Go modules also requires `go mod tidy` in
backend/_example/memory_store since the example module uses a
local replace directive and inherits indirect deps from the main
module.
2026-04-12 11:52:57 -05:00

351 lines
11 KiB
Go

package slack
import (
"context"
"encoding/json"
)
const (
VTModal ViewType = "modal"
VTHomeTab ViewType = "home"
)
type ViewType string
type ViewState struct {
Values map[string]map[string]BlockAction `json:"values"`
}
type View struct {
SlackResponse
ID string `json:"id"`
TeamID string `json:"team_id"`
Type ViewType `json:"type"`
Title *TextBlockObject `json:"title"`
Close *TextBlockObject `json:"close"`
Submit *TextBlockObject `json:"submit"`
Blocks Blocks `json:"blocks"`
PrivateMetadata string `json:"private_metadata"`
CallbackID string `json:"callback_id"`
State *ViewState `json:"state"`
Hash string `json:"hash"`
ClearOnClose bool `json:"clear_on_close"`
NotifyOnClose bool `json:"notify_on_close"`
RootViewID string `json:"root_view_id"`
PreviousViewID string `json:"previous_view_id"`
AppID string `json:"app_id"`
ExternalID string `json:"external_id"`
BotID string `json:"bot_id"`
AppInstalledTeamID string `json:"app_installed_team_id"`
}
type ViewSubmissionCallbackResponseURL struct {
BlockID string `json:"block_id"`
ActionID string `json:"action_id"`
ChannelID string `json:"channel_id"`
ResponseURL string `json:"response_url"`
}
type ViewSubmissionCallback struct {
Hash string `json:"hash"`
ResponseURLs []ViewSubmissionCallbackResponseURL `json:"response_urls,omitempty"`
}
type ViewClosedCallback struct {
IsCleared bool `json:"is_cleared"`
}
const (
RAClear ViewResponseAction = "clear"
RAUpdate ViewResponseAction = "update"
RAPush ViewResponseAction = "push"
RAErrors ViewResponseAction = "errors"
)
type ViewResponseAction string
type ViewSubmissionResponse struct {
ResponseAction ViewResponseAction `json:"response_action"`
View *ModalViewRequest `json:"view,omitempty"`
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,
View: view,
}
}
// 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,
View: view,
}
}
// 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,
Errors: errors,
}
}
type ModalViewRequest struct {
Type ViewType `json:"type"`
Title *TextBlockObject `json:"title,omitempty"`
Blocks Blocks `json:"blocks"`
Close *TextBlockObject `json:"close,omitempty"`
Submit *TextBlockObject `json:"submit,omitempty"`
PrivateMetadata string `json:"private_metadata,omitempty"`
CallbackID string `json:"callback_id,omitempty"`
ClearOnClose bool `json:"clear_on_close,omitempty"`
NotifyOnClose bool `json:"notify_on_close,omitempty"`
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
}
type HomeTabViewRequest struct {
Type ViewType `json:"type"`
Blocks Blocks `json:"blocks"`
PrivateMetadata string `json:"private_metadata,omitempty"`
CallbackID string `json:"callback_id,omitempty"`
ExternalID string `json:"external_id,omitempty"`
}
func (v *HomeTabViewRequest) ViewType() ViewType {
return v.Type
}
type openViewRequest struct {
TriggerID string `json:"trigger_id"`
View ModalViewRequest `json:"view"`
}
type pushViewRequest struct {
TriggerID string `json:"trigger_id"`
View ModalViewRequest `json:"view"`
}
type updateViewRequest struct {
View ModalViewRequest `json:"view"`
ExternalID string `json:"external_id,omitempty"`
Hash string `json:"hash,omitempty"`
ViewID string `json:"view_id,omitempty"`
}
type ViewResponse struct {
SlackResponse
View `json:"view"`
}
// OpenView opens a view for a user.
// For more information see the OpenViewContext documentation.
func (api *Client) OpenView(triggerID string, view ModalViewRequest) (*ViewResponse, error) {
return api.OpenViewContext(context.Background(), triggerID, view)
}
// ValidateUniqueBlockID will verify if each input block has a unique block ID if set
func ValidateUniqueBlockID(view ModalViewRequest) bool {
uniqueBlockID := map[string]bool{}
for _, b := range view.Blocks.BlockSet {
if inputBlock, ok := b.(*InputBlock); ok {
if inputBlock.BlockID == "" {
continue
}
if _, ok := uniqueBlockID[inputBlock.BlockID]; ok {
return false
}
uniqueBlockID[inputBlock.BlockID] = true
}
}
return true
}
// OpenViewContext opens a view for a user with a custom context.
// Slack API docs: https://docs.slack.dev/reference/methods/views.open
func (api *Client) OpenViewContext(
ctx context.Context,
triggerID string,
view ModalViewRequest,
) (*ViewResponse, error) {
if triggerID == "" {
return nil, ErrParametersMissing
}
if !ValidateUniqueBlockID(view) {
return nil, ErrBlockIDNotUnique
}
req := openViewRequest{
TriggerID: triggerID,
View: view,
}
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
resp := &ViewResponse{}
err = api.postJSONMethod(ctx, "views.open", api.token, encoded, resp)
if err != nil {
return nil, err
}
return resp, resp.Err()
}
// 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) {
var hashPtr *string
if hash != "" {
hashPtr = &hash
}
return api.PublishViewContext(context.Background(), PublishViewContextRequest{UserID: userID, View: view, Hash: hashPtr})
}
// PublishViewContext publishes a static view for a user with a custom context.
// Slack API docs: https://docs.slack.dev/reference/methods/views.publish
func (api *Client) PublishViewContext(
ctx context.Context,
req PublishViewContextRequest,
) (*ViewResponse, error) {
if req.UserID == "" {
return nil, ErrParametersMissing
}
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
resp := &ViewResponse{}
err = api.postJSONMethod(ctx, "views.publish", api.token, encoded, resp)
if err != nil {
return nil, err
}
return resp, resp.Err()
}
// PushView pushes a view onto the stack of a root view.
// For more information see the PushViewContext documentation.
func (api *Client) PushView(triggerID string, view ModalViewRequest) (*ViewResponse, error) {
return api.PushViewContext(context.Background(), triggerID, view)
}
// PushViewContext pushes a view onto the stack of a root view with a custom context.
// Slack API docs: https://docs.slack.dev/reference/methods/views.push
func (api *Client) PushViewContext(
ctx context.Context,
triggerID string,
view ModalViewRequest,
) (*ViewResponse, error) {
if triggerID == "" {
return nil, ErrParametersMissing
}
req := pushViewRequest{
TriggerID: triggerID,
View: view,
}
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
resp := &ViewResponse{}
err = api.postJSONMethod(ctx, "views.push", api.token, encoded, resp)
if err != nil {
return nil, err
}
return resp, resp.Err()
}
// UpdateView updates an existing view.
// For more information see the UpdateViewContext documentation.
func (api *Client) UpdateView(view ModalViewRequest, externalID, hash, viewID string) (*ViewResponse, error) {
return api.UpdateViewContext(context.Background(), view, externalID, hash, viewID)
}
// UpdateViewContext updates an existing view with a custom context.
// Slack API docs: https://docs.slack.dev/reference/methods/views.update
func (api *Client) UpdateViewContext(
ctx context.Context,
view ModalViewRequest,
externalID, hash,
viewID string,
) (*ViewResponse, error) {
if externalID == "" && viewID == "" {
return nil, ErrParametersMissing
}
req := updateViewRequest{
View: view,
ExternalID: externalID,
Hash: hash,
ViewID: viewID,
}
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
resp := &ViewResponse{}
err = api.postJSONMethod(ctx, "views.update", api.token, encoded, resp)
if err != nil {
return nil, err
}
return resp, resp.Err()
}