Implement /shoutrrr endpoint
All checks were successful
Build Docker and Deploy / Build Docker Container (push) Successful in 1m20s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m51s
Build Docker and Deploy / Deploy to Server (push) Successful in 27s

This commit is contained in:
2025-12-18 11:30:18 +01:00
parent 202603d16c
commit 55a91956ce
3 changed files with 187 additions and 2 deletions

View File

@@ -28,8 +28,10 @@ func NewExternalHandler(app *logic.Application) ExternalHandler {
// UptimeKuma swaggerdoc // UptimeKuma swaggerdoc
// //
// @Summary Send a new message // @Summary Send a new message (uses uptime-kuma notification schema)
// @Description All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required // @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 // @Tags External
// //
// @Param query_data query handler.UptimeKuma.query false " " // @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,
}))
})
}

View File

@@ -182,6 +182,7 @@ func (r *Router) Init(e *ginext.GinWrapper) error {
sendAPI.POST("/send.php").Handle(r.compatHandler.SendMessage) sendAPI.POST("/send.php").Handle(r.compatHandler.SendMessage)
sendAPI.POST("/external/v1/uptime-kuma").Handle(r.externalHandler.UptimeKuma) sendAPI.POST("/external/v1/uptime-kuma").Handle(r.externalHandler.UptimeKuma)
sendAPI.POST("/external/v1/shoutrrr").Handle(r.externalHandler.Shoutrrr)
} }

View File

@@ -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)
}