diff --git a/server/api/handler/api.go b/server/api/handler/api.go index e6b1b40..684f5c0 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -2,9 +2,10 @@ package handler import ( "blackforestbytes.com/simplecloudnotifier/api/apierr" - "blackforestbytes.com/simplecloudnotifier/api/models" "blackforestbytes.com/simplecloudnotifier/common/ginresp" + "blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/logic" + "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" @@ -13,7 +14,15 @@ import ( ) type APIHandler struct { - app *logic.Application + app *logic.Application + database *db.Database +} + +func NewAPIHandler(app *logic.Application) APIHandler { + return APIHandler{ + app: app, + database: app.Database, + } } // CreateUser swaggerdoc @@ -69,24 +78,24 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { sendKey := h.app.GenerateRandomAuthKey() adminKey := h.app.GenerateRandomAuthKey() - err := h.app.Database.ClearFCMTokens(ctx, b.FCMToken) + err := h.database.ClearFCMTokens(ctx, b.FCMToken) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } if b.ProToken != nil { - err := h.app.Database.ClearProTokens(ctx, *b.ProToken) + err := h.database.ClearProTokens(ctx, *b.ProToken) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } } - userobj, err := h.app.Database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, b.Username) + userobj, err := h.database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, b.Username) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } - _, err = h.app.Database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion) + _, err = h.database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } @@ -124,7 +133,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { return *permResp } - user, err := h.app.Database.GetUser(ctx, u.UserID) + user, err := h.database.GetUser(ctx, u.UserID) if err == sql.ErrNoRows { return ginresp.InternAPIError(404, apierr.USER_NOT_FOUND, "User not found", err) } @@ -177,7 +186,7 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { username = nil } - err := h.app.Database.UpdateUserUsername(ctx, u.UserID, b.Username) + err := h.database.UpdateUserUsername(ctx, u.UserID, b.Username) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update user", err) } @@ -193,18 +202,18 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { return ginresp.InternAPIError(400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) } - err = h.app.Database.ClearProTokens(ctx, *b.ProToken) + err = h.database.ClearProTokens(ctx, *b.ProToken) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } - err = h.app.Database.UpdateUserProToken(ctx, u.UserID, b.ProToken) + err = h.database.UpdateUserProToken(ctx, u.UserID, b.ProToken) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update user", err) } } - user, err := h.app.Database.GetUser(ctx, u.UserID) + user, err := h.database.GetUser(ctx, u.UserID) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err) } @@ -245,7 +254,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { return *permResp } - clients, err := h.app.Database.ListClients(ctx, u.UserID) + clients, err := h.database.ListClients(ctx, u.UserID) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query clients", err) } @@ -287,7 +296,7 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse { return *permResp } - client, err := h.app.Database.GetClient(ctx, u.UserID, u.ClientID) + client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) if err == sql.ErrNoRows { return ginresp.InternAPIError(404, apierr.CLIENT_NOT_FOUND, "Client not found", err) } @@ -346,7 +355,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { return *permResp } - client, err := h.app.Database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion) + client, err := h.database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } @@ -386,7 +395,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { return *permResp } - client, err := h.app.Database.GetClient(ctx, u.UserID, u.ClientID) + client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) if err == sql.ErrNoRows { return ginresp.InternAPIError(404, apierr.CLIENT_NOT_FOUND, "Client not found", err) } @@ -394,7 +403,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query client", err) } - err = h.app.Database.DeleteClient(ctx, u.ClientID) + err = h.database.DeleteClient(ctx, u.ClientID) if err != nil { return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to delete client", err) } @@ -446,8 +455,6 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { return ginresp.NotImplemented() } -func NewAPIHandler(app *logic.Application) APIHandler { - return APIHandler{ - app: app, - } +func (h APIHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() } diff --git a/server/api/handler/compat.go b/server/api/handler/compat.go index 026db23..cc18d88 100644 --- a/server/api/handler/compat.go +++ b/server/api/handler/compat.go @@ -1,9 +1,9 @@ package handler import ( - "blackforestbytes.com/simplecloudnotifier/api/models" "blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/logic" + "blackforestbytes.com/simplecloudnotifier/models" "github.com/gin-gonic/gin" ) diff --git a/server/api/handler/message.go b/server/api/handler/message.go index 0c64fdc..6f584fc 100644 --- a/server/api/handler/message.go +++ b/server/api/handler/message.go @@ -1,21 +1,232 @@ package handler import ( + "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/common/ginresp" + "blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/logic" + "blackforestbytes.com/simplecloudnotifier/models" + "database/sql" + "fmt" "github.com/gin-gonic/gin" + "gogs.mikescher.com/BlackForestBytes/goext/dataext" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "gogs.mikescher.com/BlackForestBytes/goext/mathext" + "gogs.mikescher.com/BlackForestBytes/goext/timeext" + "net/http" + "strings" + "time" ) type MessageHandler struct { - app *logic.Application -} - -func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() + app *logic.Application + database *db.Database } func NewMessageHandler(app *logic.Application) MessageHandler { return MessageHandler{ - app: app, + app: app, + database: app.Database, + } +} + +// SendMessage 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 +// +// @Param query_data query handler.SendMessage.query false " " +// @Param post_body body handler.SendMessage.body false " " +// +// @Success 200 {object} handler.SendMessage.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 / [POST] +// @Router /send [POST] +func (h MessageHandler) SendMessage(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:"message_title"` + Content *string `form:"message_content"` + Priority *int `form:"priority"` + UserMessageID *string `form:"msg_id"` + SendTimestamp *float64 `form:"timestamp"` + } + type body struct { + UserID *int64 `json:"user_id"` + UserKey *string `json:"user_key"` + Channel *string `json:"channel"` + ChanKey *string `form:"chan_key"` + Title *string `json:"message_title"` + Content *string `json:"message_content"` + Priority *int `json:"priority"` + UserMessageID *string `json:"msg_id"` + SendTimestamp *float64 `json:"timestamp"` + } + type response struct { + Success bool `json:"success"` + ErrorID apierr.APIError `json:"error"` + ErrorHighlight int `json:"errhighlight"` + Message string `json:"message"` + SuppressSend bool `json:"suppress_send"` + Response string `json:"response"` + MessageCount int `json:"messagecount"` + Quota int `json:"quota"` + IsPro bool `json:"is_pro"` + QuotaMax int `json:"quota_max"` + SCNMessageID int64 `json:"scn_msg_id"` + } + + var b body + var q query + ctx, errResp := h.app.StartRequest(g, nil, &q, &b) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + 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 { + return ginresp.SendAPIError(400, apierr.MISSING_UID, 102, "Missing parameter [[user_token]]") + } + if data.Title == nil { + return ginresp.SendAPIError(400, apierr.MISSING_UID, 103, "Missing parameter [[title]]") + } + if data.SendTimestamp != nil && mathext.Abs(*data.SendTimestamp-float64(time.Now().Unix())) > (24*time.Hour).Seconds() { + return ginresp.SendAPIError(400, apierr.TIMESTAMP_OUT_OF_RANGE, -1, "The timestamp mus be within 24 hours of now()") + } + if data.Priority != nil && (*data.Priority != 0 && *data.Priority != 1 && *data.Priority != 2) { + return ginresp.SendAPIError(400, apierr.INVALID_PRIO, 105, "Invalid priority") + } + if len(strings.TrimSpace(*data.Title)) == 0 { + return ginresp.SendAPIError(400, apierr.NO_TITLE, 103, "No title specified") + } + 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" + if data.Channel != nil { + channelName = strings.ToLower(strings.TrimSpace(*data.Channel)) + } + + user, err := h.database.GetUser(ctx, *data.UserID) + if err == sql.ErrNoRows { + return ginresp.SendAPIError(400, apierr.USER_NOT_FOUND, -1, "User not found") + } + if err != nil { + return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query user") + } + + if len(strings.TrimSpace(*data.Title)) > 120 { + return ginresp.SendAPIError(400, apierr.TITLE_TOO_LONG, 103, "Title too long (120 characters)") + } + if data.Content != nil && 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(*data.Content)), user.MaxContentLength())) + } + + if data.UserMessageID != nil { + msg, err := h.database.GetMessageByUserMessageID(ctx, *data.UserMessageID) + if err != nil { + return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query existing message") + } + if msg != nil { + return ginresp.JSON(http.StatusOK, response{ + Success: true, + ErrorID: apierr.NO_ERROR, + ErrorHighlight: 0, + Message: "Message already sent", + SuppressSend: true, + Response: "", + MessageCount: user.MessagesSent, + Quota: user.QuotaUsedToday(), + IsPro: user.IsPro, + QuotaMax: user.QuotaPerDay(), + SCNMessageID: msg.SCNMessageID, + }) + } + } + + if user.QuotaRemainingToday() <= 0 { + return ginresp.SendAPIError(403, apierr.QUOTA_REACHED, -1, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay())) + } + + channel, err := h.app.GetOrCreateChannel(ctx, *data.UserID, channelName) + if err != nil { + return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query/create channel") + } + + selfChanAdmin := *data.UserID == channel.OwnerUserID && *data.UserKey == user.AdminKey + selfChanSend := *data.UserID == channel.OwnerUserID && *data.UserKey == user.SendKey + forgChanSend := *data.UserID != channel.OwnerUserID && data.ChanKey != nil && *data.ChanKey == channel.SendKey + + if !selfChanAdmin && !selfChanSend && !forgChanSend { + return ginresp.SendAPIError(401, apierr.USER_AUTH_FAILED, 102, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay())) + } + + var sendTimestamp *time.Time = nil + if data.SendTimestamp != nil { + sendTimestamp = langext.Ptr(timeext.UnixFloatSeconds(*data.SendTimestamp)) + } + + priority := langext.Coalesce(data.Priority, 1) + + msg, err := h.database.CreateMessage(ctx, *data.UserID, channel, sendTimestamp, *data.Title, data.Content, priority, data.UserMessageID) + if err != nil { + return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create message in db") + } + + subscriptions, err := h.database.ListChannelSubscriptions(ctx, channel.ChannelID) + if err != nil { + return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions") + } + + for _, sub := range subscriptions { + clients, err := h.database.ListClients(ctx, sub.SubscriberUserID) + if err != nil { + return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query clients") + } + + for _, client := range clients { + + fcmDelivID, err := h.deliverMessage(ctx, client, msg) + if err != nil { + _, err = h.database.CreateRetryDelivery(ctx, client, msg) + if err != nil { + return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create delivery") + } + } else { + _, err = h.database.CreateSuccessDelivery(ctx, client, msg, *fcmDelivID) + if err != nil { + return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create delivery") + } + } + + } + } + + return ginresp.NotImplemented() +} + +func (h MessageHandler) deliverMessage(ctx *logic.AppContext, client models.Client, msg models.Message) (*string, error) { + if client.FCMToken != nil { + fcmDelivID, err := h.app.Firebase.SendNotification(ctx, client, msg) + if err != nil { + return nil, err + } + return langext.Ptr(fcmDelivID), nil + } else { + return langext.Ptr(""), nil } } diff --git a/server/api/router.go b/server/api/router.go index a0e79d9..2d53a93 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -117,7 +117,7 @@ func (r *Router) Init(e *gin.Engine) { apiv2.GET("/messages/:mid", ginresp.Wrap(r.apiHandler.GetMessage)) apiv2.DELETE("/messages/:mid", ginresp.Wrap(r.apiHandler.DeleteMessage)) - apiv2.POST("/messages", ginresp.Wrap(r.messageHandler.SendMessage)) + apiv2.POST("/messages", ginresp.Wrap(r.apiHandler.SendMessage)) } // ================ Send API ================ @@ -126,7 +126,7 @@ func (r *Router) Init(e *gin.Engine) { { sendAPI.POST("/", ginresp.Wrap(r.messageHandler.SendMessage)) sendAPI.POST("/send", ginresp.Wrap(r.messageHandler.SendMessage)) - sendAPI.POST("/send.php") + sendAPI.POST("/send.php", ginresp.Wrap(r.messageHandler.SendMessage)) } if r.app.Config.ReturnRawErrors { diff --git a/server/common/ginresp/resp.go b/server/common/ginresp/resp.go index deea489..a8ca0fd 100644 --- a/server/common/ginresp/resp.go +++ b/server/common/ginresp/resp.go @@ -85,8 +85,8 @@ func InternAPIError(status int, errorid apierr.APIError, msg string, e error) HT } } -func SendAPIError(errorid apierr.APIError, highlight int, msg string) HTTPResponse { - return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}} +func SendAPIError(status int, errorid apierr.APIError, highlight int, msg string) HTTPResponse { + return &errHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}} } func NotImplemented() HTTPResponse { diff --git a/server/db/methods.go b/server/db/methods.go index 39e5b5c..cc1495b 100644 --- a/server/db/methods.go +++ b/server/db/methods.go @@ -1,7 +1,7 @@ package db import ( - "blackforestbytes.com/simplecloudnotifier/api/models" + "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "gogs.mikescher.com/BlackForestBytes/goext/langext" "time" @@ -42,8 +42,8 @@ func (db *Database) CreateUser(ctx TxContext, readKey string, sendKey string, ad TimestampLastRead: nil, TimestampLastSent: nil, MessagesSent: 0, - QuotaToday: 0, - QuotaDay: nil, + QuotaUsed: 0, + QuotaUsedDay: nil, IsPro: protoken != nil, ProToken: protoken, }, nil @@ -254,3 +254,261 @@ func (db *Database) DeleteClient(ctx TxContext, clientid int64) error { return nil } + +func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*models.Message, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return nil, err + } + + rows, err := tx.QueryContext(ctx, "SELECT * FROM messages WHERE usr_message_id = ? LIMIT 1", usrMsgId) + if err != nil { + return nil, err + } + + msg, err := models.DecodeMessage(rows) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + return &msg, nil +} + +func (db *Database) GetChannelByName(ctx TxContext, userid int64, chanName string) (*models.Channel, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return nil, err + } + + rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE owner_user_id = ? OR name = ? LIMIT 1", userid, chanName) + if err != nil { + return nil, err + } + + channel, err := models.DecodeChannel(rows) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + return &channel, nil +} + +func (db *Database) CreateChannel(ctx TxContext, userid int64, name string, subscribeKey string, sendKey string) (models.Channel, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return models.Channel{}, err + } + + now := time.Now().UTC() + + res, err := tx.ExecContext(ctx, "INSERT INTO channels (owner_user_id, name, subscribe_key, send_key, timestamp_created) VALUES (?, ?, ?, ?, ?)", + userid, + name, + subscribeKey, + sendKey, + time2DB(now)) + if err != nil { + return models.Channel{}, err + } + + liid, err := res.LastInsertId() + if err != nil { + return models.Channel{}, err + } + + return models.Channel{ + ChannelID: liid, + OwnerUserID: userid, + Name: name, + SubscribeKey: subscribeKey, + SendKey: sendKey, + TimestampCreated: now, + TimestampLastRead: nil, + TimestampLastSent: nil, + MessagesSent: 0, + }, nil +} + +func (db *Database) CreateSubscribtion(ctx TxContext, subscriberUID int64, ownerUID int64, chanName string, chanID int64, confirmed bool) (models.Subscription, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return models.Subscription{}, err + } + + now := time.Now().UTC() + + res, err := tx.ExecContext(ctx, "INSERT INTO subscriptions (subscriber_user_id, channel_owner_user_id, channel_name, channel_id, timestamp_created, confirmed) VALUES (?, ?, ?, ?, ?, ?)", + subscriberUID, + ownerUID, + chanName, + chanID, + time2DB(now), + confirmed) + if err != nil { + return models.Subscription{}, err + } + + liid, err := res.LastInsertId() + if err != nil { + return models.Subscription{}, err + } + + return models.Subscription{ + SubscriptionID: liid, + SubscriberUserID: subscriberUID, + ChannelOwnerUserID: ownerUID, + ChannelID: chanID, + ChannelName: chanName, + TimestampCreated: now, + Confirmed: confirmed, + }, nil +} + +func (db *Database) CreateMessage(ctx TxContext, senderUserID int64, channel models.Channel, timestampSend *time.Time, title string, content *string, priority int, userMsgId *string) (models.Message, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return models.Message{}, err + } + + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?)", + senderUserID, + channel.OwnerUserID, + channel.Name, + channel.ChannelID, + time2DB(now), + time2DBOpt(timestampSend), + title, + content, + priority, + userMsgId) + if err != nil { + return models.Message{}, err + } + + liid, err := res.LastInsertId() + if err != nil { + return models.Message{}, err + } + + return models.Message{ + SCNMessageID: liid, + SenderUserID: senderUserID, + OwnerUserID: channel.OwnerUserID, + ChannelName: channel.Name, + ChannelID: channel.ChannelID, + TimestampReal: now, + TimestampClient: timestampSend, + Title: title, + Content: content, + Priority: priority, + UserMessageID: userMsgId, + }, nil +} + +func (db *Database) ListChannelSubscriptions(ctx TxContext, channelID int64) ([]models.Subscription, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return nil, err + } + + rows, err := tx.QueryContext(ctx, "SELECT * FROM subscriptions WHERE channel_id = ?", channelID) + if err != nil { + return nil, err + } + + data, err := models.DecodeSubscriptions(rows) + if err != nil { + return nil, err + } + + return data, nil +} + +func (db *Database) CreateRetryDelivery(ctx TxContext, client models.Client, msg models.Message) (models.Delivery, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return models.Delivery{}, err + } + + now := time.Now().UTC() + 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 (?, ?, ?, ?, ?, ?, ?)", + msg.SCNMessageID, + client.UserID, + client.ClientID, + time2DB(now), + nil, + models.DeliveryStatusRetry, + nil, + time2DB(next)) + if err != nil { + return models.Delivery{}, err + } + + liid, err := res.LastInsertId() + if err != nil { + return models.Delivery{}, err + } + + return models.Delivery{ + DeliveryID: liid, + SCNMessageID: msg.SCNMessageID, + ReceiverUserID: client.UserID, + ReceiverClientID: client.ClientID, + TimestampCreated: now, + TimestampFinalized: nil, + Status: models.DeliveryStatusRetry, + RetryCount: 0, + NextDelivery: langext.Ptr(next), + FCMMessageID: nil, + }, nil +} + +func (db *Database) CreateSuccessDelivery(ctx TxContext, client models.Client, msg models.Message, fcmDelivID string) (models.Delivery, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return models.Delivery{}, err + } + + 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 (?, ?, ?, ?, ?, ?, ?)", + msg.SCNMessageID, + client.UserID, + client.ClientID, + time2DB(now), + time2DB(now), + models.DeliveryStatusSuccess, + fcmDelivID, + nil) + if err != nil { + return models.Delivery{}, err + } + + liid, err := res.LastInsertId() + if err != nil { + return models.Delivery{}, err + } + + return models.Delivery{ + DeliveryID: liid, + SCNMessageID: msg.SCNMessageID, + ReceiverUserID: client.UserID, + ReceiverClientID: client.ClientID, + TimestampCreated: now, + TimestampFinalized: langext.Ptr(now), + Status: models.DeliveryStatusSuccess, + RetryCount: 0, + NextDelivery: nil, + FCMMessageID: langext.Ptr(fcmDelivID), + }, nil +} diff --git a/server/db/schema_3.ddl b/server/db/schema_3.ddl index 240bc62..59b1a2c 100644 --- a/server/db/schema_3.ddl +++ b/server/db/schema_3.ddl @@ -14,8 +14,8 @@ CREATE TABLE users messages_sent INTEGER NOT NULL DEFAULT '0', - quota_today INTEGER NOT NULL DEFAULT '0', - quota_day TEXT NULL DEFAULT NULL, + quota_used INTEGER NOT NULL DEFAULT '0', + quota_used_day TEXT NULL DEFAULT NULL, is_pro INTEGER CHECK(is_pro IN (0, 1)) NOT NULL DEFAULT 0, pro_token TEXT NULL DEFAULT NULL @@ -66,8 +66,11 @@ CREATE TABLE subscriptions subscriber_user_id INTEGER NOT NULL, channel_owner_user_id INTEGER NOT NULL, channel_name TEXT NOT NULL, + channel_id INTEGER NOT NULL, - confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL DEFAULT 0 + timestamp_created INTEGER NOT NULL, + + confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL ); CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_name); @@ -76,8 +79,8 @@ CREATE TABLE messages ( scn_message_id INTEGER PRIMARY KEY AUTOINCREMENT, sender_user_id INTEGER NOT NULL, + owner_user_id INTEGER NOT NULL, channel_name TEXT NOT NULL, - channel_id INTEGER NOT NULL, timestamp_real INTEGER NOT NULL, @@ -88,8 +91,8 @@ CREATE TABLE messages priority INTEGER CHECK(priority IN (0, 1, 2)) NOT NULL, usr_message_id TEXT NULL ); -CREATE INDEX "idx_messages_channel" ON messages (sender_user_id, channel_name); -CREATE INDEX "idx_messages_idempotency" ON messages (sender_user_id, usr_message_id); +CREATE INDEX "idx_messages_channel" ON messages (owner_user_id, channel_name); +CREATE INDEX "idx_messages_idempotency" ON messages (owner_user_id, usr_message_id); CREATE TABLE deliveries diff --git a/server/db/utils.go b/server/db/utils.go index 3258f51..2890b63 100644 --- a/server/db/utils.go +++ b/server/db/utils.go @@ -1,6 +1,9 @@ package db -import "time" +import ( + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "time" +) func bool2DB(b bool) int { if b { @@ -13,3 +16,10 @@ func bool2DB(b bool) int { func time2DB(t time.Time) int64 { return t.UnixMilli() } + +func time2DBOpt(t *time.Time) *int64 { + if t == nil { + return nil + } + return langext.Ptr(t.UnixMilli()) +} diff --git a/server/firebase/firebase.go b/server/firebase/firebase.go index bcfae00..c5caa11 100644 --- a/server/firebase/firebase.go +++ b/server/firebase/firebase.go @@ -1,12 +1,15 @@ package firebase import ( + "blackforestbytes.com/simplecloudnotifier/models" "context" _ "embed" fb "firebase.google.com/go" "firebase.google.com/go/messaging" "github.com/rs/zerolog/log" + "gogs.mikescher.com/BlackForestBytes/goext/langext" "google.golang.org/api/option" + "strconv" ) //go:embed scnserviceaccountkey.json @@ -43,26 +46,26 @@ type Notification struct { Priority int } -func (fb App) SendNotification(ctx context.Context, notification Notification) (string, error) { +func (fb App) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) { n := messaging.Message{ - Data: map[string]string{"scn_msg_id": notification.Id}, + Data: map[string]string{"scn_msg_id": strconv.FormatInt(msg.SCNMessageID, 10)}, Notification: &messaging.Notification{ - Title: notification.Title, - Body: notification.Body, + Title: msg.Title, + Body: langext.Coalesce(msg.Content, ""), }, Android: nil, APNS: nil, Webpush: nil, FCMOptions: nil, - Token: notification.Token, + Token: *client.FCMToken, Topic: "", Condition: "", } - if notification.Platform == "ios" { + if client.Type == models.ClientTypeIOS { n.APNS = nil } - if notification.Platform == "android" { + if client.Type == models.ClientTypeAndroid { n.Android = nil } diff --git a/server/go.mod b/server/go.mod index 0298115..9489431 100644 --- a/server/go.mod +++ b/server/go.mod @@ -9,7 +9,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.16 github.com/rs/zerolog v1.28.0 github.com/swaggo/swag v1.8.7 - gogs.mikescher.com/BlackForestBytes/goext v0.0.18 + gogs.mikescher.com/BlackForestBytes/goext v0.0.20 google.golang.org/api v0.103.0 ) diff --git a/server/go.sum b/server/go.sum index 2bd2c85..e77b88e 100644 --- a/server/go.sum +++ b/server/go.sum @@ -162,6 +162,12 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= gogs.mikescher.com/BlackForestBytes/goext v0.0.17 h1:jsfbvII7aa0SH9qY0fnXBdtNnQe1YY3DgXDThEwLICc= gogs.mikescher.com/BlackForestBytes/goext v0.0.17/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= +gogs.mikescher.com/BlackForestBytes/goext v0.0.18 h1:fprrLoAPGdI4ObveHR1DjiP9WhlTJppWtjqMA6ZkyS8= +gogs.mikescher.com/BlackForestBytes/goext v0.0.18/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= +gogs.mikescher.com/BlackForestBytes/goext v0.0.19 h1:IvCHlIHDviHQXntZFTNdV7qNq5yQnSEMxF8LA0Tf3IY= +gogs.mikescher.com/BlackForestBytes/goext v0.0.19/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= +gogs.mikescher.com/BlackForestBytes/goext v0.0.20 h1:HxJ0iZ838TQnp/a+/DNajdZjZkV43OsK4VbHarOiHTs= +gogs.mikescher.com/BlackForestBytes/goext v0.0.20/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= diff --git a/server/logic/application.go b/server/logic/application.go index 1df58c7..ad3593f 100644 --- a/server/logic/application.go +++ b/server/logic/application.go @@ -6,6 +6,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/firebase" + "blackforestbytes.com/simplecloudnotifier/models" "context" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" @@ -174,3 +175,31 @@ func (app *Application) getPermissions(ctx *AppContext, hdr string) (PermissionS return NewEmptyPermissions(), nil } + +func (app *Application) GetOrCreateChannel(ctx *AppContext, userid int64, chanName string) (models.Channel, error) { + chanName = strings.ToLower(strings.TrimSpace(chanName)) + + existingChan, err := app.Database.GetChannelByName(ctx, userid, chanName) + if err != nil { + return models.Channel{}, err + } + + if existingChan != nil { + return *existingChan, nil + } + + subscribeKey := app.GenerateRandomAuthKey() + sendKey := app.GenerateRandomAuthKey() + + newChan, err := app.Database.CreateChannel(ctx, userid, chanName, subscribeKey, sendKey) + if err != nil { + return models.Channel{}, err + } + + _, err = app.Database.CreateSubscribtion(ctx, userid, userid, newChan.Name, newChan.ChannelID, true) + if err != nil { + return models.Channel{}, err + } + + return newChan, nil +} diff --git a/server/api/models/channel.go b/server/models/channel.go similarity index 100% rename from server/api/models/channel.go rename to server/models/channel.go diff --git a/server/api/models/client.go b/server/models/client.go similarity index 100% rename from server/api/models/client.go rename to server/models/client.go diff --git a/server/api/models/compat.go b/server/models/compat.go similarity index 100% rename from server/api/models/compat.go rename to server/models/compat.go diff --git a/server/models/delivery.go b/server/models/delivery.go new file mode 100644 index 0000000..6aaa645 --- /dev/null +++ b/server/models/delivery.go @@ -0,0 +1,103 @@ +package models + +import ( + "database/sql" + "github.com/blockloop/scan" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "time" +) + +type DeliveryStatus string + +const ( + DeliveryStatusRetry DeliveryStatus = "RETRY" + DeliveryStatusSuccess DeliveryStatus = "SUCCESS" + DeliveryStatusFailed DeliveryStatus = "FAILED" +) + +type Delivery struct { + DeliveryID int64 + SCNMessageID int64 + ReceiverUserID int64 + ReceiverClientID int64 + TimestampCreated time.Time + TimestampFinalized *time.Time + Status DeliveryStatus + RetryCount int + NextDelivery *time.Time + FCMMessageID *string +} + +func (d Delivery) JSON() DeliveryJSON { + return DeliveryJSON{ + DeliveryID: d.DeliveryID, + SCNMessageID: d.SCNMessageID, + ReceiverUserID: d.ReceiverUserID, + ReceiverClientID: d.ReceiverClientID, + TimestampCreated: d.TimestampCreated.Format(time.RFC3339Nano), + TimestampFinalized: timeOptFmt(d.TimestampFinalized, time.RFC3339Nano), + Status: d.Status, + RetryCount: d.RetryCount, + NextDelivery: timeOptFmt(d.NextDelivery, time.RFC3339Nano), + FCMMessageID: d.FCMMessageID, + } +} + +type DeliveryJSON struct { + DeliveryID int64 `json:"delivery_id"` + SCNMessageID int64 `json:"scn_message_id"` + ReceiverUserID int64 `json:"receiver_user_id"` + ReceiverClientID int64 `json:"receiver_client_id"` + TimestampCreated string `json:"timestamp_created"` + TimestampFinalized *string `json:"tiestamp_finalized"` + Status DeliveryStatus `json:"status"` + RetryCount int `json:"retry_count"` + NextDelivery *string `json:"next_delivery"` + FCMMessageID *string `json:"fcm_message_id"` +} + +type DeliveryDB struct { + DeliveryID int64 `db:"delivery_id"` + SCNMessageID int64 `db:"scn_message_id"` + ReceiverUserID int64 `db:"receiver_user_id"` + ReceiverClientID int64 `db:"receiver_client_id"` + TimestampCreated int64 `db:"timestamp_created"` + TimestampFinalized *int64 `db:"tiestamp_finalized"` + Status DeliveryStatus `db:"status"` + RetryCount int `db:"retry_count"` + NextDelivery *int64 `db:"next_delivery"` + FCMMessageID *string `db:"fcm_message_id"` +} + +func (d DeliveryDB) Model() Delivery { + return Delivery{ + DeliveryID: d.DeliveryID, + SCNMessageID: d.SCNMessageID, + ReceiverUserID: d.ReceiverUserID, + ReceiverClientID: d.ReceiverClientID, + TimestampCreated: time.UnixMilli(d.TimestampCreated), + TimestampFinalized: timeOptFromMilli(d.TimestampFinalized), + Status: d.Status, + RetryCount: d.RetryCount, + NextDelivery: timeOptFromMilli(d.NextDelivery), + FCMMessageID: d.FCMMessageID, + } +} + +func DecodeDelivery(r *sql.Rows) (Delivery, error) { + var data DeliveryDB + err := scan.RowStrict(&data, r) + if err != nil { + return Delivery{}, err + } + return data.Model(), nil +} + +func DecodeDeliveries(r *sql.Rows) ([]Delivery, error) { + var data []DeliveryDB + err := scan.RowsStrict(&data, r) + if err != nil { + return nil, err + } + return langext.ArrMap(data, func(v DeliveryDB) Delivery { return v.Model() }), nil +} diff --git a/server/models/message.go b/server/models/message.go new file mode 100644 index 0000000..32c2f73 --- /dev/null +++ b/server/models/message.go @@ -0,0 +1,100 @@ +package models + +import ( + "database/sql" + "github.com/blockloop/scan" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "time" +) + +type Message struct { + SCNMessageID int64 + SenderUserID int64 + OwnerUserID int64 + ChannelName string + ChannelID int64 + TimestampReal time.Time + TimestampClient *time.Time + Title string + Content *string + Priority int + UserMessageID *string +} + +func (m Message) JSON() MessageJSON { + return MessageJSON{ + SCNMessageID: m.SCNMessageID, + SenderUserID: m.SenderUserID, + OwnerUserID: m.OwnerUserID, + ChannelName: m.ChannelName, + ChannelID: m.ChannelID, + Timestamp: langext.Coalesce(m.TimestampClient, m.TimestampReal).Format(time.RFC3339Nano), + Title: m.Title, + Content: m.Content, + Priority: m.Priority, + UserMessageID: m.UserMessageID, + Trimmed: false, + } +} + +type MessageJSON struct { + SCNMessageID int64 `json:"scn_message_id"` + SenderUserID int64 `json:"sender_user_id"` + OwnerUserID int64 `json:"owner_user_id"` + ChannelName string `json:"channel_name"` + ChannelID int64 `json:"channel_id"` + Timestamp string `json:"timestamp"` + Title string `json:"title"` + Content *string `json:"body"` + Priority int `json:"priority"` + UserMessageID *string `json:"usr_message_id"` + Trimmed bool `json:"trimmed"` +} + +type MessageDB struct { + SCNMessageID int64 `db:"scn_message_id"` + SenderUserID int64 `db:"sender_user_id"` + OwnerUserID int64 `db:"owner_user_id"` + ChannelName string `db:"channel_name"` + ChannelID int64 `db:"channel_id"` + TimestampReal int64 `db:"timestamp_real"` + TimestampClient *int64 `db:"timestamp_client"` + Title string `db:"title"` + Content *string `db:"content"` + Priority int `db:"priority"` + UserMessageID *string `db:"usr_message_id"` +} + +func (m MessageDB) Model() Message { + return Message{ + SCNMessageID: m.SCNMessageID, + SenderUserID: m.SenderUserID, + OwnerUserID: m.OwnerUserID, + ChannelName: m.ChannelName, + ChannelID: m.ChannelID, + TimestampReal: time.UnixMilli(m.TimestampReal), + TimestampClient: timeOptFromMilli(m.TimestampClient), + Title: m.Title, + Content: m.Content, + Priority: m.Priority, + UserMessageID: m.UserMessageID, + } +} + +func DecodeMessage(r *sql.Rows) (Message, error) { + var data MessageDB + err := scan.RowStrict(&data, r) + if err != nil { + return Message{}, err + } + return data.Model(), nil +} + +func DecodeMessages(r *sql.Rows) ([]Message, error) { + var data []MessageDB + err := scan.RowsStrict(&data, r) + if err != nil { + return nil, err + } + return langext.ArrMap(data, func(v MessageDB) Message { return v.Model() }), nil +} diff --git a/server/models/subscription.go b/server/models/subscription.go new file mode 100644 index 0000000..2730724 --- /dev/null +++ b/server/models/subscription.go @@ -0,0 +1,80 @@ +package models + +import ( + "database/sql" + "github.com/blockloop/scan" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "time" +) + +type Subscription struct { + SubscriptionID int64 + SubscriberUserID int64 + ChannelOwnerUserID int64 + ChannelID int64 + ChannelName string + TimestampCreated time.Time + Confirmed bool +} + +func (s Subscription) JSON() SubscriptionJSON { + return SubscriptionJSON{ + SubscriptionID: s.SubscriptionID, + SubscriberUserID: s.SubscriberUserID, + ChannelOwnerUserID: s.ChannelOwnerUserID, + ChannelID: s.ChannelID, + ChannelName: s.ChannelName, + TimestampCreated: s.TimestampCreated.Format(time.RFC3339Nano), + Confirmed: s.Confirmed, + } +} + +type SubscriptionJSON struct { + SubscriptionID int64 `json:"subscription_id"` + SubscriberUserID int64 `json:"subscriber_user_id"` + ChannelOwnerUserID int64 `json:"channel_owner_user_id"` + ChannelID int64 `json:"channel_id"` + ChannelName string `json:"channel_name"` + TimestampCreated string `json:"timestamp_created"` + Confirmed bool `json:"confirmed"` +} + +type SubscriptionDB struct { + SubscriptionID int64 `db:"subscription_id"` + SubscriberUserID int64 `db:"subscriber_user_id"` + ChannelOwnerUserID int64 `db:"channel_owner_user_id"` + ChannelID int64 `db:"channel_id"` + ChannelName string `db:"channel_name"` + TimestampCreated int64 `db:"timestamp_created"` + Confirmed int `db:"confirmed"` +} + +func (s SubscriptionDB) Model() Subscription { + return Subscription{ + SubscriptionID: s.SubscriptionID, + SubscriberUserID: s.SubscriberUserID, + ChannelOwnerUserID: s.ChannelOwnerUserID, + ChannelID: s.ChannelID, + ChannelName: s.ChannelName, + TimestampCreated: time.UnixMilli(s.TimestampCreated), + Confirmed: s.Confirmed != 0, + } +} + +func DecodeSubscription(r *sql.Rows) (Subscription, error) { + var data SubscriptionDB + err := scan.RowStrict(&data, r) + if err != nil { + return Subscription{}, err + } + return data.Model(), nil +} + +func DecodeSubscriptions(r *sql.Rows) ([]Subscription, error) { + var data []SubscriptionDB + err := scan.RowsStrict(&data, r) + if err != nil { + return nil, err + } + return langext.ArrMap(data, func(v SubscriptionDB) Subscription { return v.Model() }), nil +} diff --git a/server/api/models/user.go b/server/models/user.go similarity index 75% rename from server/api/models/user.go rename to server/models/user.go index 60f474f..9015b37 100644 --- a/server/api/models/user.go +++ b/server/models/user.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/blockloop/scan" "gogs.mikescher.com/BlackForestBytes/goext/langext" + "gogs.mikescher.com/BlackForestBytes/goext/timeext" "time" ) @@ -17,8 +18,8 @@ type User struct { TimestampLastRead *time.Time TimestampLastSent *time.Time MessagesSent int - QuotaToday int - QuotaDay *string + QuotaUsed int + QuotaUsedDay *string IsPro bool ProToken *string } @@ -34,12 +35,41 @@ func (u User) JSON() UserJSON { TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano), TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano), MessagesSent: u.MessagesSent, - QuotaToday: u.QuotaToday, - QuotaDay: u.QuotaDay, + QuotaUsed: u.QuotaUsed, + QuotaUsedDay: u.QuotaUsedDay, IsPro: u.IsPro, } } +func (u User) MaxContentLength() int { + if u.IsPro { + return 16384 + } else { + return 2048 + } +} + +func (u User) QuotaPerDay() int { + if u.IsPro { + return 1000 + } else { + return 50 + } +} + +func (u User) QuotaUsedToday() int { + now := time.Now().In(timeext.TimezoneBerlin).Format("2006-01-02") + if u.QuotaUsedDay != nil && *u.QuotaUsedDay == now { + return u.QuotaUsed + } else { + return 0 + } +} + +func (u User) QuotaRemainingToday() int { + return u.QuotaPerDay() - u.QuotaUsedToday() +} + type UserJSON struct { UserID int64 `json:"user_id"` Username *string `json:"username"` @@ -50,8 +80,8 @@ type UserJSON struct { TimestampLastRead *string `json:"timestamp_last_read"` TimestampLastSent *string `json:"timestamp_last_sent"` MessagesSent int `json:"messages_sent"` - QuotaToday int `json:"quota_today"` - QuotaDay *string `json:"quota_day"` + QuotaUsed int `json:"quota_used"` + QuotaUsedDay *string `json:"quota_used_day"` IsPro bool `json:"is_pro"` } @@ -65,8 +95,8 @@ type UserDB struct { TimestampLastRead *int64 `db:"timestamp_lastread"` TimestampLastSent *int64 `db:"timestamp_lastsent"` MessagesSent int `db:"messages_sent"` - QuotaToday int `db:"quota_today"` - QuotaDay *string `db:"quota_day"` + QuotaUsed int `db:"quota_used"` + QuotaUsedDay *string `db:"quota_used_day"` IsPro bool `db:"is_pro"` ProToken *string `db:"pro_token"` } @@ -82,8 +112,8 @@ func (u UserDB) Model() User { TimestampLastRead: timeOptFromMilli(u.TimestampLastRead), TimestampLastSent: timeOptFromMilli(u.TimestampLastSent), MessagesSent: u.MessagesSent, - QuotaToday: u.QuotaToday, - QuotaDay: u.QuotaDay, + QuotaUsed: u.QuotaUsed, + QuotaUsedDay: u.QuotaUsedDay, IsPro: u.IsPro, } } diff --git a/server/api/models/utils.go b/server/models/utils.go similarity index 100% rename from server/api/models/utils.go rename to server/models/utils.go diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index c18eb6a..d913745 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -9,6 +9,88 @@ "host": "scn.blackforestbytes.com", "basePath": "/", "paths": { + "/": { + "post": { + "summary": "Send a new message", + "parameters": [ + { + "type": "string", + "name": "message_content", + "in": "query" + }, + { + "type": "string", + "name": "message_title", + "in": "query" + }, + { + "type": "integer", + "name": "priority", + "in": "query" + }, + { + "type": "integer", + "name": "sendTimestamp", + "in": "query" + }, + { + "type": "string", + "name": "userMessageID", + "in": "query" + }, + { + "type": "string", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "name": "user_key", + "in": "query" + }, + { + "description": " ", + "name": "post_body", + "in": "body", + "schema": { + "$ref": "#/definitions/handler.SendMessage.body" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ClientJSON" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + } + }, "/api-v2/user/": { "post": { "summary": "Create a new user", @@ -688,6 +770,88 @@ } } } + }, + "/send": { + "post": { + "summary": "Send a new message", + "parameters": [ + { + "type": "string", + "name": "message_content", + "in": "query" + }, + { + "type": "string", + "name": "message_title", + "in": "query" + }, + { + "type": "integer", + "name": "priority", + "in": "query" + }, + { + "type": "integer", + "name": "sendTimestamp", + "in": "query" + }, + { + "type": "string", + "name": "userMessageID", + "in": "query" + }, + { + "type": "string", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "name": "user_key", + "in": "query" + }, + { + "description": " ", + "name": "post_body", + "in": "body", + "schema": { + "$ref": "#/definitions/handler.SendMessage.body" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ClientJSON" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + } } }, "definitions": { @@ -894,6 +1058,32 @@ } } }, + "handler.SendMessage.body": { + "type": "object", + "properties": { + "message_content": { + "type": "string" + }, + "message_title": { + "type": "string" + }, + "priority": { + "type": "integer" + }, + "sendTimestamp": { + "type": "integer" + }, + "userMessageID": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "user_key": { + "type": "string" + } + } + }, "handler.Update.response": { "type": "object", "properties": { diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index 10c8f22..5d92417 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -132,6 +132,23 @@ definitions: success: type: string type: object + handler.SendMessage.body: + properties: + message_content: + type: string + message_title: + type: string + priority: + type: integer + sendTimestamp: + type: integer + user_id: + type: string + user_key: + type: string + userMessageID: + type: string + type: object handler.Update.response: properties: is_pro: @@ -272,6 +289,57 @@ info: title: SimpleCloudNotifier API version: "2.0" paths: + /: + post: + parameters: + - in: query + name: message_content + type: string + - in: query + name: message_title + type: string + - in: query + name: priority + type: integer + - in: query + name: sendTimestamp + type: integer + - in: query + name: userMessageID + type: string + - in: query + name: user_id + type: string + - in: query + name: user_key + type: string + - description: ' ' + in: body + name: post_body + schema: + $ref: '#/definitions/handler.SendMessage.body' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ClientJSON' + "400": + description: Bad Request + schema: + $ref: '#/definitions/ginresp.apiError' + "401": + description: Unauthorized + 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 /api-v2/user/: post: operationId: api-user-create @@ -720,4 +788,55 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/ginresp.apiError' + /send: + post: + parameters: + - in: query + name: message_content + type: string + - in: query + name: message_title + type: string + - in: query + name: priority + type: integer + - in: query + name: sendTimestamp + type: integer + - in: query + name: userMessageID + type: string + - in: query + name: user_id + type: string + - in: query + name: user_key + type: string + - description: ' ' + in: body + name: post_body + schema: + $ref: '#/definitions/handler.SendMessage.body' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ClientJSON' + "400": + description: Bad Request + schema: + $ref: '#/definitions/ginresp.apiError' + "401": + description: Unauthorized + 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 swagger: "2.0"