CreateMessage()

This commit is contained in:
Mike Schwörer 2022-11-20 20:34:18 +01:00
parent d30e2cefc0
commit d46601be5c
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
9 changed files with 284 additions and 95 deletions

View File

@ -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
- full-text-search: https://www.sqlite.org/fts5.html#contentless_tables
- deploy

View File

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

View File

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

View File

@ -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 ================

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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