-
Notifications
You must be signed in to change notification settings - Fork 2
/
interactive.go
158 lines (141 loc) · 4.96 KB
/
interactive.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package websockets
import (
"context"
"encoding/json"
"fmt"
"html"
"github.com/slack-go/slack/socketmode"
"go.uber.org/zap"
"go.autokitteh.dev/autokitteh/integrations/internal/extrazap"
"go.autokitteh.dev/autokitteh/integrations/slack/api"
"go.autokitteh.dev/autokitteh/integrations/slack/api/chat"
"go.autokitteh.dev/autokitteh/integrations/slack/webhooks"
"go.autokitteh.dev/autokitteh/internal/kittehs"
"go.autokitteh.dev/autokitteh/sdk/sdktypes"
)
// HandleInteraction dispatches and acknowledges a user interaction callback
// from Slack, e.g. shortcuts, interactive components in messages and modals,
// and Slack workflow steps. See https://api.slack.com/messaging/interactivity
// and https://api.slack.com/interactivity/handling. Compare this function
// with the [webhooks.HandleInteraction] implementation.
func (h handler) handleInteractiveEvent(e *socketmode.Event, c *socketmode.Client) {
defer c.Ack(*e.Request)
// Reuse the Slack event's JSON payload instead of the struct.
body, err := e.Request.Payload.MarshalJSON()
if err != nil {
h.logger.Error("Bad request from Slack websocket",
zap.Any("payload", e.Request.Payload),
)
return
}
// Parse the inbound request (no need to validate authenticity, unlike webhooks).
payload := &webhooks.BlockActionsPayload{}
if err := json.Unmarshal(body, payload); err != nil {
h.logger.Error("Failed to parse interactive event's JSON payload",
zap.ByteString("json", body),
zap.Error(err),
)
return
}
// Transform the received Slack event into an autokitteh event.
wrapped, err := sdktypes.DefaultValueWrapper.Wrap(payload)
if err != nil {
h.logger.Error("Failed to wrap Slack event",
zap.Any("payload", payload),
zap.Error(err),
)
return
}
m, err := wrapped.ToStringValuesMap()
if err != nil {
h.logger.Error("Failed to convert wrapped Slack event",
zap.Any("payload", payload),
zap.Error(err),
)
return
}
pb := kittehs.TransformMapValues(m, sdktypes.ToProto)
akEvent := &sdktypes.EventPB{
IntegrationId: h.integrationID.String(),
EventType: "interaction",
Data: pb,
}
// Retrieve all the relevant connections for this event.
connTokens, err := h.secrets.List(context.Background(), h.scope, "websockets")
if err != nil {
h.logger.Error("Failed to retrieve connection tokens", zap.Error(err))
return
}
// Dispatch the event to all of them, for asynchronous handling.
h.dispatchAsyncEventsToConnections(connTokens, akEvent)
// It's a Slack best practice to update an interactive message after the interaction,
// to prevent further interaction with the same message, and to reflect the user actions.
// See: https://api.slack.com/interactivity/handling#updating_message_response.
h.updateMessage(payload, connTokens)
}
// updateMessage updates an interactive message after the interaction, to prevent
// further interaction with the same message, and to reflect the user actions.
// See: https://api.slack.com/interactivity/handling#updating_message_response.
func (h handler) updateMessage(payload *webhooks.BlockActionsPayload, connTokens []string) {
resp := webhooks.Response{
Text: payload.Message.Text,
ResponseType: "in_channel",
ReplaceOriginal: true,
}
if payload.Container.IsEphemeral {
resp.ResponseType = "ephemeral"
}
// Copy all the message's blocks, except actions.
for _, b := range payload.Message.Blocks {
if b.Type == "header" {
b.Text.Text = html.UnescapeString(b.Text.Text)
}
if b.Type != "actions" {
resp.Blocks = append(resp.Blocks, b)
}
}
// And append new blocks to reflect the user actions.
for _, a := range payload.Actions {
if a.Type == "button" {
action := "<@%s> clicked the `%s` button"
action = fmt.Sprintf(action, payload.User.ID, payload.Actions[0].Text.Text)
switch payload.Actions[0].Style {
case "primary":
action = ":large_green_square: " + action
case "danger":
action = ":large_red_square: " + action
}
resp.Blocks = append(resp.Blocks, chat.Block{
Type: "section",
Text: &chat.Text{
Type: "mrkdwn",
Text: action,
},
})
}
}
// Send the update to Slack's webhook.
appToken := h.firstBotToken(connTokens)
meta := &chat.UpdateResponse{}
ctx := extrazap.AttachLoggerToContext(h.logger, context.Background())
ctx = context.WithValue(ctx, api.OAuthTokenContextKey{}, appToken)
err := api.PostJSON(ctx, h.secrets, h.scope, resp, meta, payload.ResponseURL)
if err != nil {
h.logger.Warn("Error in reply to user via interaction webhook",
zap.String("url", payload.ResponseURL),
zap.Any("response", resp),
zap.Error(err),
)
}
}
// Return the Slack bot token of the first connection, if there is any.
func (h handler) firstBotToken(connTokens []string) string {
for _, connToken := range connTokens {
if data, err := h.secrets.Get(context.Background(), h.scope, connToken); err == nil {
return data["bot_token"]
}
}
// This will result in a warning in the server's log,
// but the caller (updateMessage()) should still work.
return ""
}