164 lines
4 KiB
Go
164 lines
4 KiB
Go
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
|
|
}
|