nikoksr/notify

View on GitHub
service/bark/bark.go

Summary

Maintainability
A
45 mins
Test Coverage
package bark

import (
    "bytes"
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "strings"
    "time"
)

// Service allow you to configure Bark service.
type Service struct {
    deviceKey  string
    client     *http.Client
    serverURLs []string
}

func defaultHTTPClient() *http.Client {
    return &http.Client{
        Timeout: 5 * time.Second, //nolint: mnd // 5 seconds is a reasonable timeout for a push notification
    }
}

// DefaultServerURL is the default server to use for the bark service.
const DefaultServerURL = "https://api.day.app/"

// normalizeServerURL normalizes the server URL. It prefixes it with https:// if it's not already and appends a slash
// if it's not already there. If the serverURL is empty, the DefaultServerURL is used. We're not validating the url here
// on purpose, we leave that to the http client.
func normalizeServerURL(serverURL string) string {
    if serverURL == "" {
        return DefaultServerURL
    }

    // Normalize the url
    if !strings.HasPrefix(serverURL, "http") {
        serverURL = "https://" + serverURL
    }
    if !strings.HasSuffix(serverURL, "/") {
        serverURL += "/"
    }

    return serverURL
}

// AddReceivers adds server URLs to the list of servers to use for sending messages. We call it Receivers and not
// servers because strictly speaking, the server is still receiving the message, and additionally we're following the
// naming convention of the other services.
func (s *Service) AddReceivers(serverURLs ...string) {
    for _, serverURL := range serverURLs {
        serverURL = normalizeServerURL(serverURL)
        s.serverURLs = append(s.serverURLs, serverURL)
    }
}

// NewWithServers returns a new instance of Bark service. You can use this service to send messages to bark. You can
// specify the servers to send the messages to. By default, the service will use the default server
// (https://api.day.app/) if you don't specify any servers.
func NewWithServers(deviceKey string, serverURLs ...string) *Service {
    s := &Service{
        deviceKey:  deviceKey,
        client:     defaultHTTPClient(),
        serverURLs: make([]string, 0),
    }

    if len(serverURLs) == 0 {
        serverURLs = append(serverURLs, DefaultServerURL)
    }

    // Calling service.AddReceivers() instead of directly setting the serverURLs because we want to normalize the URLs.
    s.AddReceivers(serverURLs...)

    return s
}

// New returns a new instance of Bark service. You can use this service to send messages to bark. By default, the
// service will use the default server (https://api.day.app/).
func New(deviceKey string) *Service {
    return NewWithServers(deviceKey)
}

// postData is the data to send to the bark server.
type postData struct {
    DeviceKey string `json:"device_key"`
    Title     string `json:"title"`
    Body      string `json:"body,omitempty"`
    Badge     int    `json:"badge,omitempty"`
    Sound     string `json:"sound,omitempty"`
    Icon      string `json:"icon,omitempty"`
    Group     string `json:"group,omitempty"`
    URL       string `json:"pushURL,omitempty"`
}

func (s *Service) send(ctx context.Context, serverURL, subject, content string) error {
    if serverURL == "" {
        return errors.New("server url is empty")
    }

    // Marshal the message to post
    message := &postData{
        DeviceKey: s.deviceKey,
        Title:     subject,
        Body:      content,
        Sound:     "alarm.caf",
    }

    messageJSON, err := json.Marshal(message)
    if err != nil {
        return fmt.Errorf("marshal message: %w", err)
    }

    pushURL := serverURL + "push"

    // Create new request
    req, err := http.NewRequestWithContext(ctx, http.MethodPost, pushURL, bytes.NewBuffer(messageJSON))
    if err != nil {
        return fmt.Errorf("create request: %w", err)
    }

    req.Header.Set("Content-Type", "application/json; charset=utf-8")

    // Send request
    resp, err := s.client.Do(req)
    if err != nil {
        return fmt.Errorf("send request: %w", err)
    }
    defer func() { _ = resp.Body.Close() }()

    // Read response and verify success
    result, err := io.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("read response: %w", err)
    }

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("bark returned status code %d: %s", resp.StatusCode, string(result))
    }

    return nil
}

// Send takes a message subject and a message content and sends them to bark application.
func (s *Service) Send(ctx context.Context, subject, content string) error {
    if s.client == nil {
        return errors.New("client is nil")
    }

    for _, serverURL := range s.serverURLs {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            err := s.send(ctx, serverURL, subject, content)
            if err != nil {
                return fmt.Errorf("send message to bark server %q: %w", serverURL, err)
            }
        }
    }

    return nil
}