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 teams
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/nicholas-fedor/shoutrrr/pkg/format"
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
"github.com/nicholas-fedor/shoutrrr/pkg/types"
)
// MaxSummaryLength defines the maximum length for a notification summary.
const MaxSummaryLength = 20
// TruncatedSummaryLen defines the length for a truncated summary.
const TruncatedSummaryLen = 21
// Service sends notifications to Microsoft Teams.
type Service struct {
standard.Standard
Config *Config
pkr format.PropKeyResolver
}
// Send delivers a notification message to Microsoft Teams.
func (service *Service) Send(message string, params *types.Params) error {
config := service.Config
if err := service.pkr.UpdateConfigFromParams(config, params); err != nil {
service.Logf("Failed to update params: %v", err)
}
return service.doSend(config, message)
}
// 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)
return service.Config.SetURL(configURL)
}
// GetID returns the service identifier.
func (service *Service) GetID() string {
return Scheme
}
// GetConfigURLFromCustom converts a custom URL to a service URL.
func (service *Service) GetConfigURLFromCustom(customURL *url.URL) (*url.URL, error) {
webhookURLStr := strings.TrimPrefix(customURL.String(), "teams+")
tempURL, err := url.Parse(webhookURLStr)
if err != nil {
return nil, fmt.Errorf("parsing custom URL %q: %w", webhookURLStr, err)
}
webhookURL := &url.URL{
Scheme: tempURL.Scheme,
Host: tempURL.Host,
Path: tempURL.Path,
}
config, err := ConfigFromWebhookURL(*webhookURL)
if err != nil {
return nil, err
}
config.Color = ""
config.Title = ""
query := customURL.Query()
for key, vals := range query {
if vals[0] != "" {
switch key {
case "color":
config.Color = vals[0]
case "host":
config.Host = vals[0]
case "title":
config.Title = vals[0]
}
}
}
return config.GetURL(), nil
}
// doSend sends the notification to Teams using the configured webhook URL.
func (service *Service) doSend(config *Config, message string) error {
lines := strings.Split(message, "\n")
sections := make([]section, 0, len(lines))
for _, line := range lines {
sections = append(sections, section{Text: line})
}
summary := config.Title
if summary == "" && len(sections) > 0 {
summary = sections[0].Text
if len(summary) > MaxSummaryLength {
summary = summary[:TruncatedSummaryLen]
}
}
payload, err := json.Marshal(payload{
CardType: "MessageCard",
Context: "http://schema.org/extensions",
Markdown: true,
Title: config.Title,
ThemeColor: config.Color,
Summary: summary,
Sections: sections,
})
if err != nil {
return fmt.Errorf("marshaling payload to JSON: %w", err)
}
if config.Host == "" {
return ErrMissingHost
}
postURL := BuildWebhookURL(
config.Host,
config.Group,
config.Tenant,
config.AltID,
config.GroupOwner,
config.ExtraID,
)
// Validate URL before sending
if err := ValidateWebhookURL(postURL); err != nil {
return err
}
res, err := safePost(postURL, payload)
if err != nil {
return fmt.Errorf("%w: %s", ErrSendFailed, err.Error())
}
defer res.Body.Close() // Move defer after error check
if res.StatusCode != http.StatusOK {
return fmt.Errorf("%w: %s", ErrSendFailedStatus, res.Status)
}
return nil
}
// safePost performs an HTTP POST with a pre-validated URL.
// Validation is already done; this wrapper isolates the call.
//
//nolint:gosec,noctx // Ignoring G107: Potential HTTP request made with variable url
func safePost(url string, payload []byte) (*http.Response, error) {
res, err := http.Post(url, "application/json", bytes.NewBuffer(payload))
if err != nil {
return nil, fmt.Errorf("making HTTP POST request: %w", err)
}
return res, nil
}
|