-
Notifications
You must be signed in to change notification settings - Fork 2
/
slash_command.go
165 lines (143 loc) · 5.3 KB
/
slash_command.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
159
160
161
162
163
164
165
package webhooks
import (
"fmt"
"net/http"
"net/url"
"strconv"
"go.uber.org/zap"
"go.autokitteh.dev/autokitteh/internal/kittehs"
valuesv1 "go.autokitteh.dev/autokitteh/proto/gen/go/autokitteh/values/v1"
"go.autokitteh.dev/autokitteh/sdk/sdktypes"
"go.autokitteh.dev/autokitteh/integrations/slack/api"
)
const (
SlashCommandPath = "/slack/command"
)
// See https://api.slack.com/interactivity/slash-commands#app_command_handling
// and https://api.slack.com/types/event.
type SlashCommand struct {
// Unique identifier of the Slack workspace where the event occurred.
TeamID string
// Human-readable name of the Slack workspace where the event occurred.
TeamDomain string
// Is the executing Slack workspace part of an Enterprise Grid?
IsEnterpriseInstall bool
EnterpriseID string
EnterpriseName string
// APIAppID is our Slack app's unique ID. Useful in case we point multiple
// Slack apps to the same webhook URL, but want to treat them differently
// (e.g. official vs. unofficial, breaking changes, and flavors).
APIAppID string
ChannelID string
// Human-readable name of the channel - don't rely on it.
ChannelName string
// ID of the user who triggered the command.
// Use "<@value>" in messages to mention them.
UserID string
// Command must be "/ak" or "/autokitteh" in our Slack app.
Command string
// Text that the user typed after the command (e.g. "help").
Text string
// Short-lived webhook URL (https://api.slack.com/messaging/webhooks) to generate
// message responses (https://api.slack.com/interactivity/handling#message_responses).
// Compare with [api.BlockActionsInteractionPayload], where this field is deprecated
// per https://api.slack.com/reference/interaction-payloads/block-actions#fields.
ResponseURL string
// Short-lived ID that will let your app open a modal
// (https://api.slack.com/surfaces/modals).
TriggerID string
}
// HandleSlashCommand dispatches and acknowledges a user's slash command registered by our
// Slack app. See https://api.slack.com/interactivity/slash-commands#responding_to_commands.
// Compare this function with the [websockets.HandleSlashCommand] implementation.
func (h handler) HandleSlashCommand(w http.ResponseWriter, r *http.Request) {
l := h.logger.With(zap.String("urlPath", SlashCommandPath))
// Validate and parse the inbound request.
body := checkRequest(w, r, l, api.ContentTypeForm)
if body == nil {
return
}
kv, err := url.ParseQuery(string(body))
if err != nil {
l.Error("Failed to parse slash command's URL-encoded form",
zap.Error(err),
zap.ByteString("body", body),
)
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
// See https://api.slack.com/interactivity/slash-commands#app_command_handling
// (the informational note under the parameters table).
if kv.Get("ssl_check") != "" {
return
}
isEnterprise, err := strconv.ParseBool(kv.Get("is_enterprise_install"))
if err != nil {
isEnterprise = false
}
cmd := SlashCommand{
TeamID: kv.Get("team_id"),
TeamDomain: kv.Get("team_domain"),
IsEnterpriseInstall: isEnterprise,
EnterpriseID: kv.Get("enterprise_id"),
EnterpriseName: kv.Get("enterprise_name"),
APIAppID: kv.Get("api_app_id"),
ChannelID: kv.Get("channel_id"),
ChannelName: kv.Get("channel_name"),
UserID: kv.Get("user_id"),
Command: kv.Get("command"),
Text: kv.Get("text"),
ResponseURL: kv.Get("response_url"),
TriggerID: kv.Get("trigger_id"),
}
// Transform the received Slack event into an autokitteh event.
data, err := transformCommand(l, w, cmd)
if err != nil {
return
}
akEvent := &sdktypes.EventPB{
EventType: "slash_command",
Data: data,
}
// Retrieve all the relevant connections for this event.
cids, err := h.listConnectionIDs(r.Context(), cmd.APIAppID, cmd.EnterpriseID, cmd.TeamID)
if err != nil {
l.Error("Failed to retrieve connection tokens",
zap.Error(err),
)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Dispatch the event to all of them, for asynchronous handling.
h.dispatchAsyncEventsToConnections(l, cids, akEvent)
// https://api.slack.com/interactivity/slash-commands#responding_to_commands
// https://api.slack.com/interactivity/slash-commands#responding_response_url
// https://api.slack.com/interactivity/slash-commands#enabling-interactivity-with-slash-commands__best-practices
if len(cmd.Text) == 0 {
return
}
w.Header().Add(api.HeaderContentType, api.ContentTypeJSONCharsetUTF8)
fmt.Fprintf(w, "{\"response_type\": \"ephemeral\", \"text\": \"Your command: `%s`\"}", cmd.Text)
}
// transformCommand transforms a received Slack event into an autokitteh event.
func transformCommand(l *zap.Logger, w http.ResponseWriter, cmd SlashCommand) (map[string]*valuesv1.Value, error) {
wrapped, err := sdktypes.DefaultValueWrapper.Wrap(cmd)
if err != nil {
l.Error("Failed to wrap Slack event",
zap.Error(err),
zap.Any("cmd", cmd),
)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return nil, err
}
data, err := wrapped.ToStringValuesMap()
if err != nil {
l.Error("Failed to convert wrapped Slack event",
zap.Error(err),
zap.Any("cmd", cmd),
)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return nil, err
}
return kittehs.TransformMapValues(data, sdktypes.ToProto), nil
}