replace PHP in html with js & bugfixes

This commit is contained in:
Mike Schwörer 2022-11-20 03:06:08 +01:00
parent 728b12107f
commit 0d3526221d
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
22 changed files with 634 additions and 233 deletions

View File

@ -4,13 +4,13 @@
- background job for re-delivery - background job for re-delivery
- accept/decline subscriptions (PATCH subs) - POST::/messages
- (message.go) api routes
- (compat.go) api routes
- https://firebase.google.com/docs/cloud-messaging/send-message#rest - https://firebase.google.com/docs/cloud-messaging/send-message#rest
- List subscriptions on owned channels /RESTful?) - List subscriptions on all owned channels (RESTful?)
- deploy - deploy
- Dockerfile - Dockerfile
- php in html - php in html
- full-text-search: https://www.sqlite.org/fts5.html#contentless_tables - full-text-search: https://www.sqlite.org/fts5.html#contentless_tables
- route to re-create keys - route to re-create keys
- ack/read deliveries && return ack-count
- compat methods should bind body as form-data

View File

@ -40,16 +40,16 @@ func NewAPIHandler(app *logic.Application) APIHandler {
// @Router /api-v2/users/ [POST] // @Router /api-v2/users/ [POST]
func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
type body struct { type body struct {
FCMToken string `json:"fcm_token"` FCMToken string `json:"fcm_token" binding:"required"`
ProToken *string `json:"pro_token"` ProToken *string `json:"pro_token"`
Username *string `json:"username"` Username *string `json:"username"`
AgentModel string `json:"agent_model"` AgentModel string `json:"agent_model" binding:"required"`
AgentVersion string `json:"agent_version"` AgentVersion string `json:"agent_version" binding:"required"`
ClientType string `json:"client_type"` ClientType string `json:"client_type" binding:"required"`
} }
var b body var b body
ctx, errResp := h.app.StartRequest(g, nil, nil, &b) ctx, errResp := h.app.StartRequest(g, nil, nil, &b, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -129,7 +129,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -176,7 +176,7 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b) ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -250,7 +250,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -292,7 +292,7 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -334,15 +334,15 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
UserID int64 `uri:"uid"` UserID int64 `uri:"uid"`
} }
type body struct { type body struct {
FCMToken string `json:"fcm_token"` FCMToken string `json:"fcm_token" binding:"required"`
AgentModel string `json:"agent_model"` AgentModel string `json:"agent_model" binding:"required"`
AgentVersion string `json:"agent_version"` AgentVersion string `json:"agent_version" binding:"required"`
ClientType string `json:"client_type"` ClientType string `json:"client_type" binding:"required"`
} }
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b) ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -391,7 +391,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -440,7 +440,7 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -482,7 +482,7 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -540,7 +540,7 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var q query var q query
ctx, errResp := h.app.StartRequest(g, &u, &q, nil) ctx, errResp := h.app.StartRequest(g, &u, &q, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -620,7 +620,7 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -665,7 +665,7 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -715,7 +715,7 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -762,7 +762,7 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -813,8 +813,8 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
UserID int64 `uri:"uid"` UserID int64 `uri:"uid"`
} }
type body struct { type body struct {
ChannelOwnerUserID int64 `form:"channel_owner_user_id"` ChannelOwnerUserID int64 `form:"channel_owner_user_id" binding:"required"`
Channel string `form:"channel_name"` Channel string `form:"channel_name" binding:"required"`
} }
type query struct { type query struct {
ChanSubscribeKey *string `form:"chan_subscribe_key"` ChanSubscribeKey *string `form:"chan_subscribe_key"`
@ -823,7 +823,7 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var q query var q query
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, &q, &b) ctx, errResp := h.app.StartRequest(g, &u, &q, &b, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -841,6 +841,10 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
return ginresp.InternAPIError(400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) return ginresp.InternAPIError(400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
} }
if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) {
ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
sub, err := h.database.CreateSubscription(ctx, u.UserID, *channel, channel.OwnerUserID == u.UserID) sub, err := h.database.CreateSubscription(ctx, u.UserID, *channel, channel.OwnerUserID == u.UserID)
if err != nil { if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create subscription", err) return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
@ -875,7 +879,7 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b) ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -944,7 +948,7 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
} }
var q query var q query
ctx, errResp := h.app.StartRequest(g, nil, &q, nil) ctx, errResp := h.app.StartRequest(g, nil, &q, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -1010,7 +1014,7 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -1079,7 +1083,7 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil) ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }

View File

@ -53,7 +53,7 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
var datq query var datq query
var datb query var datb query
ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb) ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -152,7 +152,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
var datq query var datq query
var datb query var datb query
ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb) ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -225,7 +225,7 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
var datq query var datq query
var datb query var datb query
ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb) ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -287,7 +287,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
var datq query var datq query
var datb query var datb query
ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb) ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -351,7 +351,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
var datq query var datq query
var datb query var datb query
ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb) ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -448,7 +448,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
var datq query var datq query
var datb query var datb query
ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb) ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
@ -531,7 +531,7 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
var datq query var datq query
var datb query var datb query
ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb) ctx, errResp := h.app.StartRequest(g, nil, &datq, &datb, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }

View File

@ -30,6 +30,62 @@ func NewMessageHandler(app *logic.Application) MessageHandler {
} }
} }
// SendMessageCompat swaggerdoc
//
// @Deprecated
//
// @Summary Send a new message (compatibility)
// @Description All parameter can be set via query-parameter or form-data body. Only UserID, UserKey and Title are required
//
// @Param query_data query handler.SendMessageCompat.query false " "
// @Param form_data formData handler.SendMessageCompat.form false " "
//
// @Success 200 {object} handler.sendMessageInternal.response
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError
// @Failure 403 {object} ginresp.apiError
// @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
//
// @Router /send.php [POST]
func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
type query struct {
UserID *int64 `form:"user_id"`
UserKey *string `form:"user_key"`
Channel *string `form:"channel"`
ChanKey *string `form:"chan_key"`
Title *string `form:"title"`
Content *string `form:"content"`
Priority *int `form:"priority"`
UserMessageID *string `form:"msg_id"`
SendTimestamp *float64 `form:"timestamp"`
}
type form struct {
UserID *int64 `form:"user_id"`
UserKey *string `form:"user_key"`
Channel *string `form:"channel"`
ChanKey *string `form:"chan_key"`
Title *string `form:"title"`
Content *string `form:"content"`
Priority *int `form:"priority"`
UserMessageID *string `form:"msg_id"`
SendTimestamp *float64 `form:"timestamp"`
}
var f form
var q query
ctx, errResp := h.app.StartRequest(g, nil, &q, nil, &f)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
data := dataext.ObjectMerge(f, q)
return h.sendMessageInternal(ctx, data.UserID, data.UserKey, data.Channel, data.ChanKey, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp)
}
// SendMessage swaggerdoc // SendMessage swaggerdoc
// //
// @Summary Send a new message // @Summary Send a new message
@ -38,7 +94,7 @@ func NewMessageHandler(app *logic.Application) MessageHandler {
// @Param query_data query handler.SendMessage.query false " " // @Param query_data query handler.SendMessage.query false " "
// @Param post_body body handler.SendMessage.body false " " // @Param post_body body handler.SendMessage.body false " "
// //
// @Success 200 {object} handler.SendMessage.response // @Success 200 {object} handler.sendMessageInternal.response
// @Failure 400 {object} ginresp.apiError // @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError // @Failure 401 {object} ginresp.apiError
// @Failure 403 {object} ginresp.apiError // @Failure 403 {object} ginresp.apiError
@ -53,8 +109,8 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
UserKey *string `form:"user_key"` UserKey *string `form:"user_key"`
Channel *string `form:"channel"` Channel *string `form:"channel"`
ChanKey *string `form:"chan_key"` ChanKey *string `form:"chan_key"`
Title *string `form:"message_title"` Title *string `form:"title"`
Content *string `form:"message_content"` Content *string `form:"content"`
Priority *int `form:"priority"` Priority *int `form:"priority"`
UserMessageID *string `form:"msg_id"` UserMessageID *string `form:"msg_id"`
SendTimestamp *float64 `form:"timestamp"` SendTimestamp *float64 `form:"timestamp"`
@ -64,12 +120,28 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
UserKey *string `json:"user_key"` UserKey *string `json:"user_key"`
Channel *string `json:"channel"` Channel *string `json:"channel"`
ChanKey *string `form:"chan_key"` ChanKey *string `form:"chan_key"`
Title *string `json:"message_title"` Title *string `json:"title"`
Content *string `json:"message_content"` Content *string `json:"content"`
Priority *int `json:"priority"` Priority *int `json:"priority"`
UserMessageID *string `json:"msg_id"` UserMessageID *string `json:"msg_id"`
SendTimestamp *float64 `json:"timestamp"` SendTimestamp *float64 `json:"timestamp"`
} }
var b body
var q query
ctx, errResp := h.app.StartRequest(g, nil, &q, &b, nil)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
data := dataext.ObjectMerge(b, q)
return h.sendMessageInternal(ctx, data.UserID, data.UserKey, data.Channel, data.ChanKey, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp)
}
func (h MessageHandler) sendMessageInternal(ctx *logic.AppContext, UserID *int64, UserKey *string, Channel *string, ChanKey *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64) ginresp.HTTPResponse {
type response struct { type response struct {
Success bool `json:"success"` Success bool `json:"success"`
ErrorID apierr.APIError `json:"error"` ErrorID apierr.APIError `json:"error"`
@ -83,65 +155,55 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
SCNMessageID int64 `json:"scn_msg_id"` SCNMessageID int64 `json:"scn_msg_id"`
} }
var b body if UserID == nil {
var q query return ginresp.SendAPIError(400, apierr.MISSING_UID, 101, "Missing parameter [[user_id]]", nil)
ctx, errResp := h.app.StartRequest(g, nil, &q, &b)
if errResp != nil {
return *errResp
} }
defer ctx.Cancel() if UserKey == nil {
return ginresp.SendAPIError(400, apierr.MISSING_UID, 102, "Missing parameter [[user_token]]", nil)
data := dataext.ObjectMerge(b, q)
if data.UserID == nil {
return ginresp.SendAPIError(400, apierr.MISSING_UID, 101, "Missing parameter [[user_id]]")
} }
if data.UserKey == nil { if Title == nil {
return ginresp.SendAPIError(400, apierr.MISSING_UID, 102, "Missing parameter [[user_token]]") return ginresp.SendAPIError(400, apierr.MISSING_UID, 103, "Missing parameter [[title]]", nil)
} }
if data.Title == nil { if SendTimestamp != nil && mathext.Abs(*SendTimestamp-float64(time.Now().Unix())) > (24*time.Hour).Seconds() {
return ginresp.SendAPIError(400, apierr.MISSING_UID, 103, "Missing parameter [[title]]") return ginresp.SendAPIError(400, apierr.TIMESTAMP_OUT_OF_RANGE, -1, "The timestamp mus be within 24 hours of now()", nil)
} }
if data.SendTimestamp != nil && mathext.Abs(*data.SendTimestamp-float64(time.Now().Unix())) > (24*time.Hour).Seconds() { if Priority != nil && (*Priority != 0 && *Priority != 1 && *Priority != 2) {
return ginresp.SendAPIError(400, apierr.TIMESTAMP_OUT_OF_RANGE, -1, "The timestamp mus be within 24 hours of now()") return ginresp.SendAPIError(400, apierr.INVALID_PRIO, 105, "Invalid priority", nil)
} }
if data.Priority != nil && (*data.Priority != 0 && *data.Priority != 1 && *data.Priority != 2) { if len(strings.TrimSpace(*Title)) == 0 {
return ginresp.SendAPIError(400, apierr.INVALID_PRIO, 105, "Invalid priority") return ginresp.SendAPIError(400, apierr.NO_TITLE, 103, "No title specified", nil)
} }
if len(strings.TrimSpace(*data.Title)) == 0 { if UserMessageID != nil && len(strings.TrimSpace(*UserMessageID)) > 64 {
return ginresp.SendAPIError(400, apierr.NO_TITLE, 103, "No title specified") return ginresp.SendAPIError(400, apierr.USR_MSG_ID_TOO_LONG, -1, "MessageID too long (64 characters)", nil)
}
if data.UserMessageID != nil && len(strings.TrimSpace(*data.UserMessageID)) > 64 {
return ginresp.SendAPIError(400, apierr.USR_MSG_ID_TOO_LONG, -1, "MessageID too long (64 characters)")
} }
channelName := "main" channelName := "main"
if data.Channel != nil { if Channel != nil {
channelName = strings.ToLower(strings.TrimSpace(*data.Channel)) channelName = strings.ToLower(strings.TrimSpace(*Channel))
} }
user, err := h.database.GetUser(ctx, *data.UserID) user, err := h.database.GetUser(ctx, *UserID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return ginresp.SendAPIError(400, apierr.USER_NOT_FOUND, -1, "User not found") return ginresp.SendAPIError(400, apierr.USER_NOT_FOUND, -1, "User not found", nil)
} }
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query user") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query user", err)
} }
if len(strings.TrimSpace(*data.Title)) > 120 { if len(strings.TrimSpace(*Title)) > 120 {
return ginresp.SendAPIError(400, apierr.TITLE_TOO_LONG, 103, "Title too long (120 characters)") return ginresp.SendAPIError(400, apierr.TITLE_TOO_LONG, 103, "Title too long (120 characters)", nil)
} }
if data.Content != nil && len(strings.TrimSpace(*data.Content)) > user.MaxContentLength() { if Content != nil && len(strings.TrimSpace(*Content)) > user.MaxContentLength() {
return ginresp.SendAPIError(400, apierr.CONTENT_TOO_LONG, 104, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(strings.TrimSpace(*data.Content)), user.MaxContentLength())) return ginresp.SendAPIError(400, apierr.CONTENT_TOO_LONG, 104, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(strings.TrimSpace(*Content)), user.MaxContentLength()), nil)
} }
if data.UserMessageID != nil { if UserMessageID != nil {
msg, err := h.database.GetMessageByUserMessageID(ctx, *data.UserMessageID) msg, err := h.database.GetMessageByUserMessageID(ctx, *UserMessageID)
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query existing message") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query existing message", err)
} }
if msg != nil { if msg != nil {
return ginresp.JSON(http.StatusOK, response{ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{
Success: true, Success: true,
ErrorID: apierr.NO_ERROR, ErrorID: apierr.NO_ERROR,
ErrorHighlight: -1, ErrorHighlight: -1,
@ -152,58 +214,58 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
IsPro: user.IsPro, IsPro: user.IsPro,
QuotaMax: user.QuotaPerDay(), QuotaMax: user.QuotaPerDay(),
SCNMessageID: msg.SCNMessageID, SCNMessageID: msg.SCNMessageID,
}) }))
} }
} }
if user.QuotaRemainingToday() <= 0 { if user.QuotaRemainingToday() <= 0 {
return ginresp.SendAPIError(403, apierr.QUOTA_REACHED, -1, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay())) return ginresp.SendAPIError(403, apierr.QUOTA_REACHED, -1, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil)
} }
channel, err := h.app.GetOrCreateChannel(ctx, *data.UserID, channelName) channel, err := h.app.GetOrCreateChannel(ctx, *UserID, channelName)
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query/create channel") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query/create channel", err)
} }
selfChanAdmin := *data.UserID == channel.OwnerUserID && *data.UserKey == user.AdminKey selfChanAdmin := *UserID == channel.OwnerUserID && *UserKey == user.AdminKey
selfChanSend := *data.UserID == channel.OwnerUserID && *data.UserKey == user.SendKey selfChanSend := *UserID == channel.OwnerUserID && *UserKey == user.SendKey
forgChanSend := *data.UserID != channel.OwnerUserID && data.ChanKey != nil && *data.ChanKey == channel.SendKey forgChanSend := *UserID != channel.OwnerUserID && ChanKey != nil && *ChanKey == channel.SendKey
if !selfChanAdmin && !selfChanSend && !forgChanSend { if !selfChanAdmin && !selfChanSend && !forgChanSend {
return ginresp.SendAPIError(401, apierr.USER_AUTH_FAILED, 102, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay())) return ginresp.SendAPIError(401, apierr.USER_AUTH_FAILED, 102, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil)
} }
var sendTimestamp *time.Time = nil var sendTimestamp *time.Time = nil
if data.SendTimestamp != nil { if SendTimestamp != nil {
sendTimestamp = langext.Ptr(timeext.UnixFloatSeconds(*data.SendTimestamp)) sendTimestamp = langext.Ptr(timeext.UnixFloatSeconds(*SendTimestamp))
} }
priority := langext.Coalesce(data.Priority, 1) priority := langext.Coalesce(Priority, 1)
msg, err := h.database.CreateMessage(ctx, *data.UserID, channel, sendTimestamp, *data.Title, data.Content, priority, data.UserMessageID) msg, err := h.database.CreateMessage(ctx, *UserID, channel, sendTimestamp, *Title, Content, priority, UserMessageID)
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create message in db") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create message in db", err)
} }
subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID) subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID)
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions", err)
} }
err = h.database.IncUserMessageCounter(ctx, user) err = h.database.IncUserMessageCounter(ctx, user)
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to inc user msg-counter") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to inc user msg-counter", err)
} }
err = h.database.IncChannelMessageCounter(ctx, channel) err = h.database.IncChannelMessageCounter(ctx, channel)
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to channel msg-counter") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to inc channel msg-counter", err)
} }
for _, sub := range subscriptions { for _, sub := range subscriptions {
clients, err := h.database.ListClients(ctx, sub.SubscriberUserID) clients, err := h.database.ListClients(ctx, sub.SubscriberUserID)
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query clients") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query clients", err)
} }
if !sub.Confirmed { if !sub.Confirmed {
@ -216,19 +278,19 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
if err != nil { if err != nil {
_, err = h.database.CreateRetryDelivery(ctx, client, msg) _, err = h.database.CreateRetryDelivery(ctx, client, msg)
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create delivery") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create delivery", err)
} }
} else { } else {
_, err = h.database.CreateSuccessDelivery(ctx, client, msg, *fcmDelivID) _, err = h.database.CreateSuccessDelivery(ctx, client, msg, *fcmDelivID)
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create delivery") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create delivery", err)
} }
} }
} }
} }
return ginresp.JSON(http.StatusOK, response{ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{
Success: true, Success: true,
ErrorID: apierr.NO_ERROR, ErrorID: apierr.NO_ERROR,
ErrorHighlight: -1, ErrorHighlight: -1,
@ -239,7 +301,7 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
IsPro: user.IsPro, IsPro: user.IsPro,
QuotaMax: user.QuotaPerDay(), QuotaMax: user.QuotaPerDay(),
SCNMessageID: msg.SCNMessageID, SCNMessageID: msg.SCNMessageID,
}) }))
} }
func (h MessageHandler) deliverMessage(ctx *logic.AppContext, client models.Client, msg models.Message) (*string, error) { func (h MessageHandler) deliverMessage(ctx *logic.AppContext, client models.Client, msg models.Message) (*string, error) {

View File

@ -94,7 +94,7 @@ func (r *Router) Init(e *gin.Engine) {
apiv2 := e.Group("/api-v2/") apiv2 := e.Group("/api-v2/")
{ {
apiv2.POST("/users/", ginresp.Wrap(r.apiHandler.CreateUser)) apiv2.POST("/users", ginresp.Wrap(r.apiHandler.CreateUser))
apiv2.GET("/users/:uid", ginresp.Wrap(r.apiHandler.GetUser)) apiv2.GET("/users/:uid", ginresp.Wrap(r.apiHandler.GetUser))
apiv2.PATCH("/users/:uid", ginresp.Wrap(r.apiHandler.UpdateUser)) apiv2.PATCH("/users/:uid", ginresp.Wrap(r.apiHandler.UpdateUser))
@ -127,7 +127,7 @@ func (r *Router) Init(e *gin.Engine) {
{ {
sendAPI.POST("/", ginresp.Wrap(r.messageHandler.SendMessage)) sendAPI.POST("/", ginresp.Wrap(r.messageHandler.SendMessage))
sendAPI.POST("/send", ginresp.Wrap(r.messageHandler.SendMessage)) sendAPI.POST("/send", ginresp.Wrap(r.messageHandler.SendMessage))
sendAPI.POST("/send.php", ginresp.Wrap(r.messageHandler.SendMessage)) sendAPI.POST("/send.php", ginresp.Wrap(r.messageHandler.SendMessageCompat))
} }
if r.app.Config.ReturnRawErrors { if r.app.Config.ReturnRawErrors {

View File

@ -5,7 +5,8 @@ type apiError struct {
Error int `json:"error"` Error int `json:"error"`
ErrorHighlight int `json:"errhighlight"` ErrorHighlight int `json:"errhighlight"`
Message string `json:"message"` Message string `json:"message"`
RawError error `json:"errorObject,omitempty"` RawError string `json:"errorObj,omitempty"`
Trace string `json:"traceObj,omitempty"`
} }
type compatAPIError struct { type compatAPIError struct {

View File

@ -3,8 +3,11 @@ package ginresp
import ( import (
scn "blackforestbytes.com/simplecloudnotifier" scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"net/http" "net/http"
"runtime/debug"
) )
type HTTPResponse interface { type HTTPResponse interface {
@ -64,25 +67,39 @@ func Text(sc int, data string) HTTPResponse {
} }
func InternalError(e error) HTTPResponse { func InternalError(e error) HTTPResponse {
log.Error().Err(e).Msg("[InternalError] " + e.Error())
return &jsonHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(apierr.INTERNAL_EXCEPTION), Message: e.Error()}} return &jsonHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(apierr.INTERNAL_EXCEPTION), Message: e.Error()}}
} }
func InternAPIError(status int, errorid apierr.APIError, msg string, e error) HTTPResponse { func InternAPIError(status int, errorid apierr.APIError, msg string, e error) HTTPResponse {
log.Error().Int("errorid", int(errorid)).Err(e).Msg("[InternAPIError] " + msg)
if scn.Conf.ReturnRawErrors { if scn.Conf.ReturnRawErrors {
return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), Message: msg, RawError: e}} return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), Message: msg, RawError: fmt.Sprintf("%+v", e), Trace: string(debug.Stack())}}
} else { } else {
return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), Message: msg}} return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), Message: msg}}
} }
} }
func CompatAPIError(errid int, msg string) HTTPResponse { func CompatAPIError(errid int, msg string) HTTPResponse {
log.Error().Int("errid", errid).Msg("[CompatAPIError] " + msg)
return &jsonHTTPResponse{statusCode: 200, data: compatAPIError{Success: false, ErrorID: errid, Message: msg}} return &jsonHTTPResponse{statusCode: 200, data: compatAPIError{Success: false, ErrorID: errid, Message: msg}}
} }
func SendAPIError(status int, errorid apierr.APIError, highlight int, msg string) HTTPResponse { func SendAPIError(status int, errorid apierr.APIError, highlight int, msg string, e error) HTTPResponse {
return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}} log.Error().Int("errorid", int(errorid)).Int("highlight", highlight).Err(e).Msg("[SendAPIError] " + msg)
if scn.Conf.ReturnRawErrors {
return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg, RawError: fmt.Sprintf("%+v", e), Trace: string(debug.Stack())}}
} else {
return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}}
}
} }
func NotImplemented() HTTPResponse { func NotImplemented() HTTPResponse {
log.Error().Msg("[NotImplemented]")
return &jsonHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: -1, ErrorHighlight: 0, Message: "Not Implemented"}} return &jsonHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: -1, ErrorHighlight: 0, Message: "Not Implemented"}}
} }

View File

@ -129,7 +129,7 @@ func (db *Database) IncChannelMessageCounter(ctx TxContext, channel models.Chann
return err return err
} }
_, err = tx.ExecContext(ctx, "UPDATE channels SET messages_sent = ? AND timestamp_lastsent = ? WHERE channel_id = ?", _, err = tx.ExecContext(ctx, "UPDATE channels SET messages_sent = ?, timestamp_lastsent = ? WHERE channel_id = ?",
channel.MessagesSent+1, channel.MessagesSent+1,
time2DB(time.Now()), time2DB(time.Now()),
channel.ChannelID) channel.ChannelID)

View File

@ -69,13 +69,13 @@ func (db *Database) ReadSchema(ctx context.Context) (int, error) {
return 0, errors.New("no schema entry in meta table") return 0, errors.New("no schema entry in meta table")
} }
var schema int var dbschema int
err = r2.Scan(&schema) err = r2.Scan(&dbschema)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return schema, nil return dbschema, nil
} }
func (db *Database) Ping() error { func (db *Database) Ping() error {
@ -83,5 +83,5 @@ func (db *Database) Ping() error {
} }
func (db *Database) BeginTx(ctx context.Context) (*sql.Tx, error) { func (db *Database) BeginTx(ctx context.Context) (*sql.Tx, error) {
return db.db.BeginTx(ctx, nil) return db.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelDefault})
} }

View File

@ -15,7 +15,7 @@ func (db *Database) CreateRetryDelivery(ctx TxContext, client models.Client, msg
now := time.Now().UTC() now := time.Now().UTC()
next := now.Add(5 * time.Second) next := now.Add(5 * time.Second)
res, err := tx.ExecContext(ctx, "INSERT INTO deliveries (scn_message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (?, ?, ?, ?, ?, ?, ?)", res, err := tx.ExecContext(ctx, "INSERT INTO deliveries (scn_message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
msg.SCNMessageID, msg.SCNMessageID,
client.UserID, client.UserID,
client.ClientID, client.ClientID,
@ -55,7 +55,7 @@ func (db *Database) CreateSuccessDelivery(ctx TxContext, client models.Client, m
now := time.Now().UTC() now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO deliveries (scn_message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (?, ?, ?, ?, ?, ?, ?)", res, err := tx.ExecContext(ctx, "INSERT INTO deliveries (scn_message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
msg.SCNMessageID, msg.SCNMessageID,
client.UserID, client.UserID,
client.ClientID, client.ClientID,

View File

@ -57,7 +57,7 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID int64, channel mod
now := time.Now().UTC() now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO messages (sender_user_id, owner_user_id, channel_name, channel_id, timestamp_real, timestamp_client, title, content, priority, usr_message_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", res, err := tx.ExecContext(ctx, "INSERT INTO messages (sender_user_id, owner_user_id, channel_name, channel_id, timestamp_real, timestamp_client, title, content, priority, usr_message_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
senderUserID, senderUserID,
channel.OwnerUserID, channel.OwnerUserID,
channel.Name, channel.Name,

View File

@ -103,7 +103,7 @@ CREATE TABLE deliveries
receiver_client_id INTEGER NOT NULL, receiver_client_id INTEGER NOT NULL,
timestamp_created INTEGER NOT NULL, timestamp_created INTEGER NOT NULL,
timestamp_finalized INTEGER NOT NULL, timestamp_finalized INTEGER NULL,
status TEXT CHECK(status IN ('RETRY','SUCCESS','FAILED')) NOT NULL, status TEXT CHECK(status IN ('RETRY','SUCCESS','FAILED')) NOT NULL,

View File

@ -126,7 +126,7 @@ func (db *Database) UpdateUserProToken(ctx TxContext, userid int64, protoken *st
return err return err
} }
_, err = tx.ExecContext(ctx, "UPDATE users SET pro_token = ? AND is_pro = ? WHERE user_id = ?", _, err = tx.ExecContext(ctx, "UPDATE users SET pro_token = ?, is_pro = ? WHERE user_id = ?",
protoken, protoken,
bool2DB(protoken != nil), bool2DB(protoken != nil),
userid) userid)
@ -145,7 +145,7 @@ func (db *Database) IncUserMessageCounter(ctx TxContext, user models.User) error
quota := user.QuotaUsedToday() + 1 quota := user.QuotaUsedToday() + 1
_, err = tx.ExecContext(ctx, "UPDATE users SET timestamp_lastsent = ? AND messages_sent = ? AND quota_used = ? AND quota_used_day = ? WHERE user_id = ?", _, err = tx.ExecContext(ctx, "UPDATE users SET timestamp_lastsent = ?, messages_sent = ?, quota_used = ?, quota_used_day = ? WHERE user_id = ?",
time2DB(time.Now()), time2DB(time.Now()),
user.MessagesSent+1, user.MessagesSent+1,
quota, quota,
@ -180,7 +180,7 @@ func (db *Database) UpdateUserKeys(ctx TxContext, userid int64, sendKey string,
return err return err
} }
_, err = tx.ExecContext(ctx, "UPDATE users SET send_key = ? AND read_key = ? AND admin_key = ? WHERE user_id = ?", _, err = tx.ExecContext(ctx, "UPDATE users SET send_key = ?, read_key = ?, admin_key = ? WHERE user_id = ?",
sendKey, sendKey,
readKey, readKey,
adminKey, adminKey,

View File

@ -9,6 +9,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"context" "context"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"math/rand" "math/rand"
@ -100,23 +101,29 @@ func (app *Application) Migrate() error {
return app.Database.Migrate(ctx) return app.Database.Migrate(ctx)
} }
func (app *Application) StartRequest(g *gin.Context, uri any, query any, body any) (*AppContext, *ginresp.HTTPResponse) { func (app *Application) StartRequest(g *gin.Context, uri any, query any, body any, form any) (*AppContext, *ginresp.HTTPResponse) {
if body != nil { if uri != nil {
if err := g.ShouldBindJSON(&body); err != nil { if err := g.ShouldBindUri(uri); err != nil {
return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_BODY_PARAM, "Failed to read body", err)) return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err))
} }
} }
if query != nil { if query != nil {
if err := g.ShouldBindQuery(&query); err != nil { if err := g.ShouldBindQuery(query); err != nil {
return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err)) return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err))
} }
} }
if uri != nil { if body != nil {
if err := g.ShouldBindUri(&uri); err != nil { if err := g.ShouldBindJSON(body); err != nil {
return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err)) return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_BODY_PARAM, "Failed to read body", err))
}
}
if form != nil {
if err := g.ShouldBindWith(form, binding.Form); err != nil {
return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_BODY_PARAM, "Failed to read multipart-form", err))
} }
} }

View File

@ -7,6 +7,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"github.com/rs/zerolog/log"
"time" "time"
) )
@ -47,6 +48,7 @@ func (ac *AppContext) Value(key any) any {
func (ac *AppContext) Cancel() { func (ac *AppContext) Cancel() {
ac.cancelled = true ac.cancelled = true
if ac.transaction != nil { if ac.transaction != nil {
log.Error().Msg("Rollback transaction")
err := ac.transaction.Rollback() err := ac.transaction.Rollback()
if err != nil { if err != nil {
panic("failed to rollback transaction: " + err.Error()) panic("failed to rollback transaction: " + err.Error())

View File

@ -72,7 +72,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handler.SendMessage.response" "$ref": "#/definitions/handler.sendMessageInternal.response"
} }
}, },
"400": { "400": {
@ -1404,7 +1404,144 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handler.SendMessage.response" "$ref": "#/definitions/handler.sendMessageInternal.response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/send.php": {
"post": {
"description": "All parameter can be set via query-parameter or form-data body. Only UserID, UserKey and Title are required",
"summary": "Send a new message (compatibility)",
"deprecated": true,
"parameters": [
{
"type": "string",
"name": "chanKey",
"in": "query"
},
{
"type": "string",
"name": "channel",
"in": "query"
},
{
"type": "string",
"name": "content",
"in": "query"
},
{
"type": "integer",
"name": "priority",
"in": "query"
},
{
"type": "number",
"name": "sendTimestamp",
"in": "query"
},
{
"type": "string",
"name": "title",
"in": "query"
},
{
"type": "integer",
"name": "userID",
"in": "query"
},
{
"type": "string",
"name": "userKey",
"in": "query"
},
{
"type": "string",
"name": "userMessageID",
"in": "query"
},
{
"type": "string",
"name": "chanKey",
"in": "formData"
},
{
"type": "string",
"name": "channel",
"in": "formData"
},
{
"type": "string",
"name": "content",
"in": "formData"
},
{
"type": "integer",
"name": "priority",
"in": "formData"
},
{
"type": "number",
"name": "sendTimestamp",
"in": "formData"
},
{
"type": "string",
"name": "title",
"in": "formData"
},
{
"type": "integer",
"name": "userID",
"in": "formData"
},
{
"type": "string",
"name": "userKey",
"in": "formData"
},
{
"type": "string",
"name": "userMessageID",
"in": "formData"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.sendMessageInternal.response"
} }
}, },
"400": { "400": {
@ -1451,12 +1588,17 @@
"error": { "error": {
"type": "integer" "type": "integer"
}, },
"errorObject": {}, "errorObj": {
"type": "string"
},
"message": { "message": {
"type": "string" "type": "string"
}, },
"success": { "success": {
"type": "boolean" "type": "boolean"
},
"traceObj": {
"type": "string"
} }
} }
}, },
@ -1493,6 +1635,12 @@
}, },
"handler.AddClient.body": { "handler.AddClient.body": {
"type": "object", "type": "object",
"required": [
"agent_model",
"agent_version",
"client_type",
"fcm_token"
],
"properties": { "properties": {
"agent_model": { "agent_model": {
"type": "string" "type": "string"
@ -1510,6 +1658,10 @@
}, },
"handler.CreateSubscription.body": { "handler.CreateSubscription.body": {
"type": "object", "type": "object",
"required": [
"channel",
"channelOwnerUserID"
],
"properties": { "properties": {
"channel": { "channel": {
"type": "string" "type": "string"
@ -1521,6 +1673,12 @@
}, },
"handler.CreateUser.body": { "handler.CreateUser.body": {
"type": "object", "type": "object",
"required": [
"agent_model",
"agent_version",
"client_type",
"fcm_token"
],
"properties": { "properties": {
"agent_model": { "agent_model": {
"type": "string" "type": "string"
@ -1746,10 +1904,7 @@
"channel": { "channel": {
"type": "string" "type": "string"
}, },
"message_content": { "content": {
"type": "string"
},
"message_title": {
"type": "string" "type": "string"
}, },
"msg_id": { "msg_id": {
@ -1761,6 +1916,9 @@
"timestamp": { "timestamp": {
"type": "number" "type": "number"
}, },
"title": {
"type": "string"
},
"user_id": { "user_id": {
"type": "integer" "type": "integer"
}, },
@ -1769,41 +1927,6 @@
} }
} }
}, },
"handler.SendMessage.response": {
"type": "object",
"properties": {
"errhighlight": {
"type": "integer"
},
"error": {
"type": "integer"
},
"is_pro": {
"type": "boolean"
},
"message": {
"type": "string"
},
"messagecount": {
"type": "integer"
},
"quota": {
"type": "integer"
},
"quota_max": {
"type": "integer"
},
"scn_msg_id": {
"type": "integer"
},
"success": {
"type": "boolean"
},
"suppress_send": {
"type": "boolean"
}
}
},
"handler.Update.response": { "handler.Update.response": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1901,6 +2024,41 @@
} }
} }
}, },
"handler.sendMessageInternal.response": {
"type": "object",
"properties": {
"errhighlight": {
"type": "integer"
},
"error": {
"type": "integer"
},
"is_pro": {
"type": "boolean"
},
"message": {
"type": "string"
},
"messagecount": {
"type": "integer"
},
"quota": {
"type": "integer"
},
"quota_max": {
"type": "integer"
},
"scn_msg_id": {
"type": "integer"
},
"success": {
"type": "boolean"
},
"suppress_send": {
"type": "boolean"
}
}
},
"models.ChannelJSON": { "models.ChannelJSON": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -6,11 +6,14 @@ definitions:
type: integer type: integer
error: error:
type: integer type: integer
errorObject: {} errorObj:
type: string
message: message:
type: string type: string
success: success:
type: boolean type: boolean
traceObj:
type: string
type: object type: object
ginresp.compatAPIError: ginresp.compatAPIError:
properties: properties:
@ -42,6 +45,11 @@ definitions:
type: string type: string
fcm_token: fcm_token:
type: string type: string
required:
- agent_model
- agent_version
- client_type
- fcm_token
type: object type: object
handler.CreateSubscription.body: handler.CreateSubscription.body:
properties: properties:
@ -49,6 +57,9 @@ definitions:
type: string type: string
channelOwnerUserID: channelOwnerUserID:
type: integer type: integer
required:
- channel
- channelOwnerUserID
type: object type: object
handler.CreateUser.body: handler.CreateUser.body:
properties: properties:
@ -64,6 +75,11 @@ definitions:
type: string type: string
username: username:
type: string type: string
required:
- agent_model
- agent_version
- client_type
- fcm_token
type: object type: object
handler.DatabaseTest.response: handler.DatabaseTest.response:
properties: properties:
@ -197,9 +213,7 @@ definitions:
type: string type: string
channel: channel:
type: string type: string
message_content: content:
type: string
message_title:
type: string type: string
msg_id: msg_id:
type: string type: string
@ -207,34 +221,13 @@ definitions:
type: integer type: integer
timestamp: timestamp:
type: number type: number
title:
type: string
user_id: user_id:
type: integer type: integer
user_key: user_key:
type: string type: string
type: object type: object
handler.SendMessage.response:
properties:
errhighlight:
type: integer
error:
type: integer
is_pro:
type: boolean
message:
type: string
messagecount:
type: integer
quota:
type: integer
quota_max:
type: integer
scn_msg_id:
type: integer
success:
type: boolean
suppress_send:
type: boolean
type: object
handler.Update.response: handler.Update.response:
properties: properties:
is_pro: is_pro:
@ -298,6 +291,29 @@ definitions:
uri: uri:
type: string type: string
type: object type: object
handler.sendMessageInternal.response:
properties:
errhighlight:
type: integer
error:
type: integer
is_pro:
type: boolean
message:
type: string
messagecount:
type: integer
quota:
type: integer
quota_max:
type: integer
scn_msg_id:
type: integer
success:
type: boolean
suppress_send:
type: boolean
type: object
models.ChannelJSON: models.ChannelJSON:
properties: properties:
channel_id: channel_id:
@ -468,7 +484,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handler.SendMessage.response' $ref: '#/definitions/handler.sendMessageInternal.response'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1355,7 +1371,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handler.SendMessage.response' $ref: '#/definitions/handler.sendMessageInternal.response'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1377,4 +1393,90 @@ paths:
schema: schema:
$ref: '#/definitions/ginresp.apiError' $ref: '#/definitions/ginresp.apiError'
summary: Send a new message summary: Send a new message
/send.php:
post:
deprecated: true
description: All parameter can be set via query-parameter or form-data body.
Only UserID, UserKey and Title are required
parameters:
- in: query
name: chanKey
type: string
- in: query
name: channel
type: string
- in: query
name: content
type: string
- in: query
name: priority
type: integer
- in: query
name: sendTimestamp
type: number
- in: query
name: title
type: string
- in: query
name: userID
type: integer
- in: query
name: userKey
type: string
- in: query
name: userMessageID
type: string
- in: formData
name: chanKey
type: string
- in: formData
name: channel
type: string
- in: formData
name: content
type: string
- in: formData
name: priority
type: integer
- in: formData
name: sendTimestamp
type: number
- in: formData
name: title
type: string
- in: formData
name: userID
type: integer
- in: formData
name: userKey
type: string
- in: formData
name: userMessageID
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.sendMessageInternal.response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/ginresp.apiError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: Not Found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Send a new message (compatibility)
swagger: "2.0" swagger: "2.0"

View File

@ -247,4 +247,8 @@ pre, pre span
{ {
font-family: Menlo, Consolas, monospace; font-family: Menlo, Consolas, monospace;
background: #F9F9F9;; background: #F9F9F9;;
}
.display_none {
display: none;
} }

View File

@ -28,33 +28,33 @@
<div class="row responsive-label"> <div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div> <div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div>
<div class="col-sm-12 col-md"><input placeholder="UserID" id="uid" class="doc" <?php echo (isset($_GET['preset_user_id']) ? (' value="'.$_GET['preset_user_id'].'" '):(''));?> type="number"></div> <div class="col-sm-12 col-md"><input placeholder="UserID" id="uid" class="doc" type="number"></div>
</div> </div>
<div class="row responsive-label"> <div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="ukey" class="doc">Authentification Key</label></div> <div class="col-sm-12 col-md-3"><label for="ukey" class="doc">Authentification Key</label></div>
<div class="col-sm-12 col-md"><input placeholder="Key" id="ukey" class="doc" <?php echo (isset($_GET['preset_user_key']) ? (' value="'.$_GET['preset_user_key'].'" '):(''));?> type="text" maxlength="64"></div> <div class="col-sm-12 col-md"><input placeholder="Key" id="ukey" class="doc" type="text" maxlength="64"></div>
</div> </div>
<div class="row responsive-label"> <div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="prio" class="doc">Priority</label></div> <div class="col-sm-12 col-md-3"><label for="prio" class="doc">Priority</label></div>
<div class="col-sm-12 col-md"> <div class="col-sm-12 col-md">
<select id="prio" class="doc" type="text" style="width:100%;"> <select id="prio" class="doc" type="text" style="width:100%;">
<option value="0" <?php echo (( isset($_GET['preset_priority'])&&$_GET['preset_priority']==='0') ? 'selected':'');?>>Low</option> <option value="0" >Low</option>
<option value="1" <?php echo ((!isset($_GET['preset_priority'])||$_GET['preset_priority']==='1') ? 'selected':'');?>>Normal</option> <option value="1" selected>Normal</option>
<option value="2" <?php echo (( isset($_GET['preset_priority'])&&$_GET['preset_priority']==='2') ? 'selected':'');?>>High</option> <option value="2" >High</option>
</select> </select>
</div> </div>
</div> </div>
<div class="row responsive-label"> <div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="msg" class="doc">Message Title</label></div> <div class="col-sm-12 col-md-3"><label for="msg" class="doc">Message Title</label></div>
<div class="col-sm-12 col-md"><input placeholder="Message" id="msg" class="doc" <?php echo (isset($_GET['preset_title']) ? (' value="'.$_GET['preset_title'].'" '):(''));?> type="text" maxlength="80"></div> <div class="col-sm-12 col-md"><input placeholder="Message" id="msg" class="doc" type="text" maxlength="80"></div>
</div> </div>
<div class="row responsive-label"> <div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="txt" class="doc">Message Content</label></div> <div class="col-sm-12 col-md-3"><label for="txt" class="doc">Message Content</label></div>
<div class="col-sm-12 col-md"><textarea id="txt" class="doc" <?php echo (isset($_GET['preset_content']) ? (' value="'.$_GET['preset_content'].'" '):(''));?> rows="8" maxlength="2048"></textarea></div> <div class="col-sm-12 col-md"><textarea id="txt" class="doc" rows="8" maxlength="2048"></textarea></div>
</div> </div>
<div class="row"> <div class="row">

View File

@ -28,7 +28,7 @@ function send()
data.append('priority', pio.value); data.append('priority', pio.value);
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
xhr.open('POST', '/', true); xhr.open('POST', '/send.php', true);
xhr.onreadystatechange = function () xhr.onreadystatechange = function ()
{ {
if (xhr.readyState !== 4) return; if (xhr.readyState !== 4) return;
@ -81,10 +81,27 @@ function send()
xhr.send(data); xhr.send(data);
} }
window.addEventListener("load",function () window.addEventListener("load", function ()
{ {
let btnSend = document.getElementById("btnSend"); const qp = new URLSearchParams(window.location.search);
if (btnSend !== undefined) btnSend.onclick = function () { send(); return false; }; const btnSend = document.getElementById("btnSend");
const selPrio = document.getElementById("prio");
const txtKey = document.getElementById("ukey");
const txtUID = document.getElementById("uid");
const txtTitl = document.getElementById("msg");
const txtCont = document.getElementById("txt");
},false); btnSend.onclick = function () { send(); return false; };
if (qp.has('preset_priority')) selPrio.selectedIndex = parseInt(qp.get("preset_priority"));
if (qp.has('preset_user_key')) txtKey.value = qp.get("preset_user_key");
if (qp.has('preset_user_id')) txtUID.value = qp.get("preset_user_id");
if (qp.has('preset_title')) txtTitl.value = qp.get("preset_title");
if (qp.has('preset_content')) txtCont.value = qp.get("preset_content");
}, false);

View File

@ -0,0 +1,31 @@
window.addEventListener("load", function ()
{
const qp = new URLSearchParams(window.location.search);
const spanQuota1 = document.getElementById("insQuota1");
const spanQuota2 = document.getElementById("insQuota2");
const linkSucc = document.getElementById("succ_link");
const linkErr = document.getElementById("err_link");
spanQuota1.innerText = qp.get('quota_remain') ?? 'ERR';
spanQuota2.innerText = qp.get('quota_max') ?? 'ERR';
const preset_user_id = qp.get('preset_user_id') ?? 'ERR';
const preset_user_key = qp.get('preset_user_key') ?? 'ERR';
linkSucc.setAttribute("href", "/?preset_user_id="+preset_user_id+"&preset_user_key="+preset_user_key);
if (qp.get("ok") === "1") {
linkSucc.classList.remove('display_none');
linkErr.classList.add('display_none');
} else {
linkSucc.classList.add('display_none');
linkErr.classList.remove('display_none');
}
}, false);

View File

@ -22,35 +22,31 @@
<div class="fullcenterflex"> <div class="fullcenterflex">
<?php if (isset($_GET['ok']) && $_GET['ok'] === "1" ): ?> <a id="succ_link" class="display_none card success" href="/?preset_user_id=<?php echo isset($_GET['preset_user_id'])?$_GET['preset_user_id']:'ERR';?>&preset_user_key=<?php echo isset($_GET['preset_user_key'])?$_GET['preset_user_key']:'ERR';?>">
<div class="section">
<h3 class="doc">Message sent</h3>
<p class="doc">Message succesfully sent<br>
<span id="insQuota1">ERR</span>/<span id="insQuota2">ERR</span> remaining</p>
</div>
</a>
<a class="card success" href="/index.php?preset_user_id=<?php echo isset($_GET['preset_user_id'])?$_GET['preset_user_id']:'ERR';?>&preset_user_key=<?php echo isset($_GET['preset_user_key'])?$_GET['preset_user_key']:'ERR';?>"> <a id="err_link" class="card error" href="/">
<div class="section"> <div class="section">
<h3 class="doc">Message sent</h3> <h3 class="doc">Failure</h3>
<p class="doc">Message succesfully sent<br> <p class="doc">Unknown error</p>
<?php echo isset($_GET['quota_remain'])?$_GET['quota_remain']:'ERR';?>/<?php echo isset($_GET['quota_max'])?$_GET['quota_max']:'ERR';?> remaining</p> </div>
</div> </a>
</a>
<?php else: ?>
<a class="card error" href="/index.php">
<div class="section">
<h3 class="doc">Failure</h3>
<p class="doc">Unknown error</p>
</div>
</a>
<?php endif; ?>
</div> </div>
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a> <a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a> <a tabindex="-1" href="/" class="button bordered" id="tr_link">Send</a>
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a> <a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
</div> </div>
<script src="/js/message_sent.js" type="text/javascript" ></script>
</body> </body>
</html> </html>