ListChannels(), GetChannel(), ListUserSubscriptions(), ListChannelSubscriptions(), GetSubscription(), CancelSubscription()

This commit is contained in:
Mike Schwörer 2022-11-19 17:07:30 +01:00
parent 85bfe79115
commit 5c2877bdb8
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
11 changed files with 456 additions and 58 deletions

View File

@ -24,6 +24,8 @@ const (
USER_NOT_FOUND APIError = 1301 USER_NOT_FOUND APIError = 1301
CLIENT_NOT_FOUND APIError = 1302 CLIENT_NOT_FOUND APIError = 1302
CHANNEL_NOT_FOUND APIError = 1303
SUBSCRIPTION_NOT_FOUND APIError = 1304
USER_AUTH_FAILED APIError = 1311 USER_AUTH_FAILED APIError = 1311
NO_DEVICE_LINKED APIError = 1401 NO_DEVICE_LINKED APIError = 1401

View File

@ -36,7 +36,7 @@ func NewAPIHandler(app *logic.Application) APIHandler {
// @Failure 400 {object} ginresp.apiError // @Failure 400 {object} ginresp.apiError
// @Failure 500 {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 { func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
type body struct { type body struct {
FCMToken string `json:"fcm_token"` FCMToken string `json:"fcm_token"`
@ -116,7 +116,7 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {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 { func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID int64 `uri:"uid"` UserID int64 `uri:"uid"`
@ -158,7 +158,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {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 { func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID int64 `uri:"uid"` UserID int64 `uri:"uid"`
@ -228,18 +228,18 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// //
// @Success 200 {object} handler.ListClients.result // @Success 200 {object} handler.ListClients.response
// @Failure 400 {object} ginresp.apiError // @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError // @Failure 401 {object} ginresp.apiError
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {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 { func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID int64 `uri:"uid"` UserID int64 `uri:"uid"`
} }
type result struct { type response struct {
Clients []models.ClientJSON `json:"clients"` 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() }) 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 // GetClient swaggerdoc
@ -278,7 +278,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {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 { func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID int64 `uri:"uid"` UserID int64 `uri:"uid"`
@ -322,7 +322,7 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {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 { func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID int64 `uri:"uid"` UserID int64 `uri:"uid"`
@ -377,7 +377,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {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 { func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID int64 `uri:"uid"` 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())) 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 { 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 { 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 { func (h APIHandler) GetChannelMessages(g *gin.Context) ginresp.HTTPResponse {
return ginresp.NotImplemented() 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 { 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 { 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 { 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 { 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 { func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
return ginresp.NotImplemented() return ginresp.NotImplemented()
} }
func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
return ginresp.NotImplemented()
}
func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
return ginresp.NotImplemented() return ginresp.NotImplemented()
} }

View File

@ -76,7 +76,6 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
ErrorHighlight int `json:"errhighlight"` ErrorHighlight int `json:"errhighlight"`
Message string `json:"message"` Message string `json:"message"`
SuppressSend bool `json:"suppress_send"` SuppressSend bool `json:"suppress_send"`
Response string `json:"response"`
MessageCount int `json:"messagecount"` MessageCount int `json:"messagecount"`
Quota int `json:"quota"` Quota int `json:"quota"`
IsPro bool `json:"is_pro"` IsPro bool `json:"is_pro"`
@ -145,10 +144,9 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
return ginresp.JSON(http.StatusOK, response{ return ginresp.JSON(http.StatusOK, response{
Success: true, Success: true,
ErrorID: apierr.NO_ERROR, ErrorID: apierr.NO_ERROR,
ErrorHighlight: 0, ErrorHighlight: -1,
Message: "Message already sent", Message: "Message already sent",
SuppressSend: true, SuppressSend: true,
Response: "",
MessageCount: user.MessagesSent, MessageCount: user.MessagesSent,
Quota: user.QuotaUsedToday(), Quota: user.QuotaUsedToday(),
IsPro: user.IsPro, 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") 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 { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions")
} }
@ -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) { func (h MessageHandler) deliverMessage(ctx *logic.AppContext, client models.Client, msg models.Message) (*string, error) {

View File

@ -94,24 +94,25 @@ func (r *Router) Init(e *gin.Engine) {
apiv2 := e.Group("/api-v2/") apiv2 := e.Group("/api-v2/")
{ {
apiv2.POST("/user/", ginresp.Wrap(r.apiHandler.CreateUser)) apiv2.POST("/users/", ginresp.Wrap(r.apiHandler.CreateUser))
apiv2.GET("/user/:uid", ginresp.Wrap(r.apiHandler.GetUser)) apiv2.GET("/users/:uid", ginresp.Wrap(r.apiHandler.GetUser))
apiv2.PATCH("/user/:uid", ginresp.Wrap(r.apiHandler.UpdateUser)) apiv2.PATCH("/users/:uid", ginresp.Wrap(r.apiHandler.UpdateUser))
apiv2.GET("/user/:uid/clients", ginresp.Wrap(r.apiHandler.ListClients)) apiv2.GET("/users/:uid/clients", ginresp.Wrap(r.apiHandler.ListClients))
apiv2.GET("/user/:uid/clients/:cid", ginresp.Wrap(r.apiHandler.GetClient)) apiv2.GET("/users/:uid/clients/:cid", ginresp.Wrap(r.apiHandler.GetClient))
apiv2.POST("/user/:uid/clients", ginresp.Wrap(r.apiHandler.AddClient)) apiv2.POST("/users/:uid/clients", ginresp.Wrap(r.apiHandler.AddClient))
apiv2.DELETE("/user/:uid/clients", ginresp.Wrap(r.apiHandler.DeleteClient)) apiv2.DELETE("/users/:uid/clients", ginresp.Wrap(r.apiHandler.DeleteClient))
apiv2.GET("/user/:uid/channels", ginresp.Wrap(r.apiHandler.ListChannels)) apiv2.GET("/users/:uid/channels", ginresp.Wrap(r.apiHandler.ListChannels))
apiv2.GET("/user/:uid/channels/:cid", ginresp.Wrap(r.apiHandler.GetChannel)) apiv2.GET("/users/:uid/channels/:cid", ginresp.Wrap(r.apiHandler.GetChannel))
apiv2.GET("/user/:uid/channels/:cid/messages", ginresp.Wrap(r.apiHandler.GetChannelMessages)) apiv2.GET("/users/: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/:cid/subscriptions", ginresp.Wrap(r.apiHandler.ListChannelSubscriptions))
apiv2.GET("/user/:uid/subscriptions", ginresp.Wrap(r.apiHandler.ListUserSubscriptions)) apiv2.GET("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.ListUserSubscriptions))
apiv2.GET("/user/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.GetSubscription)) apiv2.GET("/users/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.GetSubscription))
apiv2.DELETE("/user/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.CancelSubscription)) apiv2.DELETE("/users/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.CancelSubscription))
apiv2.POST("/user/:uid/subscriptions", ginresp.Wrap(r.apiHandler.CreateSubscription)) 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", ginresp.Wrap(r.apiHandler.ListMessages))
apiv2.GET("/messages/:mid", ginresp.Wrap(r.apiHandler.GetMessage)) apiv2.GET("/messages/:mid", ginresp.Wrap(r.apiHandler.GetMessage))

View File

@ -1,6 +1,7 @@
package db package db
import ( import (
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
@ -413,7 +414,7 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID int64, channel mod
}, nil }, 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err
@ -432,6 +433,25 @@ func (db *Database) ListChannelSubscriptions(ctx TxContext, channelID int64) ([]
return data, nil 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) { func (db *Database) CreateRetryDelivery(ctx TxContext, client models.Client, msg models.Message) (models.Delivery, error) {
tx, err := ctx.GetOrCreateTransaction(db) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
@ -512,3 +532,74 @@ func (db *Database) CreateSuccessDelivery(ctx TxContext, client models.Client, m
FCMMessageID: langext.Ptr(fcmDelivID), FCMMessageID: langext.Ptr(fcmDelivID),
}, nil }, 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
}

View File

@ -47,11 +47,20 @@ type Notification struct {
} }
func (fb App) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) { func (fb App) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) {
n := messaging.Message{ 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{ Notification: &messaging.Notification{
Title: msg.Title, Title: msg.Title,
Body: langext.Coalesce(msg.Content, ""), Body: msg.ShortBody(),
}, },
Android: nil, Android: nil,
APNS: nil, APNS: nil,
@ -61,12 +70,12 @@ func (fb App) SendNotification(ctx context.Context, client models.Client, msg mo
Topic: "", Topic: "",
Condition: "", Condition: "",
} }
if client.Type == models.ClientTypeIOS { if client.Type == models.ClientTypeIOS {
n.APNS = nil n.APNS = nil
} } else if client.Type == models.ClientTypeAndroid {
n.Notification = nil
if client.Type == models.ClientTypeAndroid { n.Android = &messaging.AndroidConfig{Priority: "high"}
n.Android = nil
} }
res, err := fb.messaging.Send(ctx, &n) res, err := fb.messaging.Send(ctx, &n)

View File

@ -9,7 +9,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/rs/zerolog v1.28.0 github.com/rs/zerolog v1.28.0
github.com/swaggo/swag v1.8.7 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 google.golang.org/api v0.103.0
) )

View File

@ -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.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 h1:HxJ0iZ838TQnp/a+/DNajdZjZkV43OsK4VbHarOiHTs=
gogs.mikescher.com/BlackForestBytes/goext v0.0.20/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= 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-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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=

View File

@ -28,15 +28,39 @@ func (m Message) JSON() MessageJSON {
OwnerUserID: m.OwnerUserID, OwnerUserID: m.OwnerUserID,
ChannelName: m.ChannelName, ChannelName: m.ChannelName,
ChannelID: m.ChannelID, ChannelID: m.ChannelID,
Timestamp: langext.Coalesce(m.TimestampClient, m.TimestampReal).Format(time.RFC3339Nano), Timestamp: m.Timestamp().Format(time.RFC3339Nano),
Title: m.Title, Title: m.Title,
Content: m.Content, Content: m.Content,
Priority: m.Priority, Priority: m.Priority,
UserMessageID: m.UserMessageID, 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 { type MessageJSON struct {
SCNMessageID int64 `json:"scn_message_id"` SCNMessageID int64 `json:"scn_message_id"`
SenderUserID int64 `json:"sender_user_id"` SenderUserID int64 `json:"sender_user_id"`
@ -48,7 +72,6 @@ type MessageJSON struct {
Content *string `json:"body"` Content *string `json:"body"`
Priority int `json:"priority"` Priority int `json:"priority"`
UserMessageID *string `json:"usr_message_id"` UserMessageID *string `json:"usr_message_id"`
Trimmed bool `json:"trimmed"`
} }
type MessageDB struct { type MessageDB struct {

View File

@ -91,7 +91,7 @@
} }
} }
}, },
"/api-v2/user/": { "/api-v2/users/": {
"post": { "post": {
"summary": "Create a new user", "summary": "Create a new user",
"operationId": "api-user-create", "operationId": "api-user-create",
@ -127,7 +127,7 @@
} }
} }
}, },
"/api-v2/user/{uid}": { "/api-v2/users/{uid}": {
"get": { "get": {
"summary": "Get a user", "summary": "Get a user",
"operationId": "api-user-get", "operationId": "api-user-get",
@ -221,7 +221,7 @@
} }
} }
}, },
"/api-v2/user/{uid}/clients": { "/api-v2/users/{uid}/clients": {
"get": { "get": {
"summary": "List all clients", "summary": "List all clients",
"operationId": "api-clients-list", "operationId": "api-clients-list",
@ -320,7 +320,7 @@
} }
} }
}, },
"/api-v2/user/{uid}/clients/{cid}": { "/api-v2/users/{uid}/clients/{cid}": {
"get": { "get": {
"summary": "Get a single clients", "summary": "Get a single clients",
"operationId": "api-clients-get", "operationId": "api-clients-get",

View File

@ -340,7 +340,7 @@ paths:
schema: schema:
$ref: '#/definitions/ginresp.apiError' $ref: '#/definitions/ginresp.apiError'
summary: Send a new message summary: Send a new message
/api-v2/user/: /api-v2/users/:
post: post:
operationId: api-user-create operationId: api-user-create
parameters: parameters:
@ -363,7 +363,7 @@ paths:
schema: schema:
$ref: '#/definitions/ginresp.apiError' $ref: '#/definitions/ginresp.apiError'
summary: Create a new user summary: Create a new user
/api-v2/user/{uid}: /api-v2/users/{uid}:
get: get:
operationId: api-user-get operationId: api-user-get
parameters: parameters:
@ -425,7 +425,7 @@ paths:
schema: schema:
$ref: '#/definitions/ginresp.apiError' $ref: '#/definitions/ginresp.apiError'
summary: (Partially) update a user summary: (Partially) update a user
/api-v2/user/{uid}/clients: /api-v2/users/{uid}/clients:
get: get:
operationId: api-clients-list operationId: api-clients-list
parameters: parameters:
@ -491,7 +491,7 @@ paths:
schema: schema:
$ref: '#/definitions/ginresp.apiError' $ref: '#/definitions/ginresp.apiError'
summary: Delete a client summary: Delete a client
/api-v2/user/{uid}/clients/{cid}: /api-v2/users/{uid}/clients/{cid}:
get: get:
operationId: api-clients-get operationId: api-clients-get
parameters: parameters: