// Copyright (c) 2025-2026 Probo Inc . // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package connector import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" "go.probo.inc/probo/pkg/gid" ) type ( SlackConnection struct { OAuth2Connection Settings SlackSettings `json:"settings"` } SlackSettings struct { WebhookURL string `json:"webhook_url,omitempty"` // Encrypted Channel string `json:"channel,omitempty"` ChannelID string `json:"channel_id,omitempty"` } IncomingWebhook struct { URL string `json:"url"` Channel string `json:"channel"` ChannelID string `json:"channel_id"` } SlackTokenResponse struct { Ok bool `json:"ok"` Error string `json:"error,omitempty"` IncomingWebhook *IncomingWebhook `json:"incoming_webhook,omitempty"` } ) const ( SlackProvider = "SLACK" ) var _ Connection = (*SlackConnection)(nil) func (c *SlackConnection) Type() ProtocolType { return ProtocolOAuth2 } func (c *SlackConnection) Client(ctx context.Context) (*http.Client, error) { return c.OAuth2Connection.Client(ctx) } func (c SlackConnection) MarshalJSON() ([]byte, error) { return json.Marshal(&struct { Type string `json:"type"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token,omitempty"` ExpiresAt time.Time `json:"expires_at"` TokenType string `json:"token_type"` Scope string `json:"scope,omitempty"` WebhookURL string `json:"webhook_url,omitempty"` }{ Type: string(ProtocolOAuth2), AccessToken: c.AccessToken, RefreshToken: c.RefreshToken, ExpiresAt: c.ExpiresAt, TokenType: c.TokenType, Scope: c.Scope, WebhookURL: c.Settings.WebhookURL, }) } func (c *SlackConnection) UnmarshalJSON(data []byte) error { aux := &struct { Type string `json:"type"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token,omitempty"` ExpiresAt time.Time `json:"expires_at"` TokenType string `json:"token_type"` Scope string `json:"scope,omitempty"` WebhookURL string `json:"webhook_url,omitempty"` }{} if err := json.Unmarshal(data, aux); err != nil { return err } c.OAuth2Connection = OAuth2Connection{ AccessToken: aux.AccessToken, RefreshToken: aux.RefreshToken, ExpiresAt: aux.ExpiresAt, TokenType: aux.TokenType, Scope: aux.Scope, } c.Settings.WebhookURL = aux.WebhookURL return nil } func ParseSlackTokenResponse(body []byte, oauth2Conn OAuth2Connection, organizationID gid.GID) (*SlackConnection, *gid.GID, error) { var slackResponse SlackTokenResponse if err := json.NewDecoder(bytes.NewReader(body)).Decode(&slackResponse); err != nil { return nil, nil, fmt.Errorf("cannot decode Slack token response: %w", err) } if slackResponse.Error != "" { return nil, nil, fmt.Errorf("cannot complete Slack OAuth2 flow: %s", slackResponse.Error) } if !slackResponse.Ok { return nil, nil, fmt.Errorf("cannot complete Slack OAuth2 flow: ok=false") } if oauth2Conn.AccessToken == "" { return nil, nil, fmt.Errorf("cannot complete Slack OAuth2 flow: missing access token") } settings := SlackSettings{} if slackResponse.IncomingWebhook != nil { settings.WebhookURL = slackResponse.IncomingWebhook.URL settings.Channel = slackResponse.IncomingWebhook.Channel settings.ChannelID = slackResponse.IncomingWebhook.ChannelID } return &SlackConnection{ OAuth2Connection: oauth2Conn, Settings: settings, }, &organizationID, nil }