diff --git a/server/README.md b/server/README.md index d33efea..7751bd3 100644 --- a/server/README.md +++ b/server/README.md @@ -3,11 +3,11 @@ //TODO - - POST::/messages - - https://firebase.google.com/docs/cloud-messaging/send-message#rest !! - List subscriptions on all owned channels (RESTful?) - - deploy - - 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 \ No newline at end of file + + - full-text-search: https://www.sqlite.org/fts5.html#contentless_tables + + - deploy + diff --git a/server/api/handler/api.go b/server/api/handler/api.go index 626fed4..1a030fa 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -8,10 +8,14 @@ import ( "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "fmt" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/mathext" + "gogs.mikescher.com/BlackForestBytes/goext/timeext" "net/http" + "strings" + "time" ) type APIHandler struct { @@ -61,17 +65,17 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { } else if b.ClientType == string(models.ClientTypeIOS) { clientType = models.ClientTypeIOS } else { - return ginresp.InternAPIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) + return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) } if b.ProToken != nil { ptok, err := h.app.VerifyProToken(*b.ProToken) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) + return ginresp.APIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) } if !ptok { - return ginresp.InternAPIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) + return ginresp.APIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) } } @@ -81,13 +85,13 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { err := h.database.ClearFCMTokens(ctx, b.FCMToken) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } if b.ProToken != nil { err := h.database.ClearProTokens(ctx, *b.ProToken) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } } @@ -98,12 +102,12 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { userobj, err := h.database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, username) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } _, err = h.database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSON())) @@ -141,10 +145,10 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { user, err := h.database.GetUser(ctx, u.UserID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err) + return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSON())) @@ -194,34 +198,34 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { err := h.database.UpdateUserUsername(ctx, u.UserID, b.Username) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) } } if b.ProToken != nil { ptok, err := h.app.VerifyProToken(*b.ProToken) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) + return ginresp.APIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) } if !ptok { - return ginresp.InternAPIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) + return ginresp.APIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) } err = h.database.ClearProTokens(ctx, *b.ProToken) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } err = h.database.UpdateUserProToken(ctx, u.UserID, b.ProToken) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) } } user, err := h.database.GetUser(ctx, u.UserID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSON())) @@ -262,7 +266,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { clients, err := h.database.ListClients(ctx, u.UserID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query clients", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query clients", err) } res := langext.ArrMap(clients, func(v models.Client) models.ClientJSON { return v.JSON() }) @@ -304,10 +308,10 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse { client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) + return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON())) @@ -354,7 +358,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { } else if b.ClientType == string(models.ClientTypeIOS) { clientType = models.ClientTypeIOS } else { - return ginresp.InternAPIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) + return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) } if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { @@ -363,7 +367,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { client, err := h.database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON())) @@ -403,15 +407,15 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) + return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) } err = h.database.DeleteClient(ctx, u.ClientID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON())) @@ -452,7 +456,7 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse { clients, err := h.database.ListChannels(ctx, u.UserID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) } res := langext.ArrMap(clients, func(v models.Channel) models.ChannelJSON { return v.JSON() }) @@ -494,10 +498,10 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON())) @@ -558,33 +562,33 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse { channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } userid := *ctx.GetPermissionUserID() sub, err := h.database.GetSubscriptionBySubscriber(ctx, userid, channel.ChannelID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } if !sub.Confirmed { - return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } tok, err := cursortoken.Decode(langext.Coalesce(q.NextPageToken, "")) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) + return ginresp.APIError(g, 500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) } messages, npt, err := h.database.ListChannelMessages(ctx, channel.ChannelID, pageSize, tok) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) } var res []models.MessageJSON @@ -632,7 +636,7 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse { clients, err := h.database.ListSubscriptionsByOwner(ctx, u.UserID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) } res := langext.ArrMap(clients, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) @@ -677,15 +681,15 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) } clients, err := h.database.ListSubscriptionsByChannel(ctx, u.ChannelID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) } res := langext.ArrMap(clients, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) @@ -727,14 +731,14 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse { subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) + return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } if subscription.SubscriberUserID != u.UserID { - return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) @@ -774,19 +778,19 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse { subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) + return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID { - return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } err = h.database.DeleteSubscription(ctx, u.SubscriptionID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete subscription", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) @@ -835,19 +839,19 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse { channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, h.app.NormalizeChannelName(b.Channel)) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } if channel == nil { - return ginresp.InternAPIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) { - ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + ginresp.APIError(g, 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) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, sub.JSON())) @@ -891,26 +895,26 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse { subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) + return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } if subscription.ChannelOwnerUserID != u.UserID { - return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } if b.Confirmed != nil { err = h.database.UpdateSubscriptionConfirmed(ctx, u.SubscriptionID, *b.Confirmed) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err) } } subscription, err = h.database.GetSubscription(ctx, u.SubscriptionID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) @@ -968,17 +972,17 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse { tok, err := cursortoken.Decode(langext.Coalesce(q.NextPageToken, "")) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) + return ginresp.APIError(g, 500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) } err = h.database.UpdateUserLastRead(ctx, userid) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to update last-read", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update last-read", err) } messages, npt, err := h.database.ListMessages(ctx, userid, pageSize, tok) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) } var res []models.MessageJSON @@ -1026,10 +1030,10 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { msg, err := h.database.GetMessage(ctx, u.MessageID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) + return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) } if !ctx.CheckPermissionMessageReadDirect(msg) { @@ -1040,21 +1044,21 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { if uid := ctx.GetPermissionUserID(); uid != nil && ctx.IsPermissionUserRead() { sub, err := h.database.GetSubscriptionBySubscriber(ctx, *uid, msg.ChannelID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } if sub == nil { // not subbed - return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } if !sub.Confirmed { // sub not confirmed - return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } // => perm okay } else { // auth-key is not set or not a user:x variant - return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } } @@ -1095,29 +1099,185 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { msg, err := h.database.GetMessage(ctx, u.MessageID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) + return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) } if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) } if !ctx.CheckPermissionMessageReadDirect(msg) { - return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } err = h.database.DeleteMessage(ctx, msg.SCNMessageID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete message", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete message", err) } err = h.database.CancelPendingDeliveries(ctx, msg.SCNMessageID) if err != nil { - return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to cancel deliveries", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to cancel deliveries", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON())) } -func (h APIHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented(g) //TODO +// CreateMessage swaggerdoc +// +// @Summary Create a new message +// @Description This is similar to the main route `POST -> https://scn.blackfrestbytes.com/` +// @Description But this route can change in the future, for long-living scripts etc. it's better to use the normal POST route +// @ID api-messages-create +// +// @Param post_data query handler.CreateMessage.body false " " +// +// @Success 200 {object} models.MessageJSON +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api-v2/messages [POST] +func (h APIHandler) CreateMessage(g *gin.Context) ginresp.HTTPResponse { + type body struct { + Channel *string `json:"channel"` + ChanKey *string `json:"chan_key"` + Title *string `json:"title"` + Content *string `json:"content"` + Priority *int `json:"priority"` + UserMessageID *string `json:"msg_id"` + SendTimestamp *float64 `json:"timestamp"` + } + + var b body + ctx, errResp := h.app.StartRequest(g, nil, nil, &b, nil) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + if permResp := ctx.CheckPermissionSend(); permResp != nil { + return *permResp + } + + userID := *ctx.GetPermissionUserID() + + if b.Title != nil { + b.Title = langext.Ptr(strings.TrimSpace(*b.Title)) + } + if b.UserMessageID != nil { + b.UserMessageID = langext.Ptr(strings.TrimSpace(*b.UserMessageID)) + } + + if b.Title == nil { + return ginresp.APIError(g, 400, apierr.MISSING_TITLE, "Missing parameter [[title]]", nil) + } + if b.SendTimestamp != nil && mathext.Abs(*b.SendTimestamp-float64(time.Now().Unix())) > (24*time.Hour).Seconds() { + return ginresp.SendAPIError(g, 400, apierr.TIMESTAMP_OUT_OF_RANGE, -1, "The timestamp mus be within 24 hours of now()", nil) + } + if b.Priority != nil && (*b.Priority != 0 && *b.Priority != 1 && *b.Priority != 2) { + return ginresp.SendAPIError(g, 400, apierr.INVALID_PRIO, 105, "Invalid priority", nil) + } + if len(*b.Title) == 0 { + return ginresp.SendAPIError(g, 400, apierr.NO_TITLE, 103, "No title specified", nil) + } + if b.UserMessageID != nil && len(*b.UserMessageID) > 64 { + return ginresp.SendAPIError(g, 400, apierr.USR_MSG_ID_TOO_LONG, -1, "MessageID too long (64 characters)", nil) + } + + channelName := h.app.DefaultChannel + if b.Channel != nil { + channelName = h.app.NormalizeChannelName(*b.Channel) + } + + user, err := h.database.GetUser(ctx, userID) + if err == sql.ErrNoRows { + return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, -1, "User not found", nil) + } + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query user", err) + } + + if len(*b.Title) > user.MaxTitleLength() { + return ginresp.SendAPIError(g, 400, apierr.TITLE_TOO_LONG, 103, fmt.Sprintf("Title too long (max %d characters)", user.MaxTitleLength()), nil) + } + if b.Content != nil && len(*b.Content) > user.MaxContentLength() { + return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, 104, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*b.Content), user.MaxContentLength()), nil) + } + + if b.UserMessageID != nil { + msg, err := h.database.GetMessageByUserMessageID(ctx, *b.UserMessageID) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query existing message", err) + } + if msg != nil { + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON())) + } + } + + if user.QuotaRemainingToday() <= 0 { + return ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, -1, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil) + } + + channel, err := h.app.GetOrCreateChannel(ctx, userID, channelName) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query/create channel", err) + } + + var sendTimestamp *time.Time = nil + if b.SendTimestamp != nil { + sendTimestamp = langext.Ptr(timeext.UnixFloatSeconds(*b.SendTimestamp)) + } + + priority := langext.Coalesce(b.Priority, 1) + + msg, err := h.database.CreateMessage(ctx, userID, channel, sendTimestamp, *b.Title, b.Content, priority, b.UserMessageID) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to create message in db", err) + } + + subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions", err) + } + + err = h.database.IncUserMessageCounter(ctx, user) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to inc user msg-counter", err) + } + + err = h.database.IncChannelMessageCounter(ctx, channel) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to inc channel msg-counter", err) + } + + for _, sub := range subscriptions { + clients, err := h.database.ListClients(ctx, sub.SubscriberUserID) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query clients", err) + } + + if !sub.Confirmed { + continue + } + + for _, client := range clients { + + fcmDelivID, err := h.app.DeliverMessage(ctx, client, msg) + if err != nil { + _, err = h.database.CreateRetryDelivery(ctx, client, msg) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to create delivery", err) + } + } else { + _, err = h.database.CreateSuccessDelivery(ctx, client, msg, *fcmDelivID) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to create delivery", err) + } + } + + } + } + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON())) } diff --git a/server/api/handler/message.go b/server/api/handler/message.go index 0d39a57..a3fcb05 100644 --- a/server/api/handler/message.go +++ b/server/api/handler/message.go @@ -88,6 +88,7 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse { // // @Param query_data query handler.SendMessage.query false " " // @Param post_body body handler.SendMessage.body false " " +// @Param form_body formData handler.SendMessage.body false " " // // @Success 200 {object} handler.sendMessageInternal.response // @Failure 400 {object} ginresp.apiError @@ -114,23 +115,35 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { UserID *int64 `json:"user_id"` UserKey *string `json:"user_key"` Channel *string `json:"channel"` - ChanKey *string `form:"chan_key"` + ChanKey *string `json:"chan_key"` Title *string `json:"title"` Content *string `json:"content"` Priority *int `json:"priority"` UserMessageID *string `json:"msg_id"` SendTimestamp *float64 `json:"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 b body var q query - ctx, errResp := h.app.StartRequest(g, nil, &q, &b, nil) + var f form + ctx, errResp := h.app.StartRequest(g, nil, &q, &b, &f) if errResp != nil { return *errResp } defer ctx.Cancel() - data := dataext.ObjectMerge(b, q) + data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q) return h.sendMessageInternal(g, ctx, data.UserID, data.UserKey, data.Channel, data.ChanKey, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp) @@ -161,10 +174,10 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex return ginresp.SendAPIError(g, 400, apierr.MISSING_UID, 101, "Missing parameter [[user_id]]", nil) } if UserKey == nil { - return ginresp.SendAPIError(g, 400, apierr.MISSING_UID, 102, "Missing parameter [[user_token]]", nil) + return ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, 102, "Missing parameter [[user_token]]", nil) } if Title == nil { - return ginresp.SendAPIError(g, 400, apierr.MISSING_UID, 103, "Missing parameter [[title]]", nil) + return ginresp.SendAPIError(g, 400, apierr.MISSING_TITLE, 103, "Missing parameter [[title]]", nil) } if SendTimestamp != nil && mathext.Abs(*SendTimestamp-float64(time.Now().Unix())) > (24*time.Hour).Seconds() { return ginresp.SendAPIError(g, 400, apierr.TIMESTAMP_OUT_OF_RANGE, -1, "The timestamp mus be within 24 hours of now()", nil) @@ -192,8 +205,8 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query user", err) } - if len(*Title) > 120 { - return ginresp.SendAPIError(g, 400, apierr.TITLE_TOO_LONG, 103, "Title too long (120 characters)", nil) + if len(*Title) > user.MaxTitleLength() { + return ginresp.SendAPIError(g, 400, apierr.TITLE_TOO_LONG, 103, fmt.Sprintf("Title too long (max %d characters)", user.MaxTitleLength()), nil) } if Content != nil && len(*Content) > user.MaxContentLength() { return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, 104, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*Content), user.MaxContentLength()), nil) diff --git a/server/api/router.go b/server/api/router.go index 1b63702..4f9b2b6 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -118,7 +118,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.apiHandler.SendMessage)) + apiv2.POST("/messages", ginresp.Wrap(r.apiHandler.CreateMessage)) } // ================ Send API ================ diff --git a/server/common/ginresp/resp.go b/server/common/ginresp/resp.go index 46a82ee..59388d5 100644 --- a/server/common/ginresp/resp.go +++ b/server/common/ginresp/resp.go @@ -69,8 +69,8 @@ func InternalError(e error) HTTPResponse { return createApiError(nil, "InternalError", 500, apierr.INTERNAL_EXCEPTION, 0, e.Error(), e) } -func InternAPIError(g *gin.Context, status int, errorid apierr.APIError, msg string, e error) HTTPResponse { - return createApiError(g, "InternAPIError", status, errorid, 0, msg, e) +func APIError(g *gin.Context, status int, errorid apierr.APIError, msg string, e error) HTTPResponse { + return createApiError(g, "APIError", status, errorid, 0, msg, e) } func SendAPIError(g *gin.Context, status int, errorid apierr.APIError, highlight int, msg string, e error) HTTPResponse { diff --git a/server/logic/appcontext.go b/server/logic/appcontext.go index bc58e36..034fe68 100644 --- a/server/logic/appcontext.go +++ b/server/logic/appcontext.go @@ -76,7 +76,7 @@ func (ac *AppContext) FinishSuccess(res ginresp.HTTPResponse) ginresp.HTTPRespon if ac.transaction != nil { err := ac.transaction.Commit() if err != nil { - return ginresp.InternAPIError(ac.ginContext, 500, apierr.COMMIT_FAILED, "Failed to comit changes to DB", err) + return ginresp.APIError(ac.ginContext, 500, apierr.COMMIT_FAILED, "Failed to comit changes to DB", err) } ac.transaction = nil } diff --git a/server/logic/application.go b/server/logic/application.go index 8c09aa1..b9a7da5 100644 --- a/server/logic/application.go +++ b/server/logic/application.go @@ -122,25 +122,25 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an if uri != nil { if err := g.ShouldBindUri(uri); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err)) + return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err)) } } if query != nil { if err := g.ShouldBindQuery(query); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err)) + return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err)) } } if body != nil && g.Request.Header.Get("Content-Type") == "application/javascript" { if err := g.ShouldBindJSON(body); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read body", err)) + return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read body", err)) } } if form != nil && g.Request.Header.Get("Content-Type") == "multipart/form-data" { if err := g.ShouldBindWith(form, binding.Form); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read multipart-form", err)) + return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read multipart-form", err)) } } @@ -152,7 +152,7 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an perm, err := app.getPermissions(actx, authheader) if err != nil { cancel() - return nil, langext.Ptr(ginresp.InternAPIError(g, 400, apierr.PERM_QUERY_FAIL, "Failed to determine permissions", err)) + return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.PERM_QUERY_FAIL, "Failed to determine permissions", err)) } actx.permissions = perm diff --git a/server/logic/permissions.go b/server/logic/permissions.go index c524da9..399509b 100644 --- a/server/logic/permissions.go +++ b/server/logic/permissions.go @@ -37,7 +37,7 @@ func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPRespons return nil } - return langext.Ptr(ginresp.InternAPIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) + return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) } func (ac *AppContext) CheckPermissionRead() *ginresp.HTTPResponse { @@ -49,7 +49,7 @@ func (ac *AppContext) CheckPermissionRead() *ginresp.HTTPResponse { return nil } - return langext.Ptr(ginresp.InternAPIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) + return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) } func (ac *AppContext) CheckPermissionUserAdmin(userid int64) *ginresp.HTTPResponse { @@ -58,13 +58,25 @@ func (ac *AppContext) CheckPermissionUserAdmin(userid int64) *ginresp.HTTPRespon return nil } - return langext.Ptr(ginresp.InternAPIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) + return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) +} + +func (ac *AppContext) CheckPermissionSend() *ginresp.HTTPResponse { + p := ac.permissions + if p.UserID != nil && p.KeyType == PermKeyTypeUserSend { + return nil + } + if p.UserID != nil && p.KeyType == PermKeyTypeUserAdmin { + return nil + } + + return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) } func (ac *AppContext) CheckPermissionAny() *ginresp.HTTPResponse { p := ac.permissions if p.KeyType == PermKeyTypeNone { - return langext.Ptr(ginresp.InternAPIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) + return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) } return nil diff --git a/server/models/user.go b/server/models/user.go index e68eba6..f80699b 100644 --- a/server/models/user.go +++ b/server/models/user.go @@ -49,6 +49,10 @@ func (u User) MaxContentLength() int { } } +func (u User) MaxTitleLength() int { + return 120 +} + func (u User) QuotaPerDay() int { if u.IsPro { return 1000