prism/service/server/handlers_ntfy.go

131 lines
2.8 KiB
Go

package server
import (
"bytes"
"encoding/json"
"io"
"mime"
"net/http"
"net/url"
"strings"
"time"
"prism/service/delivery"
"prism/service/util"
"github.com/go-chi/chi/v5"
)
func (s *Server) handleNtfyPublish(w http.ResponseWriter, r *http.Request) {
appName := chi.URLParam(r, "appName")
decodedAppName, err := url.PathUnescape(appName)
if err != nil {
util.JSONError(w, "Invalid app name", http.StatusBadRequest)
return
}
appName = decodedAppName
if appName == "" || strings.Contains(appName, "/") {
util.JSONError(w, "Invalid app name", http.StatusBadRequest)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
util.JSONError(w, "Failed to read body", http.StatusBadRequest)
return
}
message, title := parseNtfyPayload(r, body)
if message == "" {
util.JSONError(w, "Message required", http.StatusBadRequest)
return
}
if title == appName {
title = ""
}
notif := delivery.Notification{
Title: title,
Message: message,
}
if err := s.publisher.Publish(appName, notif); err != nil {
util.LogAndError(w, s.logger, "Failed to send notification", http.StatusInternalServerError, err, "app", appName)
return
}
s.logger.Debug("Sent ntfy message", "app", appName, "title", title, "preview", truncate(message, 50))
now := time.Now()
w.Header().Set("Content-Type", "application/json")
response := map[string]any{
"id": now.UnixNano(),
"time": now.Unix(),
"event": "message",
"topic": appName,
"message": message,
}
if err := json.NewEncoder(w).Encode(response); err != nil {
s.logger.Error("Failed to encode response", "error", err)
}
}
func parseNtfyPayload(r *http.Request, body []byte) (string, string) {
var message, title string
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
mediaType = ""
}
switch mediaType {
case "application/json":
var payload struct {
Title string `json:"title"`
Message string `json:"message"`
}
if err := json.Unmarshal(body, &payload); err == nil {
message = payload.Message
title = payload.Title
} else {
message = string(body)
}
case "application/x-www-form-urlencoded":
r.Body = io.NopCloser(bytes.NewReader(body))
if err := r.ParseForm(); err == nil {
message = r.PostForm.Get("message")
if message == "" {
message = string(body)
}
title = firstNonEmpty(r.PostForm.Get("title"), r.PostForm.Get("t"))
} else {
message = string(body)
}
default:
message = string(body)
}
if title == "" {
title = firstNonEmpty(r.Header.Get("X-Title"), r.Header.Get("Title"), r.Header.Get("t"))
}
return message, title
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
func truncate(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max]
}