File: slack.go

package info (click to toggle)
golang-github-nicholas-fedor-shoutrrr 0.12.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,680 kB
  • sloc: sh: 74; makefile: 58
file content (143 lines) | stat: -rw-r--r-- 3,592 bytes parent folder | download
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)
	}
}