11package main
22
33import (
4+ "encoding/json"
45 "fmt"
56 "net/url"
67 "os"
@@ -21,7 +22,7 @@ type config struct {
2122 SlackBotScopes []string
2223 AckReaction string
2324 RemoveAckAfterReply bool
24- ChannelActionsToken string
25+ ChannelActionTokens [] channelActionToken
2526 PresetID string
2627 BackendBaseURL string
2728 BackendFastAPIBaseURL string
@@ -42,6 +43,16 @@ type config struct {
4243 InstallationPolicyCacheTTL time.Duration
4344}
4445
46+ type channelActionToken struct {
47+ Token string
48+ Target channelActionTarget
49+ }
50+
51+ type channelActionTarget struct {
52+ TeamID string
53+ ChannelID string
54+ }
55+
4556func loadConfig () (config , error ) {
4657 cfg := config {
4758 Addr : envOrDefault ("SPRITZ_SLACK_GATEWAY_ADDR" , ":8080" ),
@@ -56,7 +67,6 @@ func loadConfig() (config, error) {
5667 SlackBotScopes : splitCSV (envOrDefault ("SPRITZ_SLACK_BOT_SCOPES" , "app_mentions:read,channels:history,chat:write,im:history,mpim:history,reactions:write" )),
5768 AckReaction : normalizeSlackReactionName (envOrDefault ("SPRITZ_SLACK_ACK_REACTION" , "eyes" )),
5869 RemoveAckAfterReply : parseBoolEnv ("SPRITZ_SLACK_REMOVE_ACK_AFTER_REPLY" , true ),
59- ChannelActionsToken : strings .TrimSpace (os .Getenv ("SPRITZ_SLACK_CHANNEL_ACTIONS_TOKEN" )),
6070 PresetID : strings .TrimSpace (envOrDefault ("SPRITZ_SLACK_PRESET_ID" , defaultSlackPresetID )),
6171 BackendBaseURL : strings .TrimRight (strings .TrimSpace (os .Getenv ("SPRITZ_SLACK_BACKEND_BASE_URL" )), "/" ),
6272 BackendFastAPIBaseURL : strings .TrimRight (strings .TrimSpace (os .Getenv ("SPRITZ_SLACK_BACKEND_FASTAPI_BASE_URL" )), "/" ),
@@ -76,6 +86,15 @@ func loadConfig() (config, error) {
7686 PromptRetryTimeout : parseDurationEnv ("SPRITZ_SLACK_PROMPT_RETRY_TIMEOUT" , 8 * time .Second ),
7787 InstallationPolicyCacheTTL : parseDurationEnv ("SPRITZ_SLACK_INSTALLATION_POLICY_CACHE_TTL" , 10 * time .Second ),
7888 }
89+ channelActionTokens , err := parseChannelActionTokens (
90+ os .Getenv ("SPRITZ_SLACK_CHANNEL_ACTIONS_TOKEN" ),
91+ os .Getenv ("SPRITZ_SLACK_CHANNEL_ACTIONS_TARGETS" ),
92+ os .Getenv ("SPRITZ_SLACK_CHANNEL_ACTIONS_TOKEN_BINDINGS" ),
93+ )
94+ if err != nil {
95+ return config {}, err
96+ }
97+ cfg .ChannelActionTokens = channelActionTokens
7998
8099 if cfg .PublicURL == "" {
81100 return config {}, fmt .Errorf ("SPRITZ_SLACK_GATEWAY_PUBLIC_URL is required" )
@@ -130,6 +149,78 @@ func loadConfig() (config, error) {
130149 return cfg , nil
131150}
132151
152+ func parseChannelActionTokens (legacyToken , legacyTargets , bindingsRaw string ) ([]channelActionToken , error ) {
153+ tokens := make ([]channelActionToken , 0 )
154+ legacyToken = strings .TrimSpace (legacyToken )
155+ for _ , rawTarget := range splitCSV (legacyTargets ) {
156+ if legacyToken == "" {
157+ return nil , fmt .Errorf ("SPRITZ_SLACK_CHANNEL_ACTIONS_TARGETS requires SPRITZ_SLACK_CHANNEL_ACTIONS_TOKEN" )
158+ }
159+ target , err := parseChannelActionTarget (rawTarget )
160+ if err != nil {
161+ return nil , fmt .Errorf ("SPRITZ_SLACK_CHANNEL_ACTIONS_TARGETS is invalid: %w" , err )
162+ }
163+ tokens = append (tokens , channelActionToken {Token : legacyToken , Target : target })
164+ }
165+
166+ bindingsRaw = strings .TrimSpace (bindingsRaw )
167+ if bindingsRaw == "" {
168+ return tokens , nil
169+ }
170+ var bindings []struct {
171+ Token string `json:"token"`
172+ Targets []struct {
173+ TeamID string `json:"teamId"`
174+ ChannelID string `json:"channelId"`
175+ } `json:"targets"`
176+ }
177+ if err := json .Unmarshal ([]byte (bindingsRaw ), & bindings ); err != nil {
178+ return nil , fmt .Errorf ("SPRITZ_SLACK_CHANNEL_ACTIONS_TOKEN_BINDINGS is invalid JSON: %w" , err )
179+ }
180+ for index , binding := range bindings {
181+ token := strings .TrimSpace (binding .Token )
182+ if token == "" {
183+ return nil , fmt .Errorf ("SPRITZ_SLACK_CHANNEL_ACTIONS_TOKEN_BINDINGS[%d].token is required" , index )
184+ }
185+ if len (binding .Targets ) == 0 {
186+ return nil , fmt .Errorf ("SPRITZ_SLACK_CHANNEL_ACTIONS_TOKEN_BINDINGS[%d].targets is required" , index )
187+ }
188+ for targetIndex , rawTarget := range binding .Targets {
189+ target := channelActionTarget {
190+ TeamID : strings .TrimSpace (rawTarget .TeamID ),
191+ ChannelID : strings .TrimSpace (rawTarget .ChannelID ),
192+ }
193+ if target .TeamID == "" || target .ChannelID == "" {
194+ return nil , fmt .Errorf ("SPRITZ_SLACK_CHANNEL_ACTIONS_TOKEN_BINDINGS[%d].targets[%d] is invalid" , index , targetIndex )
195+ }
196+ tokens = append (tokens , channelActionToken {Token : token , Target : target })
197+ }
198+ }
199+ return tokens , nil
200+ }
201+
202+ func parseChannelActionTarget (raw string ) (channelActionTarget , error ) {
203+ raw = strings .TrimSpace (raw )
204+ if raw == "" {
205+ return channelActionTarget {}, fmt .Errorf ("target is empty" )
206+ }
207+ separators := []string {":" , "/" }
208+ for _ , separator := range separators {
209+ teamID , channelID , ok := strings .Cut (raw , separator )
210+ if ok {
211+ target := channelActionTarget {
212+ TeamID : strings .TrimSpace (teamID ),
213+ ChannelID : strings .TrimSpace (channelID ),
214+ }
215+ if target .TeamID == "" || target .ChannelID == "" {
216+ return channelActionTarget {}, fmt .Errorf ("target %q must be TEAM%sCHANNEL" , raw , separator )
217+ }
218+ return target , nil
219+ }
220+ }
221+ return channelActionTarget {}, fmt .Errorf ("target %q must be TEAM:CHANNEL or TEAM/CHANNEL" , raw )
222+ }
223+
133224func defaultReactBaseURL (publicURL string , spritzBaseURL string ) string {
134225 spritzBaseURL = strings .TrimRight (strings .TrimSpace (spritzBaseURL ), "/" )
135226 if ! isPrivateServiceBaseURL (spritzBaseURL ) {
0 commit comments