Fix SQL unmarshalling of optional nested structs (LEFT JOIN)
This commit is contained in:
parent
0cb2a977a0
commit
0112d681ac
@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
- diff my currently used scnsend script vs the one in the docs here
|
- diff my currently used scnsend script vs the one in the docs here
|
||||||
|
|
||||||
|
- Pagination for ListChannels / ListSubscriptions / ListClients / ListChannelSubscriptions / ListUserSubscriptions
|
||||||
|
|
||||||
-------------------------------------------------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
- in my script: use (backupname || hostname) for sendername
|
- in my script: use (backupname || hostname) for sendername
|
||||||
|
@ -18,6 +18,7 @@ const (
|
|||||||
BINDFAIL_QUERY_PARAM APIError = 1151
|
BINDFAIL_QUERY_PARAM APIError = 1151
|
||||||
BINDFAIL_BODY_PARAM APIError = 1152
|
BINDFAIL_BODY_PARAM APIError = 1152
|
||||||
BINDFAIL_URI_PARAM APIError = 1153
|
BINDFAIL_URI_PARAM APIError = 1153
|
||||||
|
INVALID_BODY_PARAM APIError = 1161
|
||||||
INVALID_ENUM_VALUE APIError = 1171
|
INVALID_ENUM_VALUE APIError = 1171
|
||||||
|
|
||||||
NO_TITLE APIError = 1201
|
NO_TITLE APIError = 1201
|
||||||
@ -29,12 +30,13 @@ const (
|
|||||||
CHANNEL_TOO_LONG APIError = 1207
|
CHANNEL_TOO_LONG APIError = 1207
|
||||||
CHANNEL_NAME_WOULD_CHANGE APIError = 1207
|
CHANNEL_NAME_WOULD_CHANGE APIError = 1207
|
||||||
|
|
||||||
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
|
CHANNEL_NOT_FOUND APIError = 1303
|
||||||
SUBSCRIPTION_NOT_FOUND APIError = 1304
|
SUBSCRIPTION_NOT_FOUND APIError = 1304
|
||||||
MESSAGE_NOT_FOUND APIError = 1305
|
MESSAGE_NOT_FOUND APIError = 1305
|
||||||
USER_AUTH_FAILED APIError = 1311
|
SUBSCRIPTION_USER_MISMATCH APIError = 1306
|
||||||
|
USER_AUTH_FAILED APIError = 1311
|
||||||
|
|
||||||
NO_DEVICE_LINKED APIError = 1401
|
NO_DEVICE_LINKED APIError = 1401
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||||
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
|
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||||
"blackforestbytes.com/simplecloudnotifier/db"
|
"blackforestbytes.com/simplecloudnotifier/db"
|
||||||
"blackforestbytes.com/simplecloudnotifier/db/cursortoken"
|
"blackforestbytes.com/simplecloudnotifier/db/cursortoken"
|
||||||
@ -501,8 +500,8 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// @ID api-channels-list
|
// @ID api-channels-list
|
||||||
// @Tags API-v2
|
// @Tags API-v2
|
||||||
//
|
//
|
||||||
// @Param uid path int true "UserID"
|
// @Param uid path int true "UserID"
|
||||||
// @Param selector query string true "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any)
|
// @Param selector query string false "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any)
|
||||||
//
|
//
|
||||||
// @Success 200 {object} handler.ListChannels.response
|
// @Success 200 {object} handler.ListChannels.response
|
||||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||||
@ -667,6 +666,10 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
return *permResp
|
return *permResp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.Name == "" {
|
||||||
|
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Missing parameter: name", nil)
|
||||||
|
}
|
||||||
|
|
||||||
channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name)
|
channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name)
|
||||||
channelInternalName := h.app.NormalizeChannelInternalName(b.Name)
|
channelInternalName := h.app.NormalizeChannelInternalName(b.Name)
|
||||||
|
|
||||||
@ -677,17 +680,17 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
user, err := h.database.GetUser(ctx, u.UserID)
|
user, err := h.database.GetUser(ctx, u.UserID)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", nil)
|
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(channelDisplayName) > user.MaxChannelNameLength() {
|
if len(channelDisplayName) > user.MaxChannelNameLength() {
|
||||||
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
|
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
|
||||||
}
|
}
|
||||||
if len(channelInternalName) > user.MaxChannelNameLength() {
|
if len(channelInternalName) > user.MaxChannelNameLength() {
|
||||||
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
|
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if channelExisting != nil {
|
if channelExisting != nil {
|
||||||
@ -915,27 +918,27 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
// ListUserSubscriptions swaggerdoc
|
// ListUserSubscriptions swaggerdoc
|
||||||
//
|
//
|
||||||
// @Summary List all subscriptions of a user (incoming/owned)
|
// @Summary List all subscriptions of a user (incoming/owned)
|
||||||
// // @Description The possible values for 'selector' are:
|
// @Description The possible values for 'selector' are:
|
||||||
// // @Description - "owner_all" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels)
|
// @Description - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)
|
||||||
// // @Description - "owner_confirmed" Confirmed subscriptions with the user as owner
|
// @Description - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber
|
||||||
// // @Description - "owner_unconfirmed" Unconfirmed (Pending) subscriptions with the user as owner
|
// @Description - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber
|
||||||
// // @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
|
// @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
|
||||||
// // @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
|
// @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
|
||||||
// // @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
|
// @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
|
||||||
// //
|
|
||||||
// @ID api-user-subscriptions-list
|
|
||||||
// @Tags API-v2
|
|
||||||
//
|
//
|
||||||
// @Param uid path int true "UserID"
|
// @ID api-user-subscriptions-list
|
||||||
// @Param selector query string true "Filter subscribptions (default: owner_all)" Enums(owner_all, owner_confirmed, owner_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed)
|
// @Tags API-v2
|
||||||
//
|
//
|
||||||
// @Success 200 {object} handler.ListUserSubscriptions.response
|
// @Param uid path int true "UserID"
|
||||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
// @Param selector query string true "Filter subscriptions (default: owner_all)" Enums(outgoing_all, outgoing_confirmed, outgoing_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed)
|
||||||
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
|
||||||
// @Failure 500 {object} ginresp.apiError "internal server error"
|
|
||||||
//
|
//
|
||||||
// @Router /api/users/{uid}/subscriptions [GET]
|
// @Success 200 {object} handler.ListUserSubscriptions.response
|
||||||
|
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||||
|
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||||
|
// @Failure 500 {object} ginresp.apiError "internal server error"
|
||||||
|
//
|
||||||
|
// @Router /api/users/{uid}/subscriptions [GET]
|
||||||
func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
|
func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type uri struct {
|
type uri struct {
|
||||||
UserID models.UserID `uri:"uid"`
|
UserID models.UserID `uri:"uid"`
|
||||||
@ -964,48 +967,48 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
var res []models.Subscription
|
var res []models.Subscription
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if sel == "owner_all" {
|
if sel == "outgoing_all" {
|
||||||
|
|
||||||
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if sel == "owner_confirmed" {
|
|
||||||
|
|
||||||
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, langext.Ptr(true))
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if sel == "owner_unconfirmed" {
|
|
||||||
|
|
||||||
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, langext.Ptr(false))
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if sel == "incoming_all" {
|
|
||||||
|
|
||||||
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, nil)
|
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if sel == "incoming_confirmed" {
|
} else if sel == "outgoing_confirmed" {
|
||||||
|
|
||||||
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(true))
|
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if sel == "incoming_unconfirmed" {
|
} else if sel == "outgoing_unconfirmed" {
|
||||||
|
|
||||||
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(false))
|
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(false))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if sel == "incoming_all" {
|
||||||
|
|
||||||
|
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if sel == "incoming_confirmed" {
|
||||||
|
|
||||||
|
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(true))
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if sel == "incoming_unconfirmed" {
|
||||||
|
|
||||||
|
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(false))
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
|
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
|
||||||
@ -1111,9 +1114,8 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(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 {
|
||||||
if subscription.SubscriberUserID != u.UserID {
|
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", 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()))
|
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON()))
|
||||||
@ -1159,9 +1161,8 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(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 {
|
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
|
||||||
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
|
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.database.DeleteSubscription(ctx, u.SubscriptionID)
|
err = h.database.DeleteSubscription(ctx, u.SubscriptionID)
|
||||||
@ -1174,27 +1175,29 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
// CreateSubscription swaggerdoc
|
// CreateSubscription swaggerdoc
|
||||||
//
|
//
|
||||||
// @Summary Creare/Request a subscription
|
// @Summary Create/Request a subscription
|
||||||
// @ID api-subscriptions-create
|
// @Description Either [channel_owner_user_id, channel_internal_name] or [channel_id] must be supplied in the request body
|
||||||
// @Tags API-v2
|
// @ID api-subscriptions-create
|
||||||
|
// @Tags API-v2
|
||||||
//
|
//
|
||||||
// @Param uid path int true "UserID"
|
// @Param uid path int true "UserID"
|
||||||
// @Param query_data query handler.CreateSubscription.query false " "
|
// @Param query_data query handler.CreateSubscription.query false " "
|
||||||
// @Param post_data body handler.CreateSubscription.body false " "
|
// @Param post_data body handler.CreateSubscription.body false " "
|
||||||
//
|
//
|
||||||
// @Success 200 {object} models.SubscriptionJSON
|
// @Success 200 {object} models.SubscriptionJSON
|
||||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||||
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||||
// @Failure 500 {object} ginresp.apiError "internal server error"
|
// @Failure 500 {object} ginresp.apiError "internal server error"
|
||||||
//
|
//
|
||||||
// @Router /api/users/{uid}/subscriptions [POST]
|
// @Router /api/users/{uid}/subscriptions [POST]
|
||||||
func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
|
func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type uri struct {
|
type uri struct {
|
||||||
UserID models.UserID `uri:"uid"`
|
UserID models.UserID `uri:"uid"`
|
||||||
}
|
}
|
||||||
type body struct {
|
type body struct {
|
||||||
ChannelOwnerUserID models.UserID `form:"channel_owner_user_id" binding:"required"`
|
ChannelOwnerUserID *models.UserID `json:"channel_owner_user_id"`
|
||||||
Channel string `form:"channel_name" binding:"required"`
|
ChannelInternalName *string `json:"channel_internal_name"`
|
||||||
|
ChannelID *models.ChannelID `json:"channel_id"`
|
||||||
}
|
}
|
||||||
type query struct {
|
type query struct {
|
||||||
ChanSubscribeKey *string `json:"chan_subscribe_key" form:"chan_subscribe_key"`
|
ChanSubscribeKey *string `json:"chan_subscribe_key" form:"chan_subscribe_key"`
|
||||||
@ -1213,21 +1216,45 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
return *permResp
|
return *permResp
|
||||||
}
|
}
|
||||||
|
|
||||||
channelInternalName := h.app.NormalizeChannelInternalName(b.Channel)
|
var channel models.Channel
|
||||||
|
|
||||||
|
if b.ChannelOwnerUserID != nil && b.ChannelInternalName != nil && b.ChannelID == nil {
|
||||||
|
|
||||||
|
channelInternalName := h.app.NormalizeChannelInternalName(*b.ChannelInternalName)
|
||||||
|
|
||||||
|
outchannel, err := h.database.GetChannelByName(ctx, *b.ChannelOwnerUserID, channelInternalName)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
|
||||||
|
}
|
||||||
|
if outchannel == nil {
|
||||||
|
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
channel = *outchannel
|
||||||
|
|
||||||
|
} else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil {
|
||||||
|
|
||||||
|
outchannel, err := h.database.GetChannelByID(ctx, *b.ChannelID)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
|
||||||
|
}
|
||||||
|
if outchannel == nil {
|
||||||
|
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
channel = *outchannel
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Must either supply [channel_owner_user_id, channel_internal_name] or [channel_id]", nil)
|
||||||
|
|
||||||
channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, channelInternalName)
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
|
|
||||||
}
|
|
||||||
if channel == nil {
|
|
||||||
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) {
|
if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) {
|
||||||
ginresp.APIError(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
sub, err := h.database.CreateSubscription(ctx, u.UserID, *channel, channel.OwnerUserID == u.UserID)
|
sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, channel.OwnerUserID == u.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
|
||||||
}
|
}
|
||||||
@ -1279,9 +1306,8 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(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 {
|
||||||
if subscription.ChannelOwnerUserID != u.UserID {
|
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
|
||||||
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.Confirmed != nil {
|
if b.Confirmed != nil {
|
||||||
|
@ -124,7 +124,7 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
_, libVersionNumber, _ := sqlite3.Version()
|
_, libVersionNumber, _ := sqlite3.Version()
|
||||||
|
|
||||||
if libVersionNumber < 3039000 {
|
if libVersionNumber < 3039000 {
|
||||||
ginresp.InternalError(errors.New("sqlite version too low"))
|
return ginresp.InternalError(errors.New("sqlite version too low"))
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.app.Database.Ping(ctx)
|
err := h.app.Database.Ping(ctx)
|
||||||
|
@ -57,6 +57,30 @@ func (db *Database) GetChannelByNameAndSendKey(ctx TxContext, chanName string, s
|
|||||||
return &channel, nil
|
return &channel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*models.Channel, error) {
|
||||||
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE channel_id = :cid LIMIT 1", sq.PP{
|
||||||
|
"cid": chanid,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, err := models.DecodeChannel(rows)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &channel, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName string, intName string, subscribeKey string, sendKey string) (models.Channel, error) {
|
func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName string, intName string, subscribeKey string, sendKey string) (models.Channel, error) {
|
||||||
tx, err := ctx.GetOrCreateTransaction(db)
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -206,12 +206,7 @@ func (pp *DBPreprocessor) getTableColumns(ctx context.Context, tablename string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
type res struct {
|
type res struct {
|
||||||
CID int64 `db:"cid"`
|
Name string `db:"name"`
|
||||||
Name string `db:"name"`
|
|
||||||
Type string `db:"type"`
|
|
||||||
NotNull int `db:"notnull"`
|
|
||||||
DFLT *string `db:"dflt_value"`
|
|
||||||
PK int `db:"pk"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := pp.db.Query(ctx, "PRAGMA table_info('"+tablename+"');", sq.PP{})
|
rows, err := pp.db.Query(ctx, "PRAGMA table_info('"+tablename+"');", sq.PP{})
|
||||||
@ -219,7 +214,7 @@ func (pp *DBPreprocessor) getTableColumns(ctx context.Context, tablename string)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resrows, err := sq.ScanAll[res](rows, true)
|
resrows, err := sq.ScanAll[res](rows, sq.SModeFast, sq.Unsafe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID models.C
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) ListSubscriptionsByOwner(ctx TxContext, ownerUserID models.UserID, confirmed *bool) ([]models.Subscription, error) {
|
func (db *Database) ListSubscriptionsByChannelOwner(ctx TxContext, ownerUserID models.UserID, confirmed *bool) ([]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
|
||||||
|
@ -8,7 +8,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.42
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.46
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -122,6 +122,12 @@ gogs.mikescher.com/BlackForestBytes/goext v0.0.41 h1:3p/MtkHZ2gulSdizXql3VnFf2v7
|
|||||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.41/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.41/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.42 h1:u6+pDRrL9wSvJG7gVsGUO4dA54qzac5LsqoXqi6oo9E=
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.42 h1:u6+pDRrL9wSvJG7gVsGUO4dA54qzac5LsqoXqi6oo9E=
|
||||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.42/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.42/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.44 h1:YC8SrQk1BEDR5wCdLZ2trnNvkUg/sssW94XYKrsKyc4=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.44/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.45 h1:1naABIgSa5hhWPT7kYAAEeIUBNLo7nVvE6/kz9LoY9Q=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.45/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.46 h1:7nV9RKnnz/qgkVWvlj4MOAITbe+Gas1niVQgvbHnNk8=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.46/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||||
|
@ -117,7 +117,7 @@ func (c ChannelWithSubscriptionDB) Model() ChannelWithSubscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeChannel(r *sqlx.Rows) (Channel, error) {
|
func DecodeChannel(r *sqlx.Rows) (Channel, error) {
|
||||||
data, err := sq.ScanSingle[ChannelDB](r, true)
|
data, err := sq.ScanSingle[ChannelDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Channel{}, err
|
return Channel{}, err
|
||||||
}
|
}
|
||||||
@ -125,7 +125,7 @@ func DecodeChannel(r *sqlx.Rows) (Channel, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeChannels(r *sqlx.Rows) ([]Channel, error) {
|
func DecodeChannels(r *sqlx.Rows) ([]Channel, error) {
|
||||||
data, err := sq.ScanAll[ChannelDB](r, true)
|
data, err := sq.ScanAll[ChannelDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ func DecodeChannels(r *sqlx.Rows) ([]Channel, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeChannelWithSubscription(r *sqlx.Rows) (ChannelWithSubscription, error) {
|
func DecodeChannelWithSubscription(r *sqlx.Rows) (ChannelWithSubscription, error) {
|
||||||
data, err := sq.ScanSingle[ChannelWithSubscriptionDB](r, true)
|
data, err := sq.ScanSingle[ChannelWithSubscriptionDB](r, sq.SModeExtended, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ChannelWithSubscription{}, err
|
return ChannelWithSubscription{}, err
|
||||||
}
|
}
|
||||||
@ -141,7 +141,7 @@ func DecodeChannelWithSubscription(r *sqlx.Rows) (ChannelWithSubscription, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeChannelsWithSubscription(r *sqlx.Rows) ([]ChannelWithSubscription, error) {
|
func DecodeChannelsWithSubscription(r *sqlx.Rows) ([]ChannelWithSubscription, error) {
|
||||||
data, err := sq.ScanAll[ChannelWithSubscriptionDB](r, true)
|
data, err := sq.ScanAll[ChannelWithSubscriptionDB](r, sq.SModeExtended, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ func (c ClientDB) Model() Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeClient(r *sqlx.Rows) (Client, error) {
|
func DecodeClient(r *sqlx.Rows) (Client, error) {
|
||||||
data, err := sq.ScanSingle[ClientDB](r, true)
|
data, err := sq.ScanSingle[ClientDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Client{}, err
|
return Client{}, err
|
||||||
}
|
}
|
||||||
@ -77,7 +77,7 @@ func DecodeClient(r *sqlx.Rows) (Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeClients(r *sqlx.Rows) ([]Client, error) {
|
func DecodeClients(r *sqlx.Rows) ([]Client, error) {
|
||||||
data, err := sq.ScanAll[ClientDB](r, true)
|
data, err := sq.ScanAll[ClientDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@ func (d DeliveryDB) Model() Delivery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeDelivery(r *sqlx.Rows) (Delivery, error) {
|
func DecodeDelivery(r *sqlx.Rows) (Delivery, error) {
|
||||||
data, err := sq.ScanSingle[DeliveryDB](r, true)
|
data, err := sq.ScanSingle[DeliveryDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Delivery{}, err
|
return Delivery{}, err
|
||||||
}
|
}
|
||||||
@ -97,7 +97,7 @@ func DecodeDelivery(r *sqlx.Rows) (Delivery, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeDeliveries(r *sqlx.Rows) ([]Delivery, error) {
|
func DecodeDeliveries(r *sqlx.Rows) ([]Delivery, error) {
|
||||||
data, err := sq.ScanAll[DeliveryDB](r, true)
|
data, err := sq.ScanAll[DeliveryDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@ func (m MessageDB) Model() Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeMessage(r *sqlx.Rows) (Message, error) {
|
func DecodeMessage(r *sqlx.Rows) (Message, error) {
|
||||||
data, err := sq.ScanSingle[MessageDB](r, true)
|
data, err := sq.ScanSingle[MessageDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Message{}, err
|
return Message{}, err
|
||||||
}
|
}
|
||||||
@ -154,7 +154,7 @@ func DecodeMessage(r *sqlx.Rows) (Message, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeMessages(r *sqlx.Rows) ([]Message, error) {
|
func DecodeMessages(r *sqlx.Rows) ([]Message, error) {
|
||||||
data, err := sq.ScanAll[MessageDB](r, true)
|
data, err := sq.ScanAll[MessageDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ func (s SubscriptionDB) Model() Subscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeSubscription(r *sqlx.Rows) (Subscription, error) {
|
func DecodeSubscription(r *sqlx.Rows) (Subscription, error) {
|
||||||
data, err := sq.ScanSingle[SubscriptionDB](r, true)
|
data, err := sq.ScanSingle[SubscriptionDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Subscription{}, err
|
return Subscription{}, err
|
||||||
}
|
}
|
||||||
@ -70,7 +70,7 @@ func DecodeSubscription(r *sqlx.Rows) (Subscription, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeSubscriptions(r *sqlx.Rows) ([]Subscription, error) {
|
func DecodeSubscriptions(r *sqlx.Rows) ([]Subscription, error) {
|
||||||
data, err := sq.ScanAll[SubscriptionDB](r, true)
|
data, err := sq.ScanAll[SubscriptionDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ func (u UserDB) Model() User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeUser(r *sqlx.Rows) (User, error) {
|
func DecodeUser(r *sqlx.Rows) (User, error) {
|
||||||
data, err := sq.ScanSingle[UserDB](r, true)
|
data, err := sq.ScanSingle[UserDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return User{}, err
|
return User{}, err
|
||||||
}
|
}
|
||||||
@ -171,7 +171,7 @@ func DecodeUser(r *sqlx.Rows) (User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DecodeUsers(r *sqlx.Rows) ([]User, error) {
|
func DecodeUsers(r *sqlx.Rows) ([]User, error) {
|
||||||
data, err := sq.ScanAll[UserDB](r, true)
|
data, err := sq.ScanAll[UserDB](r, sq.SModeFast, sq.Safe, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1176,8 +1176,7 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Filter channels (default: owned)",
|
"description": "Filter channels (default: owned)",
|
||||||
"name": "selector",
|
"name": "selector",
|
||||||
"in": "query",
|
"in": "query"
|
||||||
"required": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@ -1743,6 +1742,7 @@
|
|||||||
},
|
},
|
||||||
"/api/users/{uid}/subscriptions": {
|
"/api/users/{uid}/subscriptions": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"description": "The possible values for 'selector' are:\n- \"owner_all\" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels)\n- \"owner_confirmed\" Confirmed subscriptions with the user as owner\n- \"owner_unconfirmed\" Unconfirmed (Pending) subscriptions with the user as owner\n- \"incoming_all\" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)\n- \"incoming_confirmed\" Confirmed subscriptions from other users to channels of this user\n- \"incoming_unconfirmed\" Unconfirmed subscriptions from other users to channels of this user (= requests)",
|
||||||
"tags": [
|
"tags": [
|
||||||
"API-v2"
|
"API-v2"
|
||||||
],
|
],
|
||||||
@ -1766,7 +1766,7 @@
|
|||||||
"incoming_unconfirmed"
|
"incoming_unconfirmed"
|
||||||
],
|
],
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Filter subscribptions (default: owner_all)",
|
"description": "Filter subscriptions (default: owner_all)",
|
||||||
"name": "selector",
|
"name": "selector",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"required": true
|
"required": true
|
||||||
@ -1800,10 +1800,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
|
"description": "Either [channel_owner_user_id, channel_internal_name] or [channel_id] must be supplied in the request body",
|
||||||
"tags": [
|
"tags": [
|
||||||
"API-v2"
|
"API-v2"
|
||||||
],
|
],
|
||||||
"summary": "Creare/Request a subscription",
|
"summary": "Create/Request a subscription",
|
||||||
"operationId": "api-subscriptions-create",
|
"operationId": "api-subscriptions-create",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -2410,11 +2411,15 @@
|
|||||||
"handler.CreateSubscription.body": {
|
"handler.CreateSubscription.body": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"channel",
|
"channel_id",
|
||||||
|
"channel_internal_name",
|
||||||
"channel_owner_user_id"
|
"channel_owner_user_id"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"channel": {
|
"channel_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"channel_internal_name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"channel_owner_user_id": {
|
"channel_owner_user_id": {
|
||||||
|
@ -60,12 +60,15 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
handler.CreateSubscription.body:
|
handler.CreateSubscription.body:
|
||||||
properties:
|
properties:
|
||||||
channel:
|
channel_id:
|
||||||
|
type: integer
|
||||||
|
channel_internal_name:
|
||||||
type: string
|
type: string
|
||||||
channel_owner_user_id:
|
channel_owner_user_id:
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- channel
|
- channel_id
|
||||||
|
- channel_internal_name
|
||||||
- channel_owner_user_id
|
- channel_owner_user_id
|
||||||
type: object
|
type: object
|
||||||
handler.CreateUser.body:
|
handler.CreateUser.body:
|
||||||
@ -1315,7 +1318,6 @@ paths:
|
|||||||
- all_any
|
- all_any
|
||||||
in: query
|
in: query
|
||||||
name: selector
|
name: selector
|
||||||
required: true
|
|
||||||
type: string
|
type: string
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
@ -1697,6 +1699,14 @@ paths:
|
|||||||
- API-v2
|
- API-v2
|
||||||
/api/users/{uid}/subscriptions:
|
/api/users/{uid}/subscriptions:
|
||||||
get:
|
get:
|
||||||
|
description: |-
|
||||||
|
The possible values for 'selector' are:
|
||||||
|
- "owner_all" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels)
|
||||||
|
- "owner_confirmed" Confirmed subscriptions with the user as owner
|
||||||
|
- "owner_unconfirmed" Unconfirmed (Pending) subscriptions with the user as owner
|
||||||
|
- "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
|
||||||
|
- "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
|
||||||
|
- "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
|
||||||
operationId: api-user-subscriptions-list
|
operationId: api-user-subscriptions-list
|
||||||
parameters:
|
parameters:
|
||||||
- description: UserID
|
- description: UserID
|
||||||
@ -1704,7 +1714,7 @@ paths:
|
|||||||
name: uid
|
name: uid
|
||||||
required: true
|
required: true
|
||||||
type: integer
|
type: integer
|
||||||
- description: 'Filter subscribptions (default: owner_all)'
|
- description: 'Filter subscriptions (default: owner_all)'
|
||||||
enum:
|
enum:
|
||||||
- owner_all
|
- owner_all
|
||||||
- owner_confirmed
|
- owner_confirmed
|
||||||
@ -1737,6 +1747,8 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- API-v2
|
- API-v2
|
||||||
post:
|
post:
|
||||||
|
description: Either [channel_owner_user_id, channel_internal_name] or [channel_id]
|
||||||
|
must be supplied in the request body
|
||||||
operationId: api-subscriptions-create
|
operationId: api-subscriptions-create
|
||||||
parameters:
|
parameters:
|
||||||
- description: UserID
|
- description: UserID
|
||||||
@ -1769,7 +1781,7 @@ paths:
|
|||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.apiError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: Creare/Request a subscription
|
summary: Create/Request a subscription
|
||||||
tags:
|
tags:
|
||||||
- API-v2
|
- API-v2
|
||||||
/api/users/{uid}/subscriptions/{sid}:
|
/api/users/{uid}/subscriptions/{sid}:
|
||||||
|
@ -161,10 +161,10 @@ func TestListChannelsOwned(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testdata := map[int][]string{
|
testdata := map[int][]string{
|
||||||
0: {"main", "chattingchamber", "unicdhll", "promotions", "reminders"},
|
0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
|
||||||
1: {"main", "private"},
|
1: {"main", "private"},
|
||||||
2: {"main", "ü", "ö", "ä"},
|
2: {"main", "ü", "ö", "ä"},
|
||||||
3: {"main", "innovations", "reminders"},
|
3: {"main", "\U0001f5ff", "innovations", "reminders"},
|
||||||
4: {"main"},
|
4: {"main"},
|
||||||
5: {"main", "test1", "test2", "test3", "test4", "test5"},
|
5: {"main", "test1", "test2", "test3", "test4", "test5"},
|
||||||
6: {"main", "security", "lipsum"},
|
6: {"main", "security", "lipsum"},
|
||||||
@ -175,8 +175,8 @@ func TestListChannelsOwned(t *testing.T) {
|
|||||||
11: {"promotions"},
|
11: {"promotions"},
|
||||||
12: {},
|
12: {},
|
||||||
13: {},
|
13: {},
|
||||||
14: {"", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
|
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
|
||||||
15: {"", "chan_other_nosub", "chan_other_request", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
|
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range testdata {
|
for k, v := range testdata {
|
||||||
@ -186,19 +186,143 @@ func TestListChannelsOwned(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestListChannelsSubscribedAny(t *testing.T) {
|
func TestListChannelsSubscribedAny(t *testing.T) {
|
||||||
t.SkipNow() //TODO
|
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
data := tt.InitDefaultData(t, ws)
|
||||||
|
|
||||||
|
type chanlist struct {
|
||||||
|
Channels []gin.H `json:"channels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testdata := map[int][]string{
|
||||||
|
0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
|
||||||
|
1: {"main", "private"},
|
||||||
|
2: {"main", "ü", "ö", "ä"},
|
||||||
|
3: {"main", "\U0001f5ff", "innovations", "reminders"},
|
||||||
|
4: {"main"},
|
||||||
|
5: {"main", "test1", "test2", "test3", "test4", "test5"},
|
||||||
|
6: {"main", "security", "lipsum"},
|
||||||
|
7: {"main"},
|
||||||
|
8: {"main"},
|
||||||
|
9: {"main", "manual@chan"},
|
||||||
|
10: {"main"},
|
||||||
|
11: {"promotions"},
|
||||||
|
12: {},
|
||||||
|
13: {},
|
||||||
|
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
|
||||||
|
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range testdata {
|
||||||
|
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
|
||||||
|
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListChannelsAllAny(t *testing.T) {
|
func TestListChannelsAllAny(t *testing.T) {
|
||||||
t.SkipNow() //TODO
|
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
data := tt.InitDefaultData(t, ws)
|
||||||
|
|
||||||
|
type chanlist struct {
|
||||||
|
Channels []gin.H `json:"channels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testdata := map[int][]string{
|
||||||
|
0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
|
||||||
|
1: {"main", "private"},
|
||||||
|
2: {"main", "ü", "ö", "ä"},
|
||||||
|
3: {"main", "\U0001f5ff", "innovations", "reminders"},
|
||||||
|
4: {"main"},
|
||||||
|
5: {"main", "test1", "test2", "test3", "test4", "test5"},
|
||||||
|
6: {"main", "security", "lipsum"},
|
||||||
|
7: {"main"},
|
||||||
|
8: {"main"},
|
||||||
|
9: {"main", "manual@chan"},
|
||||||
|
10: {"main"},
|
||||||
|
11: {"promotions"},
|
||||||
|
12: {},
|
||||||
|
13: {},
|
||||||
|
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
|
||||||
|
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range testdata {
|
||||||
|
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
|
||||||
|
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListChannelsSubscribed(t *testing.T) {
|
func TestListChannelsSubscribed(t *testing.T) {
|
||||||
t.SkipNow() //TODO
|
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
data := tt.InitDefaultData(t, ws)
|
||||||
|
|
||||||
|
type chanlist struct {
|
||||||
|
Channels []gin.H `json:"channels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testdata := map[int][]string{
|
||||||
|
0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
|
||||||
|
1: {"main", "private"},
|
||||||
|
2: {"main", "ü", "ö", "ä"},
|
||||||
|
3: {"main", "\U0001f5ff", "innovations", "reminders"},
|
||||||
|
4: {"main"},
|
||||||
|
5: {"main", "test1", "test2", "test3", "test4", "test5"},
|
||||||
|
6: {"main", "security", "lipsum"},
|
||||||
|
7: {"main"},
|
||||||
|
8: {"main"},
|
||||||
|
9: {"main", "manual@chan"},
|
||||||
|
10: {"main"},
|
||||||
|
11: {"promotions"},
|
||||||
|
12: {},
|
||||||
|
13: {},
|
||||||
|
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
|
||||||
|
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range testdata {
|
||||||
|
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
|
||||||
|
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListChannelsAll(t *testing.T) {
|
func TestListChannelsAll(t *testing.T) {
|
||||||
t.SkipNow() //TODO
|
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
data := tt.InitDefaultData(t, ws)
|
||||||
|
|
||||||
|
type chanlist struct {
|
||||||
|
Channels []gin.H `json:"channels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testdata := map[int][]string{
|
||||||
|
0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
|
||||||
|
1: {"main", "private"},
|
||||||
|
2: {"main", "ü", "ö", "ä"},
|
||||||
|
3: {"main", "\U0001f5ff", "innovations", "reminders"},
|
||||||
|
4: {"main"},
|
||||||
|
5: {"main", "test1", "test2", "test3", "test4", "test5"},
|
||||||
|
6: {"main", "security", "lipsum"},
|
||||||
|
7: {"main"},
|
||||||
|
8: {"main"},
|
||||||
|
9: {"main", "manual@chan"},
|
||||||
|
10: {"main"},
|
||||||
|
11: {"promotions"},
|
||||||
|
12: {},
|
||||||
|
13: {},
|
||||||
|
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
|
||||||
|
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range testdata {
|
||||||
|
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
|
||||||
|
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO test missing channel-xx methods
|
//TODO test missing channel-xx methods
|
||||||
|
@ -257,8 +257,8 @@ var messageExamples = []msgex{
|
|||||||
{11, "Promotions", "localhost", P2, SKEY, "New Product Launch: Introducing Our Latest Innovation", "We are excited to announce the release of our newest product, designed to revolutionize the industry. Don't miss out on this game-changing technology.", timeext.FromHours(-12.21)},
|
{11, "Promotions", "localhost", P2, SKEY, "New Product Launch: Introducing Our Latest Innovation", "We are excited to announce the release of our newest product, designed to revolutionize the industry. Don't miss out on this game-changing technology.", timeext.FromHours(-12.21)},
|
||||||
{11, "Promotions", "#S0", P0, SKEY, "Limited Time Offer: Get 50% Off Your Next Purchase", "For a limited time, take advantage of our special offer and get half off your next purchase. Don't miss out on this amazing deal.", 0},
|
{11, "Promotions", "#S0", P0, SKEY, "Limited Time Offer: Get 50% Off Your Next Purchase", "For a limited time, take advantage of our special offer and get half off your next purchase. Don't miss out on this amazing deal.", 0},
|
||||||
{11, "Promotions", "#S0", P2, SKEY, "Customer Appreciation Sale: Save Up to 75% on Your Favorite Products", "", 0},
|
{11, "Promotions", "#S0", P2, SKEY, "Customer Appreciation Sale: Save Up to 75% on Your Favorite Products", "", 0},
|
||||||
{11, "Promotions", "", P0, SKEY, "Sign Up for Our Newsletter and Save 10% on Your Next Order", "", 0},
|
{11, " Promotions", "", P0, SKEY, "Sign Up for Our Newsletter and Save 10% on Your Next Order", "", 0},
|
||||||
{11, "Promotions", "", PX, AKEY, "New Arrivals: Check Out Our Latest Collection", "We've just added new items to our collection and we think you'll love them. Take a look and see what's new in fashion, home decor, and more.", 0},
|
{11, "Promotions ", "", PX, AKEY, "New Arrivals: Check Out Our Latest Collection", "We've just added new items to our collection and we think you'll love them. Take a look and see what's new in fashion, home decor, and more.", 0},
|
||||||
{11, "Promotions", "", PX, AKEY, "Join Our Rewards Program and Earn Points on Every Purchase", "Sign up for our rewards program and earn points on every purchase you make. Redeem your points for discounts, free products, and more.", 0},
|
{11, "Promotions", "", PX, AKEY, "Join Our Rewards Program and Earn Points on Every Purchase", "Sign up for our rewards program and earn points on every purchase you make. Redeem your points for discounts, free products, and more.", 0},
|
||||||
{11, "Promotions", "#S0", P0, SKEY, "Seasonal Special: Save on Your Favorite Fall Products", "As the leaves change color and the air gets cooler, we have the perfect products to help you enjoy the season. Take advantage of our special offers and save on your favorite fall products.", 0},
|
{11, "Promotions", "#S0", P0, SKEY, "Seasonal Special: Save on Your Favorite Fall Products", "As the leaves change color and the air gets cooler, we have the perfect products to help you enjoy the season. Take advantage of our special offers and save on your favorite fall products.", 0},
|
||||||
{11, "Promotions", "192.168.0.1", P1, AKEY, "Refer a Friend and Save on Your Next Order", "Share the love and refer a friend to our store. When they make a purchase, you'll receive a discount on your next order. It's a win-win for both of you.", 0},
|
{11, "Promotions", "192.168.0.1", P1, AKEY, "Refer a Friend and Save on Your Next Order", "Share the love and refer a friend to our store. When they make a purchase, you'll receive a discount on your next order. It's a win-win for both of you.", 0},
|
||||||
@ -384,10 +384,10 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
|
|||||||
// Sub/Unsub for Users 12+13
|
// Sub/Unsub for Users 12+13
|
||||||
|
|
||||||
{
|
{
|
||||||
//TODO User 12 unsubscribe from 12:chan_self_unsub
|
doUnsubscribe(t, baseUrl, users[14], users[14], "chan_self_unsub")
|
||||||
//TODO User 13 request-subscribe to 13:chan_other_request
|
doSubscribe(t, baseUrl, users[14], users[15], "chan_other_request")
|
||||||
//TODO User 13 request-subscribe to 13:chan_other_accepted
|
doSubscribe(t, baseUrl, users[14], users[15], "chan_other_accepted")
|
||||||
//TODO User 13 accept subscription from user 12 to 13:chan_other_accepted
|
doAcceptSub(t, baseUrl, users[15], users[14], "chan_other_accepted")
|
||||||
}
|
}
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
@ -395,6 +395,78 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
|
|||||||
return DefData{User: users}
|
return DefData{User: users}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func doSubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat, chanInternalName string) {
|
||||||
|
|
||||||
|
if user == chanOwner {
|
||||||
|
|
||||||
|
RequestAuthPost[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", user.UID), gin.H{
|
||||||
|
"channel_owner_user_id": chanOwner.UID,
|
||||||
|
"channel_internal_name": chanInternalName,
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
type chanlist struct {
|
||||||
|
Channels []gin.H `json:"channels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
clist := RequestAuthGet[chanlist](t, chanOwner.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels?selector=owned", chanOwner.UID))
|
||||||
|
|
||||||
|
var chandat gin.H
|
||||||
|
for _, v := range clist.Channels {
|
||||||
|
if v["internal_name"].(string) == chanInternalName {
|
||||||
|
chandat = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestAuthPost[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions?chan_subscribe_key=%s", user.UID, chandat["subscribe_key"].(string)), gin.H{
|
||||||
|
"channel_id": chandat["channel_id"].(float64),
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func doUnsubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat, chanInternalName string) {
|
||||||
|
|
||||||
|
type chanlist struct {
|
||||||
|
Subscriptions []gin.H `json:"subscriptions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions?selector=outgoing_confirmed", user.UID))
|
||||||
|
|
||||||
|
var subdat gin.H
|
||||||
|
for _, v := range slist.Subscriptions {
|
||||||
|
if v["channel_internal_name"].(string) == chanInternalName && int64(v["channel_owner_user_id"].(float64)) == chanOwner.UID {
|
||||||
|
subdat = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestAuthDelete[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions/%v", user.UID, subdat["subscription_id"]), gin.H{})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func doAcceptSub(t *testing.T, baseUrl string, user Userdat, subscriber Userdat, chanInternalName string) {
|
||||||
|
|
||||||
|
type chanlist struct {
|
||||||
|
Subscriptions []gin.H `json:"subscriptions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions?selector=incoming_unconfirmed", user.UID))
|
||||||
|
|
||||||
|
var subdat gin.H
|
||||||
|
for _, v := range slist.Subscriptions {
|
||||||
|
if v["channel_internal_name"].(string) == chanInternalName && int64(v["subscriber_user_id"].(float64)) == subscriber.UID {
|
||||||
|
subdat = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestAuthDelete[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions/%v", user.UID, subdat["subscription_id"]), gin.H{})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func Lipsum(seed int64, paracount int) string {
|
func Lipsum(seed int64, paracount int) string {
|
||||||
return loremipsum.NewWithSeed(seed).Paragraphs(paracount)
|
return loremipsum.NewWithSeed(seed).Paragraphs(paracount)
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,7 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s
|
|||||||
TPrintln("")
|
TPrintln("")
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
TestFail(t, "Statuscode != 200")
|
TestFailFmt(t, "Statuscode != 200 (actual = %d)", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var data TResult
|
var data TResult
|
||||||
|
Loading…
Reference in New Issue
Block a user