Adding upstream version 0.8.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
3b2c48b5e4
commit
c0c4addb85
285 changed files with 25880 additions and 0 deletions
237
pkg/services/lark/lark_service.go
Normal file
237
pkg/services/lark/lark_service.go
Normal file
|
@ -0,0 +1,237 @@
|
|||
package lark
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
// Constants for the Lark service configuration and limits.
|
||||
const (
|
||||
apiFormat = "https://%s/open-apis/bot/v2/hook/%s" // API endpoint format
|
||||
maxLength = 4096 // Maximum message length in bytes
|
||||
defaultTime = 30 * time.Second // Default HTTP client timeout
|
||||
)
|
||||
|
||||
const (
|
||||
larkHost = "open.larksuite.com"
|
||||
feishuHost = "open.feishu.cn"
|
||||
)
|
||||
|
||||
// Error variables for the Lark service.
|
||||
var (
|
||||
ErrInvalidHost = errors.New("invalid host, use 'open.larksuite.com' or 'open.feishu.cn'")
|
||||
ErrNoPath = errors.New(
|
||||
"no path, path like 'xxx' in 'https://open.larksuite.com/open-apis/bot/v2/hook/xxx'",
|
||||
)
|
||||
ErrLargeMessage = errors.New("message exceeds the max length")
|
||||
ErrMissingHost = errors.New("host is required but not specified in the configuration")
|
||||
ErrSendFailed = errors.New("failed to send notification to Lark")
|
||||
ErrInvalidSignature = errors.New("failed to generate valid signature")
|
||||
)
|
||||
|
||||
// httpClient is configured with a default timeout.
|
||||
var httpClient = &http.Client{Timeout: defaultTime}
|
||||
|
||||
// Service sends notifications to Lark.
|
||||
type Service struct {
|
||||
standard.Standard
|
||||
config *Config
|
||||
pkr format.PropKeyResolver
|
||||
}
|
||||
|
||||
// Send delivers a notification message to Lark.
|
||||
func (service *Service) Send(message string, params *types.Params) error {
|
||||
if len(message) > maxLength {
|
||||
return ErrLargeMessage
|
||||
}
|
||||
|
||||
config := *service.config
|
||||
if err := service.pkr.UpdateConfigFromParams(&config, params); err != nil {
|
||||
return fmt.Errorf("updating params: %w", err)
|
||||
}
|
||||
|
||||
if config.Host != larkHost && config.Host != feishuHost {
|
||||
return ErrInvalidHost
|
||||
}
|
||||
|
||||
if config.Path == "" {
|
||||
return ErrNoPath
|
||||
}
|
||||
|
||||
return service.doSend(config, message, params)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// doSend sends the notification to Lark using the configured API URL.
|
||||
func (service *Service) doSend(config Config, message string, params *types.Params) error {
|
||||
if config.Host == "" {
|
||||
return ErrMissingHost
|
||||
}
|
||||
|
||||
postURL := fmt.Sprintf(apiFormat, config.Host, config.Path)
|
||||
|
||||
payload, err := service.preparePayload(message, config, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.sendRequest(postURL, payload)
|
||||
}
|
||||
|
||||
// preparePayload constructs and marshals the request payload for the Lark API.
|
||||
func (service *Service) preparePayload(
|
||||
message string,
|
||||
config Config,
|
||||
params *types.Params,
|
||||
) ([]byte, error) {
|
||||
body := service.getRequestBody(message, config.Title, config.Secret, params)
|
||||
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling payload to JSON: %w", err)
|
||||
}
|
||||
|
||||
service.Logf("Lark Request Body: %s", string(data))
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// sendRequest performs the HTTP POST request to the Lark API and handles the response.
|
||||
func (service *Service) sendRequest(postURL string, payload []byte) error {
|
||||
req, err := http.NewRequestWithContext(
|
||||
context.Background(),
|
||||
http.MethodPost,
|
||||
postURL,
|
||||
bytes.NewReader(payload),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating HTTP request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: making HTTP request: %w", ErrSendFailed, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return service.handleResponse(resp)
|
||||
}
|
||||
|
||||
// handleResponse processes the API response and checks for errors.
|
||||
func (service *Service) handleResponse(resp *http.Response) error {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%w: unexpected status %s", ErrSendFailed, resp.Status)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
var response Response
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return fmt.Errorf("unmarshaling response: %w", err)
|
||||
}
|
||||
|
||||
if response.Code != 0 {
|
||||
return fmt.Errorf(
|
||||
"%w: server returned code %d: %s",
|
||||
ErrSendFailed,
|
||||
response.Code,
|
||||
response.Msg,
|
||||
)
|
||||
}
|
||||
|
||||
service.Logf(
|
||||
"Notification sent successfully to %s/%s",
|
||||
service.config.Host,
|
||||
service.config.Path,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// genSign generates a signature for the request using the secret and timestamp.
|
||||
func (service *Service) genSign(secret string, timestamp int64) (string, error) {
|
||||
stringToSign := fmt.Sprintf("%v\n%s", timestamp, secret)
|
||||
|
||||
h := hmac.New(sha256.New, []byte(stringToSign))
|
||||
if _, err := h.Write([]byte{}); err != nil {
|
||||
return "", fmt.Errorf("%w: computing HMAC: %w", ErrInvalidSignature, err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// getRequestBody constructs the request body for the Lark API, supporting rich content via params.
|
||||
func (service *Service) getRequestBody(
|
||||
message, title, secret string,
|
||||
params *types.Params,
|
||||
) *RequestBody {
|
||||
body := &RequestBody{}
|
||||
|
||||
if secret != "" {
|
||||
ts := time.Now().Unix()
|
||||
body.Timestamp = strconv.FormatInt(ts, 10)
|
||||
|
||||
sign, err := service.genSign(secret, ts)
|
||||
if err != nil {
|
||||
sign = "" // Fallback to empty string on error
|
||||
}
|
||||
|
||||
body.Sign = sign
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
body.MsgType = MsgTypeText
|
||||
body.Content.Text = message
|
||||
} else {
|
||||
body.MsgType = MsgTypePost
|
||||
content := [][]Item{{{Tag: TagValueText, Text: message}}}
|
||||
|
||||
if params != nil {
|
||||
if link, ok := (*params)["link"]; ok && link != "" {
|
||||
content = append(content, []Item{{Tag: TagValueLink, Text: "More Info", Link: link}})
|
||||
}
|
||||
}
|
||||
|
||||
body.Content.Post = &Post{
|
||||
En: &Message{
|
||||
Title: title,
|
||||
Content: content,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue