129 lines
3.3 KiB
Go
129 lines
3.3 KiB
Go
package zulip
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
|
|
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
|
)
|
|
|
|
// contentMaxSize defines the maximum allowed message size in bytes.
|
|
const (
|
|
contentMaxSize = 10000 // bytes
|
|
topicMaxLength = 60 // characters
|
|
)
|
|
|
|
// ErrTopicTooLong indicates the topic exceeds the maximum allowed length.
|
|
var (
|
|
ErrTopicTooLong = errors.New("topic exceeds max length")
|
|
ErrMessageTooLong = errors.New("message exceeds max size")
|
|
ErrResponseStatusFailure = errors.New("response status code unexpected")
|
|
ErrInvalidHost = errors.New("invalid host format")
|
|
)
|
|
|
|
// hostValidator ensures the host is a valid hostname or domain.
|
|
var hostValidator = regexp.MustCompile(
|
|
`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`,
|
|
)
|
|
|
|
// Service sends notifications to a pre-configured Zulip channel or user.
|
|
type Service struct {
|
|
standard.Standard
|
|
Config *Config
|
|
}
|
|
|
|
// Send delivers a notification message to Zulip.
|
|
func (service *Service) Send(message string, params *types.Params) error {
|
|
// Clone the config to avoid modifying the original for this send operation.
|
|
config := service.Config.Clone()
|
|
|
|
if params != nil {
|
|
if stream, found := (*params)["stream"]; found {
|
|
config.Stream = stream
|
|
}
|
|
|
|
if topic, found := (*params)["topic"]; found {
|
|
config.Topic = topic
|
|
}
|
|
}
|
|
|
|
topicLength := len([]rune(config.Topic))
|
|
if topicLength > topicMaxLength {
|
|
return fmt.Errorf("%w: %d characters, got %d", ErrTopicTooLong, topicMaxLength, topicLength)
|
|
}
|
|
|
|
messageSize := len(message)
|
|
if messageSize > contentMaxSize {
|
|
return fmt.Errorf(
|
|
"%w: %d bytes, got %d bytes",
|
|
ErrMessageTooLong,
|
|
contentMaxSize,
|
|
messageSize,
|
|
)
|
|
}
|
|
|
|
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{}
|
|
|
|
if err := service.Config.setURL(nil, configURL); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetID returns the identifier for this service.
|
|
func (service *Service) GetID() string {
|
|
return Scheme
|
|
}
|
|
|
|
// doSend sends the notification to Zulip using the configured API URL.
|
|
//
|
|
//nolint:gosec,noctx // Ignoring G107: Potential HTTP request made with variable url
|
|
func (service *Service) doSend(config *Config, message string) error {
|
|
apiURL := service.getAPIURL(config)
|
|
|
|
// Validate the host to mitigate SSRF risks
|
|
if !hostValidator.MatchString(config.Host) {
|
|
return fmt.Errorf("%w: %q", ErrInvalidHost, config.Host)
|
|
}
|
|
|
|
payload := CreatePayload(config, message)
|
|
|
|
res, err := http.Post(
|
|
apiURL,
|
|
"application/x-www-form-urlencoded",
|
|
strings.NewReader(payload.Encode()),
|
|
)
|
|
if err == nil && res.StatusCode != http.StatusOK {
|
|
err = fmt.Errorf("%w: %s", ErrResponseStatusFailure, res.Status)
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send zulip message: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getAPIURL constructs the API URL for Zulip based on the Config.
|
|
func (service *Service) getAPIURL(config *Config) string {
|
|
return (&url.URL{
|
|
User: url.UserPassword(config.BotMail, config.BotKey),
|
|
Host: config.Host,
|
|
Path: "api/v1/messages",
|
|
Scheme: "https",
|
|
}).String()
|
|
}
|