ntfy-bridge/bridge/bridge.go
Bastien Riviere dd38badc6e
All checks were successful
Go / build (push) Successful in 36s
style: run formatter
2023-09-11 20:01:26 +02:00

192 lines
3.5 KiB
Go

package bridge
import (
"encoding/base64"
"encoding/json"
"errors"
"log/slog"
"net/http"
"strconv"
"strings"
)
type Handler interface {
ProduceNotifications(r *http.Request) ([]Notification, error)
}
type NotificationError struct {
Code int `json:"code"`
Error string `json:"error"`
}
type NotificationAction struct {
Action string
Label string
Params []string
}
func NewViewAction(label, url string, params ...string) NotificationAction {
return NotificationAction{
Action: "view",
Label: label,
Params: append([]string{url}, params...),
}
}
func (n NotificationAction) Format() string {
var sb strings.Builder
sb.WriteString(n.Action + ", " + n.Label)
if len(n.Params) > 0 {
sb.WriteString(", " + strings.Join(n.Params, ", "))
}
return sb.String()
}
type Notification struct {
Title string
Body string
Priority int
Tags []string
Actions []NotificationAction
IsMarkdown bool
topic string
auth Auth
}
func (n Notification) IsEmpty() bool {
return n.Title == "" && n.Body == ""
}
func (n Notification) Send(base string) error {
req, err := http.NewRequest("POST", base+"/"+n.topic, strings.NewReader(n.Body))
if err != nil {
return err
}
if n.IsMarkdown {
req.Header.Set("Content-Type", "text/markdown")
} else {
req.Header.Set("Content-Type", "text/plain")
}
if n.Title != "" {
req.Header.Set("Title", n.Title)
}
if n.Priority > 0 {
req.Header.Set("Priority", strconv.Itoa(n.Priority))
}
if len(n.Tags) > 0 {
req.Header.Set("Tags", strings.Join(n.Tags, ","))
}
if n.auth.Username != "" {
req.Header.Set("Authorization", n.auth.basic())
}
if n.auth.AccessToken != "" {
req.Header.Set("Authorization", n.auth.bearer())
}
if len(n.Actions) > 0 {
actions := make([]string, len(n.Actions))
for i, act := range n.Actions {
actions[i] = act.Format()
}
req.Header.Set("Actions", strings.Join(actions, "; "))
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
var e NotificationError
dec := json.NewDecoder(resp.Body)
dec.Decode(&e)
return errors.New(e.Error)
}
return nil
}
type Auth struct {
Username string
Password string
AccessToken string
}
func (a Auth) IsEmpty() bool {
return a.Username == "" && a.AccessToken == ""
}
func (a Auth) basic() string {
return "Basic " + base64.StdEncoding.EncodeToString([]byte(a.Username+":"+a.Password))
}
func (a Auth) bearer() string {
return "Bearer " + a.AccessToken
}
type Bridge struct {
baseURL string
topic string
h Handler
auth Auth
}
func NewBridge(baseURL, topic string, handler Handler) Bridge {
return Bridge{
baseURL: baseURL,
topic: topic,
h: handler,
}
}
func (b *Bridge) WithAuth(auth Auth) {
b.auth = auth
}
func (b Bridge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusNotFound)
return
}
nots, err := b.h.ProduceNotifications(r)
if err != nil {
slog.Error("failed to format notification")
w.WriteHeader(http.StatusBadRequest)
return
}
if len(nots) == 0 {
slog.Debug("no notification produced")
w.WriteHeader(http.StatusNoContent)
return
}
for _, not := range nots {
not.topic = b.topic
not.auth = b.auth
if err = not.Send(b.baseURL); err != nil {
slog.Error("unable to send notification", "error", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
slog.Debug("notifications sent with success", "sent", len(nots))
w.WriteHeader(http.StatusNoContent)
}