diff --git a/server/api/apierr/enums.go b/server/api/apierr/enums.go index e0d2728..ecf1d71 100644 --- a/server/api/apierr/enums.go +++ b/server/api/apierr/enums.go @@ -22,9 +22,11 @@ const ( USR_MSG_ID_TOO_LONG APIError = 1204 TIMESTAMP_OUT_OF_RANGE APIError = 1205 - USER_NOT_FOUND APIError = 1301 - CLIENT_NOT_FOUND APIError = 1302 - USER_AUTH_FAILED APIError = 1311 + USER_NOT_FOUND APIError = 1301 + CLIENT_NOT_FOUND APIError = 1302 + CHANNEL_NOT_FOUND APIError = 1303 + SUBSCRIPTION_NOT_FOUND APIError = 1304 + USER_AUTH_FAILED APIError = 1311 NO_DEVICE_LINKED APIError = 1401 diff --git a/server/api/handler/api.go b/server/api/handler/api.go index 684f5c0..dfdb34e 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -36,7 +36,7 @@ func NewAPIHandler(app *logic.Application) APIHandler { // @Failure 400 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError // -// @Router /api-v2/user/ [POST] +// @Router /api-v2/users/ [POST] func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { type body struct { FCMToken string `json:"fcm_token"` @@ -116,7 +116,7 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { // @Failure 404 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError // -// @Router /api-v2/user/{uid} [GET] +// @Router /api-v2/users/{uid} [GET] func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { type uri struct { UserID int64 `uri:"uid"` @@ -158,7 +158,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { // @Failure 404 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError // -// @Router /api-v2/user/{uid} [PATCH] +// @Router /api-v2/users/{uid} [PATCH] func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { type uri struct { UserID int64 `uri:"uid"` @@ -228,18 +228,18 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { // // @Param uid path int true "UserID" // -// @Success 200 {object} handler.ListClients.result +// @Success 200 {object} handler.ListClients.response // @Failure 400 {object} ginresp.apiError // @Failure 401 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError // -// @Router /api-v2/user/{uid}/clients [GET] +// @Router /api-v2/users/{uid}/clients [GET] func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { type uri struct { UserID int64 `uri:"uid"` } - type result struct { + type response struct { Clients []models.ClientJSON `json:"clients"` } @@ -261,7 +261,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { res := langext.ArrMap(clients, func(v models.Client) models.ClientJSON { return v.JSON() }) - return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, result{Clients: res})) + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Clients: res})) } // GetClient swaggerdoc @@ -278,7 +278,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { // @Failure 404 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError // -// @Router /api-v2/user/{uid}/clients/{cid} [GET] +// @Router /api-v2/users/{uid}/clients/{cid} [GET] func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse { type uri struct { UserID int64 `uri:"uid"` @@ -322,7 +322,7 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse { // @Failure 404 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError // -// @Router /api-v2/user/{uid}/clients [POST] +// @Router /api-v2/users/{uid}/clients [POST] func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { type uri struct { UserID int64 `uri:"uid"` @@ -377,7 +377,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { // @Failure 404 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError // -// @Router /api-v2/user/{uid}/clients [POST] +// @Router /api-v2/users/{uid}/clients [POST] func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { type uri struct { UserID int64 `uri:"uid"` @@ -411,38 +411,299 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON())) } +// ListChannels swaggerdoc +// +// @Summary List all channels of a user +// @ID api-channels-list +// +// @Param uid path int true "UserID" +// +// @Success 200 {object} handler.ListChannels.response +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api-v2/users/{uid}/channels [GET] func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() + type uri struct { + UserID int64 `uri:"uid"` + } + type response struct { + Channels []models.ChannelJSON `json:"channels"` + } + + var u uri + ctx, errResp := h.app.StartRequest(g, &u, nil, nil) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } + + clients, err := h.database.ListChannels(ctx, u.UserID) + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + + res := langext.ArrMap(clients, func(v models.Channel) models.ChannelJSON { return v.JSON() }) + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Channels: res})) } +// GetChannel swaggerdoc +// +// @Summary List all channels of a user +// @ID api-channels-get +// +// @Param uid path int true "UserID" +// @Param cid path int true "ChannelID" +// +// @Success 200 {object} models.ChannelJSON +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api-v2/users/{uid}/channels/{cid} [GET] func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() + type uri struct { + UserID int64 `uri:"uid"` + ChannelID int64 `uri:"cid"` + } + + var u uri + ctx, errResp := h.app.StartRequest(g, &u, nil, nil) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } + + channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) + if err == sql.ErrNoRows { + return ginresp.InternAPIError(404, apierr.CLIENT_NOT_FOUND, "Channel not found", err) + } + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) + } + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON())) } func (h APIHandler) GetChannelMessages(g *gin.Context) ginresp.HTTPResponse { return ginresp.NotImplemented() } +// ListUserSubscriptions swaggerdoc +// +// @Summary List all channels of a user +// @ID api-user-subscriptions-list +// +// @Param uid path int true "UserID" +// +// @Success 200 {object} handler.ListUserSubscriptions.response +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api-v2/users/{uid}/subscriptions [GET] func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() + type uri struct { + UserID int64 `uri:"uid"` + } + type response struct { + Subscriptions []models.SubscriptionJSON `json:"subscriptions"` + } + + var u uri + ctx, errResp := h.app.StartRequest(g, &u, nil, nil) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } + + clients, err := h.database.ListSubscriptionsByOwner(ctx, u.UserID) + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + + res := langext.ArrMap(clients, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Subscriptions: res})) } +// ListChannelSubscriptions swaggerdoc +// +// @Summary List all subscriptions of a channel +// @ID api-chan-subscriptions-list +// +// @Param uid path int true "UserID" +// @Param cid path int true "ChannelID" +// +// @Success 200 {object} handler.ListChannelSubscriptions.response +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api-v2/users/{uid}/channels/{cid}/subscriptions [GET] func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() + type uri struct { + UserID int64 `uri:"uid"` + ChannelID int64 `uri:"cid"` + } + type response struct { + Subscriptions []models.SubscriptionJSON `json:"subscriptions"` + } + + var u uri + ctx, errResp := h.app.StartRequest(g, &u, nil, nil) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } + + _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) + if err == sql.ErrNoRows { + return ginresp.InternAPIError(404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + } + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + + clients, err := h.database.ListSubscriptionsByChannel(ctx, u.ChannelID) + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + + res := langext.ArrMap(clients, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Subscriptions: res})) } +// GetSubscription swaggerdoc +// +// @Summary Get a single subscription +// @ID api-subscriptions-get +// +// @Param uid path int true "UserID" +// @Param sid path int true "SubscriptionID" +// +// @Success 200 {object} models.SubscriptionJSON +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api-v2/users/{uid}/subscriptions/{sid} [GET] func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() + type uri struct { + UserID int64 `uri:"uid"` + SubscriptionID int64 `uri:"sid"` + } + + var u uri + ctx, errResp := h.app.StartRequest(g, &u, nil, nil) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } + + subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) + if err == sql.ErrNoRows { + return ginresp.InternAPIError(404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) + } + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) + } + + if subscription.SubscriberUserID != u.UserID { + return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + } + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) } +// CancelSubscription swaggerdoc +// +// @Summary Cancel (delete) subscription +// @ID api-subscriptions-delete +// +// @Param uid path int true "UserID" +// @Param sid path int true "SubscriptionID" +// +// @Success 200 {object} models.SubscriptionJSON +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api-v2/users/{uid}/subscriptions/{sid} [DELETE] func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() + type uri struct { + UserID int64 `uri:"uid"` + SubscriptionID int64 `uri:"sid"` + } + + var u uri + ctx, errResp := h.app.StartRequest(g, &u, nil, nil) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } + + subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) + if err == sql.ErrNoRows { + return ginresp.InternAPIError(404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) + } + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) + } + + if subscription.SubscriberUserID != u.UserID { + return ginresp.InternAPIError(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(500, apierr.DATABASE_ERROR, "Failed to delete subscription", err) + } + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) } func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse { return ginresp.NotImplemented() } +func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse { return ginresp.NotImplemented() } diff --git a/server/api/handler/message.go b/server/api/handler/message.go index 6f584fc..627f928 100644 --- a/server/api/handler/message.go +++ b/server/api/handler/message.go @@ -76,7 +76,6 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { 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"` @@ -145,10 +144,9 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { return ginresp.JSON(http.StatusOK, response{ Success: true, ErrorID: apierr.NO_ERROR, - ErrorHighlight: 0, + ErrorHighlight: -1, Message: "Message already sent", SuppressSend: true, - Response: "", MessageCount: user.MessagesSent, Quota: user.QuotaUsedToday(), IsPro: user.IsPro, @@ -187,7 +185,7 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create message in db") } - subscriptions, err := h.database.ListChannelSubscriptions(ctx, channel.ChannelID) + subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID) if err != nil { return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions") } @@ -216,7 +214,18 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { } } - return ginresp.NotImplemented() + return ginresp.JSON(http.StatusOK, response{ + Success: true, + ErrorID: apierr.NO_ERROR, + ErrorHighlight: -1, + Message: "Message sent", + SuppressSend: false, + MessageCount: user.MessagesSent, + Quota: user.QuotaUsedToday(), + IsPro: user.IsPro, + QuotaMax: user.QuotaPerDay(), + SCNMessageID: msg.SCNMessageID, + }) } func (h MessageHandler) deliverMessage(ctx *logic.AppContext, client models.Client, msg models.Message) (*string, error) { diff --git a/server/api/router.go b/server/api/router.go index 2d53a93..ca099d1 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -94,24 +94,25 @@ func (r *Router) Init(e *gin.Engine) { apiv2 := e.Group("/api-v2/") { - apiv2.POST("/user/", ginresp.Wrap(r.apiHandler.CreateUser)) - apiv2.GET("/user/:uid", ginresp.Wrap(r.apiHandler.GetUser)) - apiv2.PATCH("/user/:uid", ginresp.Wrap(r.apiHandler.UpdateUser)) + apiv2.POST("/users/", ginresp.Wrap(r.apiHandler.CreateUser)) + apiv2.GET("/users/:uid", ginresp.Wrap(r.apiHandler.GetUser)) + apiv2.PATCH("/users/:uid", ginresp.Wrap(r.apiHandler.UpdateUser)) - apiv2.GET("/user/:uid/clients", ginresp.Wrap(r.apiHandler.ListClients)) - apiv2.GET("/user/:uid/clients/:cid", ginresp.Wrap(r.apiHandler.GetClient)) - apiv2.POST("/user/:uid/clients", ginresp.Wrap(r.apiHandler.AddClient)) - apiv2.DELETE("/user/:uid/clients", ginresp.Wrap(r.apiHandler.DeleteClient)) + apiv2.GET("/users/:uid/clients", ginresp.Wrap(r.apiHandler.ListClients)) + apiv2.GET("/users/:uid/clients/:cid", ginresp.Wrap(r.apiHandler.GetClient)) + apiv2.POST("/users/:uid/clients", ginresp.Wrap(r.apiHandler.AddClient)) + apiv2.DELETE("/users/:uid/clients", ginresp.Wrap(r.apiHandler.DeleteClient)) - apiv2.GET("/user/:uid/channels", ginresp.Wrap(r.apiHandler.ListChannels)) - apiv2.GET("/user/:uid/channels/:cid", ginresp.Wrap(r.apiHandler.GetChannel)) - apiv2.GET("/user/:uid/channels/:cid/messages", ginresp.Wrap(r.apiHandler.GetChannelMessages)) - apiv2.GET("/user/:uid/channels/:cid/subscriptions", ginresp.Wrap(r.apiHandler.ListChannelSubscriptions)) + apiv2.GET("/users/:uid/channels", ginresp.Wrap(r.apiHandler.ListChannels)) + apiv2.GET("/users/:uid/channels/:cid", ginresp.Wrap(r.apiHandler.GetChannel)) + apiv2.GET("/users/:uid/channels/:cid/messages", ginresp.Wrap(r.apiHandler.GetChannelMessages)) + apiv2.GET("/users/:uid/channels/:cid/subscriptions", ginresp.Wrap(r.apiHandler.ListChannelSubscriptions)) - apiv2.GET("/user/:uid/subscriptions", ginresp.Wrap(r.apiHandler.ListUserSubscriptions)) - apiv2.GET("/user/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.GetSubscription)) - apiv2.DELETE("/user/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.CancelSubscription)) - apiv2.POST("/user/:uid/subscriptions", ginresp.Wrap(r.apiHandler.CreateSubscription)) + apiv2.GET("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.ListUserSubscriptions)) + apiv2.GET("/users/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.GetSubscription)) + apiv2.DELETE("/users/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.CancelSubscription)) + apiv2.POST("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.CreateSubscription)) + apiv2.PATCH("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.UpdateSubscription)) apiv2.GET("/messages", ginresp.Wrap(r.apiHandler.ListMessages)) apiv2.GET("/messages/:mid", ginresp.Wrap(r.apiHandler.GetMessage)) diff --git a/server/db/methods.go b/server/db/methods.go index cc1495b..e778aa4 100644 --- a/server/db/methods.go +++ b/server/db/methods.go @@ -1,6 +1,7 @@ package db import ( + "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "gogs.mikescher.com/BlackForestBytes/goext/langext" @@ -413,7 +414,7 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID int64, channel mod }, nil } -func (db *Database) ListChannelSubscriptions(ctx TxContext, channelID int64) ([]models.Subscription, error) { +func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID int64) ([]models.Subscription, error) { tx, err := ctx.GetOrCreateTransaction(db) if err != nil { return nil, err @@ -432,6 +433,25 @@ func (db *Database) ListChannelSubscriptions(ctx TxContext, channelID int64) ([] return data, nil } +func (db *Database) ListSubscriptionsByOwner(ctx TxContext, ownerUserID 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_owner_user_id = ?", ownerUserID) + 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 { @@ -512,3 +532,74 @@ func (db *Database) CreateSuccessDelivery(ctx TxContext, client models.Client, m FCMMessageID: langext.Ptr(fcmDelivID), }, nil } + +func (db *Database) ListChannels(ctx *logic.AppContext, userid int64) ([]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 = ?", userid) + if err != nil { + return nil, err + } + + data, err := models.DecodeChannels(rows) + if err != nil { + return nil, err + } + + return data, nil +} + +func (db *Database) GetChannel(ctx TxContext, userid int64, channelid int64) (models.Channel, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return models.Channel{}, err + } + + rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE owner_user_id = ? AND channel_id = ? LIMIT 1", userid, channelid) + if err != nil { + return models.Channel{}, err + } + + client, err := models.DecodeChannel(rows) + if err != nil { + return models.Channel{}, err + } + + return client, nil +} + +func (db *Database) GetSubscription(ctx *logic.AppContext, subid int64) (models.Subscription, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return models.Subscription{}, err + } + + rows, err := tx.QueryContext(ctx, "SELECT * FROM subscriptions WHERE subscription_id = ? LIMIT 1", subid) + if err != nil { + return models.Subscription{}, err + } + + sub, err := models.DecodeSubscription(rows) + if err != nil { + return models.Subscription{}, err + } + + return sub, nil +} + +func (db *Database) DeleteSubscription(ctx *logic.AppContext, subid int64) error { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DELETE FROM subscriptions WHERE subscription_id = ?", subid) + if err != nil { + return err + } + + return nil +} diff --git a/server/firebase/firebase.go b/server/firebase/firebase.go index c5caa11..db527ac 100644 --- a/server/firebase/firebase.go +++ b/server/firebase/firebase.go @@ -47,11 +47,20 @@ type Notification struct { } 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": strconv.FormatInt(msg.SCNMessageID, 10)}, + Data: map[string]string{ + "scn_msg_id": strconv.FormatInt(msg.SCNMessageID, 10), + "usr_msg_id": langext.Coalesce(msg.UserMessageID, ""), + "timestamp": strconv.FormatInt(msg.Timestamp().Unix(), 10), + "priority": strconv.Itoa(msg.Priority), + "trimmed": langext.Conditional(msg.NeedsTrim(), "true", "false"), + "title": msg.Title, + "body": msg.TrimmedBody(), + }, Notification: &messaging.Notification{ Title: msg.Title, - Body: langext.Coalesce(msg.Content, ""), + Body: msg.ShortBody(), }, Android: nil, APNS: nil, @@ -61,12 +70,12 @@ func (fb App) SendNotification(ctx context.Context, client models.Client, msg mo Topic: "", Condition: "", } + if client.Type == models.ClientTypeIOS { n.APNS = nil - } - - if client.Type == models.ClientTypeAndroid { - n.Android = nil + } else if client.Type == models.ClientTypeAndroid { + n.Notification = nil + n.Android = &messaging.AndroidConfig{Priority: "high"} } res, err := fb.messaging.Send(ctx, &n) diff --git a/server/go.mod b/server/go.mod index 9489431..753d7b2 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.20 + gogs.mikescher.com/BlackForestBytes/goext v0.0.21 google.golang.org/api v0.103.0 ) diff --git a/server/go.sum b/server/go.sum index e77b88e..210b146 100644 --- a/server/go.sum +++ b/server/go.sum @@ -168,6 +168,8 @@ gogs.mikescher.com/BlackForestBytes/goext v0.0.19 h1:IvCHlIHDviHQXntZFTNdV7qNq5y 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= +gogs.mikescher.com/BlackForestBytes/goext v0.0.21 h1:OibsssmorZsTdFYRiQFlkXtjUYweQg9SBkWO40ONe0Y= +gogs.mikescher.com/BlackForestBytes/goext v0.0.21/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/models/message.go b/server/models/message.go index 32c2f73..b33d5f6 100644 --- a/server/models/message.go +++ b/server/models/message.go @@ -28,15 +28,39 @@ func (m Message) JSON() MessageJSON { OwnerUserID: m.OwnerUserID, ChannelName: m.ChannelName, ChannelID: m.ChannelID, - Timestamp: langext.Coalesce(m.TimestampClient, m.TimestampReal).Format(time.RFC3339Nano), + Timestamp: m.Timestamp().Format(time.RFC3339Nano), Title: m.Title, Content: m.Content, Priority: m.Priority, UserMessageID: m.UserMessageID, - Trimmed: false, } } +func (m Message) Timestamp() time.Time { + return langext.Coalesce(m.TimestampClient, m.TimestampReal) +} + +func (m Message) NeedsTrim() bool { + return m.Content != nil && len(*m.Content) > 1900 +} + +func (m Message) TrimmedBody() string { + if !m.NeedsTrim() { + return langext.Coalesce(m.Content, "") + } + return langext.Coalesce(m.Content, "")[0:1900-3] + "..." +} + +func (m Message) ShortBody() string { + if m.Content == nil { + return "" + } + if len(*m.Content) < 200 { + return *m.Content + } + return (*m.Content)[0:200-3] + "..." +} + type MessageJSON struct { SCNMessageID int64 `json:"scn_message_id"` SenderUserID int64 `json:"sender_user_id"` @@ -48,7 +72,6 @@ type MessageJSON struct { Content *string `json:"body"` Priority int `json:"priority"` UserMessageID *string `json:"usr_message_id"` - Trimmed bool `json:"trimmed"` } type MessageDB struct { diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index d913745..c51109c 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -91,7 +91,7 @@ } } }, - "/api-v2/user/": { + "/api-v2/users/": { "post": { "summary": "Create a new user", "operationId": "api-user-create", @@ -127,7 +127,7 @@ } } }, - "/api-v2/user/{uid}": { + "/api-v2/users/{uid}": { "get": { "summary": "Get a user", "operationId": "api-user-get", @@ -221,7 +221,7 @@ } } }, - "/api-v2/user/{uid}/clients": { + "/api-v2/users/{uid}/clients": { "get": { "summary": "List all clients", "operationId": "api-clients-list", @@ -320,7 +320,7 @@ } } }, - "/api-v2/user/{uid}/clients/{cid}": { + "/api-v2/users/{uid}/clients/{cid}": { "get": { "summary": "Get a single clients", "operationId": "api-clients-get", diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index 5d92417..f34f37f 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -340,7 +340,7 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' summary: Send a new message - /api-v2/user/: + /api-v2/users/: post: operationId: api-user-create parameters: @@ -363,7 +363,7 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' summary: Create a new user - /api-v2/user/{uid}: + /api-v2/users/{uid}: get: operationId: api-user-get parameters: @@ -425,7 +425,7 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' summary: (Partially) update a user - /api-v2/user/{uid}/clients: + /api-v2/users/{uid}/clients: get: operationId: api-clients-list parameters: @@ -491,7 +491,7 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' summary: Delete a client - /api-v2/user/{uid}/clients/{cid}: + /api-v2/users/{uid}/clients/{cid}: get: operationId: api-clients-get parameters: