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
|
package slack
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/nicholas-fedor/shoutrrr/pkg/format"
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
"github.com/nicholas-fedor/shoutrrr/pkg/types"
"github.com/nicholas-fedor/shoutrrr/pkg/util/jsonclient"
)
// apiPostMessage is the Slack API endpoint for sending messages.
const (
apiPostMessage = "https://slack.com/api/chat.postMessage"
defaultHTTPTimeout = 10 * time.Second // defaultHTTPTimeout is the default timeout for HTTP requests.
)
// Service sends notifications to a pre-configured Slack channel or user.
type Service struct {
standard.Standard
Config *Config
pkr format.PropKeyResolver
client *http.Client
}
// Send delivers a notification message to Slack.
func (service *Service) Send(message string, params *types.Params) error {
config := service.Config
if err := service.pkr.UpdateConfigFromParams(config, params); err != nil {
return fmt.Errorf("updating config from params: %w", err)
}
payload := CreateJSONPayload(config, message)
var err error
if config.Token.IsAPIToken() {
err = service.sendAPI(config, payload)
} else {
err = service.sendWebhook(config, payload)
}
if err != nil {
return fmt.Errorf("failed to send slack notification: %w", err)
}
return nil
}
// Initialize configures the service with a URL and logger.
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
service.SetLogger(logger)
service.Config = &Config{}
service.pkr = format.NewPropKeyResolver(service.Config)
service.client = &http.Client{
Timeout: defaultHTTPTimeout,
}
return service.Config.setURL(&service.pkr, configURL)
}
// GetID returns the service identifier.
func (service *Service) GetID() string {
return Scheme
}
// sendAPI sends a notification using the Slack API.
func (service *Service) sendAPI(config *Config, payload any) error {
response := APIResponse{}
jsonClient := jsonclient.NewClient()
jsonClient.Headers().Set("Authorization", config.Token.Authorization())
if err := jsonClient.Post(apiPostMessage, payload, &response); err != nil {
return fmt.Errorf("posting to Slack API: %w", err)
}
if !response.Ok {
if response.Error != "" {
return fmt.Errorf("%w: %v", ErrAPIResponseFailure, response.Error)
}
return ErrUnknownAPIError
}
if response.Warning != "" {
service.Logf("Slack API warning: %q", response.Warning)
}
return nil
}
// sendWebhook sends a notification using a Slack webhook.
func (service *Service) sendWebhook(config *Config, payload any) error {
payloadBytes, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultHTTPTimeout)
defer cancel()
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
config.Token.WebhookURL(),
bytes.NewBuffer(payloadBytes),
)
if err != nil {
return fmt.Errorf("failed to create webhook request: %w", err)
}
req.Header.Set("Content-Type", jsonclient.ContentType)
res, err := service.client.Do(req)
if err != nil {
return fmt.Errorf("failed to invoke webhook: %w", err)
}
defer func() { _ = res.Body.Close() }()
resBytes, _ := io.ReadAll(res.Body)
response := string(resBytes)
switch response {
case "":
if res.StatusCode != http.StatusOK {
return fmt.Errorf("%w: %v", ErrWebhookStatusFailure, res.Status)
}
fallthrough
case "ok":
return nil
default:
return fmt.Errorf("%w: %v", ErrWebhookResponseFailure, response)
}
}
|