diff --git a/scnserver/api/handler/external.go b/scnserver/api/handler/external.go index aece95b..b0ba600 100644 --- a/scnserver/api/handler/external.go +++ b/scnserver/api/handler/external.go @@ -28,8 +28,10 @@ func NewExternalHandler(app *logic.Application) ExternalHandler { // UptimeKuma swaggerdoc // -// @Summary Send a new message -// @Description All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required +// @Summary Send a new message (uses uptime-kuma notification schema) +// @Description Set necessary parameter via query (key, channel etc.), title+message are build from uptime-kuma payload +// @Description You can specify different channels/priorities for [up] and [down] notifications +// // @Tags External // // @Param query_data query handler.UptimeKuma.query false " " @@ -136,3 +138,58 @@ func (h ExternalHandler) UptimeKuma(pctx ginext.PreContext) ginext.HTTPResponse }) } + +// Shoutrrr swaggerdoc +// +// @Summary Send a new message (uses shoutrrr generic:// format=json schema) +// @Description Set necessary parameter via query (key, channel etc.), title+message are set via the shoutrrr payload +// @Description Use the shoutrrr format `generic://{{url}}?template=json` +// +// @Tags External +// +// @Param query_data query handler.Shoutrrr.query false " " +// @Param post_body body handler.Shoutrrr.body false " " +// +// @Success 200 {object} handler.Shoutrrr.response +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError "The user_key is wrong" +// @Failure 403 {object} ginresp.apiError "The user has exceeded its daily quota - wait 24 hours or upgrade your account" +// @Failure 500 {object} ginresp.apiError "An internal server error occurred - try again later" +// +// @Router /external/v1/uptime-kuma [POST] +func (h ExternalHandler) Shoutrrr(pctx ginext.PreContext) ginext.HTTPResponse { + type query struct { + KeyToken *string `form:"key" example:"P3TNH8mvv14fm"` + Channel *string `form:"channel"` + Priority *int `form:"priority"` + SenderName *string `form:"senderName"` + } + type body struct { + Title string `json:"title"` + Message string `json:"message"` + } + type response struct { + MessageID models.MessageID `json:"message_id"` + } + + var b body + var q query + ctx, g, errResp := pctx.Query(&q).Body(&b).Start() + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + + okResp, errResp := h.app.SendMessage(g, ctx, q.KeyToken, q.Channel, &b.Title, &b.Message, q.Priority, nil, nil, q.SenderName) + if errResp != nil { + return *errResp + } + + return finishSuccess(ginext.JSON(http.StatusOK, response{ + MessageID: okResp.Message.MessageID, + })) + + }) +} diff --git a/scnserver/api/router.go b/scnserver/api/router.go index 37161d5..32f58e6 100644 --- a/scnserver/api/router.go +++ b/scnserver/api/router.go @@ -182,6 +182,7 @@ func (r *Router) Init(e *ginext.GinWrapper) error { sendAPI.POST("/send.php").Handle(r.compatHandler.SendMessage) sendAPI.POST("/external/v1/uptime-kuma").Handle(r.externalHandler.UptimeKuma) + sendAPI.POST("/external/v1/shoutrrr").Handle(r.externalHandler.Shoutrrr) } diff --git a/scnserver/test/shoutrrr_test.go b/scnserver/test/shoutrrr_test.go new file mode 100644 index 0000000..410b0d7 --- /dev/null +++ b/scnserver/test/shoutrrr_test.go @@ -0,0 +1,127 @@ +package test + +import ( + "blackforestbytes.com/simplecloudnotifier/push" + tt "blackforestbytes.com/simplecloudnotifier/test/util" + "fmt" + "github.com/gin-gonic/gin" + "testing" +) + +func TestShoutrrrBasic(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitSingleData(t, ws) + + pusher := ws.Pusher.(*push.TestSink) + + suffix := fmt.Sprintf("/external/v1/shoutrrr?key=%v", data.SendKey) + _ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{ + "title": "Test Title", + "message": "Test Message Content", + }) + + tt.AssertEqual(t, "messageCount", 1, len(pusher.Data)) + tt.AssertStrRepEqual(t, "msg.title", "Test Title", pusher.Last().Message.Title) + tt.AssertStrRepEqual(t, "msg.content", "Test Message Content", pusher.Last().Message.Content) + + type mglist struct { + Messages []gin.H `json:"messages"` + } + + msgList1 := tt.RequestAuthGet[mglist](t, data.AdminKey, baseUrl, "/api/v2/messages") + tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages)) + tt.AssertStrRepEqual(t, "msg.title", "Test Title", msgList1.Messages[0]["title"]) + tt.AssertStrRepEqual(t, "msg.content", "Test Message Content", msgList1.Messages[0]["content"]) +} + +func TestShoutrrrChannelNone(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitSingleData(t, ws) + + pusher := ws.Pusher.(*push.TestSink) + + suffix := fmt.Sprintf("/external/v1/shoutrrr?key=%v", data.SendKey) + _ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{ + "title": "Test Title", + "message": "Test Message", + }) + + tt.AssertEqual(t, "messageCount", 1, len(pusher.Data)) + tt.AssertStrRepEqual(t, "msg.channel", "main", pusher.Last().Message.ChannelInternalName) +} + +func TestShoutrrrChannelCustom(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitSingleData(t, ws) + + pusher := ws.Pusher.(*push.TestSink) + + suffix := fmt.Sprintf("/external/v1/shoutrrr?key=%v&channel=CTEST", data.SendKey) + _ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{ + "title": "Test Title", + "message": "Test Message", + }) + + tt.AssertEqual(t, "messageCount", 1, len(pusher.Data)) + tt.AssertStrRepEqual(t, "msg.channel", "CTEST", pusher.Last().Message.ChannelInternalName) +} + +func TestShoutrrrPriorityNone(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitSingleData(t, ws) + + pusher := ws.Pusher.(*push.TestSink) + + suffix := fmt.Sprintf("/external/v1/shoutrrr?key=%v", data.SendKey) + _ = tt.RequestPost[gin.H](t, baseUrl, suffix, gin.H{ + "title": "Test Title", + "message": "Test Message", + }) + + tt.AssertEqual(t, "messageCount", 1, len(pusher.Data)) + tt.AssertStrRepEqual(t, "msg.priority", 1, pusher.Last().Message.Priority) +} + +func TestShoutrrrPrioritySingle(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitSingleData(t, ws) + + pusher := ws.Pusher.(*push.TestSink) + + suffix0 := fmt.Sprintf("/external/v1/shoutrrr?key=%v&priority=0", data.SendKey) + _ = tt.RequestPost[gin.H](t, baseUrl, suffix0, gin.H{ + "title": "Test Title", + "message": "Test Message", + }) + + tt.AssertEqual(t, "messageCount", 1, len(pusher.Data)) + tt.AssertStrRepEqual(t, "msg.prio", 0, pusher.Last().Message.Priority) + + suffix1 := fmt.Sprintf("/external/v1/shoutrrr?key=%v&priority=1", data.SendKey) + _ = tt.RequestPost[gin.H](t, baseUrl, suffix1, gin.H{ + "title": "Test Title", + "message": "Test Message", + }) + + tt.AssertEqual(t, "messageCount", 2, len(pusher.Data)) + tt.AssertStrRepEqual(t, "msg.prio", 1, pusher.Last().Message.Priority) + + suffix2 := fmt.Sprintf("/external/v1/shoutrrr?key=%v&priority=2", data.SendKey) + _ = tt.RequestPost[gin.H](t, baseUrl, suffix2, gin.H{ + "title": "Test Title", + "message": "Test Message", + }) + + tt.AssertEqual(t, "messageCount", 3, len(pusher.Data)) + tt.AssertStrRepEqual(t, "msg.prio", 2, pusher.Last().Message.Priority) +}