From cdc5e5d9eb19ec2550f99b38ed10c85375531438 Mon Sep 17 00:00:00 2001 From: Bastien Riviere Date: Fri, 1 Sep 2023 19:06:04 +0200 Subject: [PATCH] feat: add bridge implementation --- bridge/bridge.go | 147 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 bridge/bridge.go diff --git a/bridge/bridge.go b/bridge/bridge.go new file mode 100644 index 0000000..68d1e7a --- /dev/null +++ b/bridge/bridge.go @@ -0,0 +1,147 @@ +package bridge + +import ( + "encoding/base64" + "encoding/json" + "errors" + "io" + "log/slog" + "net/http" + "strconv" + "strings" +) + +type Handler interface { + FormatNotification(r io.Reader) (Notification, error) +} + +type NotificationError struct { + Code int `json:"code"` + Error string `json:"error"` +} + +type Notification struct { + Title string + Body string + Priority int + Tags []string + IsMarkdown bool + + topic string + auth Auth +} + +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()) + } + + 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 + } + + not, err := b.h.FormatNotification(r.Body) + defer r.Body.Close() + + if err != nil { + slog.Error("failed to format notification") + w.WriteHeader(http.StatusBadRequest) + return + } + + 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("notification sent with success") + + w.WriteHeader(http.StatusOK) +}