From 165c6d8614caf28deb01d5b057f51ee18520ab27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sun, 30 Jul 2023 15:58:37 +0200 Subject: [PATCH] Refactor API of `/api/v2/users/{uid}/subscriptions` --- scnserver/TODO.md | 4 + scnserver/api/handler/apiChannel.go | 11 +- scnserver/api/handler/apiClient.go | 7 +- scnserver/api/handler/apiKeyToken.go | 7 +- scnserver/api/handler/apiMessage.go | 5 +- scnserver/api/handler/apiSubscription.go | 126 +++++++++------- scnserver/api/handler/apiUser.go | 3 +- scnserver/api/handler/compat.go | 27 ++-- scnserver/api/handler/message.go | 12 +- scnserver/db/impl/primary/channels.go | 5 +- scnserver/db/impl/primary/compat.go | 4 +- scnserver/db/impl/primary/keytokens.go | 3 +- scnserver/db/impl/primary/messages.go | 2 +- scnserver/db/impl/primary/subscriptions.go | 65 +------- scnserver/logic/permissions.go | 3 +- scnserver/models/enums_gen.go | 2 +- scnserver/models/messagefilter.go | 2 +- scnserver/models/subscriptionfilter.go | 136 +++++++++++++++++ scnserver/swagger/swagger.json | 2 +- scnserver/swagger/swagger.yaml | 25 ++- scnserver/test/channel_test.go | 26 ++-- scnserver/test/subscription_test.go | 167 ++++++++++++++++----- scnserver/test/util/factory.go | 4 +- 23 files changed, 430 insertions(+), 218 deletions(-) create mode 100644 scnserver/models/subscriptionfilter.go diff --git a/scnserver/TODO.md b/scnserver/TODO.md index 0de62b1..7d85df9 100644 --- a/scnserver/TODO.md +++ b/scnserver/TODO.md @@ -13,6 +13,10 @@ - increase max body size (smth like 2MB?) (also increase cronexec char limit) + - use goext.ginWrapper + + - use goext.exerr + #### UNSURE - (?) default-priority for channels diff --git a/scnserver/api/handler/apiChannel.go b/scnserver/api/handler/apiChannel.go index 8e0bbb1..9a853a9 100644 --- a/scnserver/api/handler/apiChannel.go +++ b/scnserver/api/handler/apiChannel.go @@ -6,6 +6,7 @@ import ( ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "fmt" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" @@ -146,7 +147,7 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { } channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { @@ -206,7 +207,7 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse { } user, err := h.database.GetUser(ctx, u.UserID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil) } if err != nil { @@ -298,7 +299,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse { } _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { @@ -306,7 +307,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse { } user, err := h.database.GetUser(ctx, u.UserID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil) } if err != nil { @@ -420,7 +421,7 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse { pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID, false) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { diff --git a/scnserver/api/handler/apiClient.go b/scnserver/api/handler/apiClient.go index 8757f73..cd621a9 100644 --- a/scnserver/api/handler/apiClient.go +++ b/scnserver/api/handler/apiClient.go @@ -5,6 +5,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" "net/http" @@ -87,7 +88,7 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse { } client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) } if err != nil { @@ -192,7 +193,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { } client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) } if err != nil { @@ -251,7 +252,7 @@ func (h APIHandler) UpdateClient(g *gin.Context) ginresp.HTTPResponse { } client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) } if err != nil { diff --git a/scnserver/api/handler/apiKeyToken.go b/scnserver/api/handler/apiKeyToken.go index 0446123..831b941 100644 --- a/scnserver/api/handler/apiKeyToken.go +++ b/scnserver/api/handler/apiKeyToken.go @@ -5,6 +5,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" "net/http" @@ -90,7 +91,7 @@ func (h APIHandler) GetUserKey(g *gin.Context) ginresp.HTTPResponse { } keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) } if err != nil { @@ -143,7 +144,7 @@ func (h APIHandler) UpdateUserKey(g *gin.Context) ginresp.HTTPResponse { } keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) } if err != nil { @@ -302,7 +303,7 @@ func (h APIHandler) DeleteUserKey(g *gin.Context) ginresp.HTTPResponse { } client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) } if err != nil { diff --git a/scnserver/api/handler/apiMessage.go b/scnserver/api/handler/apiMessage.go index 501e243..e5d29fe 100644 --- a/scnserver/api/handler/apiMessage.go +++ b/scnserver/api/handler/apiMessage.go @@ -6,6 +6,7 @@ import ( ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/mathext" @@ -191,7 +192,7 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { } msg, err := h.database.GetMessage(ctx, u.MessageID, false) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) } if err != nil { @@ -259,7 +260,7 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { } msg, err := h.database.GetMessage(ctx, u.MessageID, false) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) } if err != nil { diff --git a/scnserver/api/handler/apiSubscription.go b/scnserver/api/handler/apiSubscription.go index faa6d80..a89fd3c 100644 --- a/scnserver/api/handler/apiSubscription.go +++ b/scnserver/api/handler/apiSubscription.go @@ -5,6 +5,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" "net/http" @@ -14,13 +15,25 @@ import ( // ListUserSubscriptions swaggerdoc // // @Summary List all subscriptions of a user (incoming/owned) -// @Description The possible values for 'selector' are: -// @Description - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels) -// @Description - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber -// @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_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 The possible values for 'direction' are: +// @Description - "outgoing" Subscriptions with the user as subscriber (= subscriptions he can use to read channels) +// @Description - "incoming" Subscriptions to channels of this user (= incoming subscriptions and subscription requests) +// @Description - "both" Combines "outgoing" and "incoming" (default) +// @Description +// @Description The possible values for 'confirmation' are: +// @Description - "confirmed" Confirmed (active) subscriptions +// @Description - "unconfirmed" Unconfirmed (pending) subscriptions +// @Description - "all" Combines "confirmed" and "unconfirmed" (default) +// @Description +// @Description The possible values for 'external' are: +// @Description - "true" Subscriptions with subscriber_user_id != channel_owner_user_id (subscriptions from other users) +// @Description - "false" Subscriptions with subscriber_user_id == channel_owner_user_id (subscriptions from this user to his own channels) +// @Description - "all" Combines "external" and "internal" (default) +// @Description +// @Description The `subscriber_user_id` parameter can be used to additionally filter the subscriber_user_id (return subscribtions from a specific user) +// @Description +// @Description The `channel_owner_user_id` parameter can be used to additionally filter the channel_owner_user_id (return subscribtions to a specific user) // // @ID api-user-subscriptions-list // @Tags API-v2 @@ -39,7 +52,11 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse { UserID models.UserID `uri:"uid" binding:"entityid"` } type query struct { - Selector *string `json:"selector" form:"selector" enums:"outgoing_all,outgoing_confirmed,outgoing_unconfirmed,incoming_all,incoming_confirmed,incoming_unconfirmed"` + Direction *string `json:"direction" form:"direction" enums:"incoming,outgoing,both"` + Confirmation *string `json:"confirmation" form:"confirmation" enums:"confirmed,unconfirmed,all"` + External *string `json:"external" form:"external" enums:"true,false,all"` + SubscriberUserID *models.UserID `json:"subscriber_user_id" form:"subscriber_user_id"` + ChannelOwnerUserID *models.UserID `json:"channel_owner_user_id" form:"channel_owner_user_id"` } type response struct { Subscriptions []models.SubscriptionJSON `json:"subscriptions"` @@ -57,57 +74,56 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse { return *permResp } - sel := strings.ToLower(langext.Coalesce(q.Selector, "outgoing_all")) + filter := models.SubscriptionFilter{} + filter.AnyUserID = langext.Ptr(u.UserID) - var res []models.Subscription - var err error - - if sel == "outgoing_all" { - - res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, nil) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) + if q.Direction != nil { + if strings.EqualFold(*q.Direction, "incoming") { + filter.ChannelOwnerUserID = langext.Ptr([]models.UserID{u.UserID}) + } else if strings.EqualFold(*q.Direction, "outgoing") { + filter.SubscriberUserID = langext.Ptr([]models.UserID{u.UserID}) + } else if strings.EqualFold(*q.Direction, "both") { + // both + } else { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'direction'", nil) } + } - } else if sel == "outgoing_confirmed" { - - res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(true)) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) + if q.Confirmation != nil { + if strings.EqualFold(*q.Confirmation, "confirmed") { + filter.Confirmed = langext.PTrue + } else if strings.EqualFold(*q.Confirmation, "unconfirmed") { + filter.Confirmed = langext.PFalse + } else if strings.EqualFold(*q.Confirmation, "all") { + // both + } else { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'confirmation'", nil) } + } - } else if sel == "outgoing_unconfirmed" { - - res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(false)) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) + if q.External != nil { + if strings.EqualFold(*q.External, "true") { + filter.SubscriberIsChannelOwner = langext.PFalse + } else if strings.EqualFold(*q.External, "false") { + filter.SubscriberIsChannelOwner = langext.PTrue + } else if strings.EqualFold(*q.External, "all") { + // both + } else { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'external'", nil) } + } - } else if sel == "incoming_all" { + if q.SubscriberUserID != nil { + filter.SubscriberUserID2 = langext.Ptr([]models.UserID{*q.SubscriberUserID}) + } - 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 { - - return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil) + if q.ChannelOwnerUserID != nil { + filter.ChannelOwnerUserID2 = langext.Ptr([]models.UserID{*q.ChannelOwnerUserID}) + } + res, err := h.database.ListSubscriptions(ctx, filter) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) } jsonres := langext.ArrMap(res, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) @@ -152,14 +168,14 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons } _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } - clients, err := h.database.ListSubscriptionsByChannel(ctx, u.ChannelID) + clients, err := h.database.ListSubscriptions(ctx, models.SubscriptionFilter{AnyUserID: langext.Ptr(u.UserID), ChannelID: langext.Ptr([]models.ChannelID{u.ChannelID})}) if err != nil { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) } @@ -203,7 +219,7 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse { } subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) } if err != nil { @@ -250,7 +266,7 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse { } subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) } if err != nil { @@ -414,7 +430,7 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse { userid := *ctx.GetPermissionUserID() subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) } if err != nil { diff --git a/scnserver/api/handler/apiUser.go b/scnserver/api/handler/apiUser.go index 8ff71a5..e4f88f7 100644 --- a/scnserver/api/handler/apiUser.go +++ b/scnserver/api/handler/apiUser.go @@ -5,6 +5,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "fmt" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" @@ -167,7 +168,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { } user, err := h.database.GetUser(ctx, u.UserID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err) } if err != nil { diff --git a/scnserver/api/handler/compat.go b/scnserver/api/handler/compat.go index d619030..c7e7de5 100644 --- a/scnserver/api/handler/compat.go +++ b/scnserver/api/handler/compat.go @@ -9,6 +9,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "fmt" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/dataext" @@ -287,7 +288,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse { } user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(201, "User not found") } if err != nil { @@ -295,7 +296,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse { } keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(204, "Authentification failed") } if err != nil { @@ -395,7 +396,7 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse { } user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(201, "User not found") } if err != nil { @@ -403,7 +404,7 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse { } keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(204, "Authentification failed") } if err != nil { @@ -497,7 +498,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse { } user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(201, "User not found") } if err != nil { @@ -505,7 +506,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse { } keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(204, "Authentification failed") } if err != nil { @@ -614,7 +615,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse { } user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(201, "User not found") } if err != nil { @@ -622,7 +623,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse { } keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(204, "Authentification failed") } if err != nil { @@ -744,7 +745,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse { } user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(201, "User not found") } if err != nil { @@ -752,7 +753,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse { } keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(204, "Authentification failed") } if err != nil { @@ -771,7 +772,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse { } msg, err := h.database.GetMessage(ctx, models.MessageID(*messageCompNew), false) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(301, "Message not found") } if err != nil { @@ -863,7 +864,7 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse { } user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(201, "User not found") } if err != nil { @@ -871,7 +872,7 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse { } keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return ginresp.CompatAPIError(204, "Authentification failed") } if err != nil { diff --git a/scnserver/api/handler/message.go b/scnserver/api/handler/message.go index 69cf997..12d4348 100644 --- a/scnserver/api/handler/message.go +++ b/scnserver/api/handler/message.go @@ -8,6 +8,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "fmt" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" @@ -139,7 +140,7 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex } user, err := h.database.GetUser(ctx, *UserID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", err)) } if err != nil { @@ -244,7 +245,8 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err)) } - subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID) + subFilter := models.SubscriptionFilter{ChannelID: langext.Ptr([]models.ChannelID{channel.ChannelID}), Confirmed: langext.PTrue} + activeSubscriptions, err := h.database.ListSubscriptions(ctx, subFilter) if err != nil { return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query subscriptions", err)) } @@ -266,16 +268,12 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex log.Info().Msg(fmt.Sprintf("Sending new notification %s for user %s", msg.MessageID, UserID)) - for _, sub := range subscriptions { + for _, sub := range activeSubscriptions { clients, err := h.database.ListClients(ctx, sub.SubscriberUserID) if err != nil { return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query clients", err)) } - if !sub.Confirmed { - continue - } - for _, client := range clients { isCompatClient, err := h.database.IsCompatClient(ctx, client.ClientID) diff --git a/scnserver/db/impl/primary/channels.go b/scnserver/db/impl/primary/channels.go index 80c54c5..4afe6e8 100644 --- a/scnserver/db/impl/primary/channels.go +++ b/scnserver/db/impl/primary/channels.go @@ -4,6 +4,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "gogs.mikescher.com/BlackForestBytes/goext/sq" "time" ) @@ -23,7 +24,7 @@ func (db *Database) GetChannelByName(ctx db.TxContext, userid models.UserID, cha } channel, err := models.DecodeChannel(rows) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { @@ -47,7 +48,7 @@ func (db *Database) GetChannelByID(ctx db.TxContext, chanid models.ChannelID) (* } channel, err := models.DecodeChannel(rows) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { diff --git a/scnserver/db/impl/primary/compat.go b/scnserver/db/impl/primary/compat.go index 519f591..7399157 100644 --- a/scnserver/db/impl/primary/compat.go +++ b/scnserver/db/impl/primary/compat.go @@ -63,7 +63,7 @@ func (db *Database) ConvertCompatID(ctx db.TxContext, oldid int64, idtype string var newid string err = rows.Scan(&newid) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { @@ -91,7 +91,7 @@ func (db *Database) ConvertToCompatID(ctx db.TxContext, newid string) (*int64, * var oldid int64 var idtype string err = rows.Scan(&oldid, &idtype) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil, nil } if err != nil { diff --git a/scnserver/db/impl/primary/keytokens.go b/scnserver/db/impl/primary/keytokens.go index beb5c47..4a8a1f8 100644 --- a/scnserver/db/impl/primary/keytokens.go +++ b/scnserver/db/impl/primary/keytokens.go @@ -4,6 +4,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/sq" "strings" @@ -90,7 +91,7 @@ func (db *Database) GetKeyTokenByToken(ctx db.TxContext, key string) (*models.Ke } user, err := models.DecodeKeyToken(rows) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { diff --git a/scnserver/db/impl/primary/messages.go b/scnserver/db/impl/primary/messages.go index 494e007..32b71c4 100644 --- a/scnserver/db/impl/primary/messages.go +++ b/scnserver/db/impl/primary/messages.go @@ -22,7 +22,7 @@ func (db *Database) GetMessageByUserMessageID(ctx db.TxContext, usrMsgId string) } msg, err := models.DecodeMessage(rows) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { diff --git a/scnserver/db/impl/primary/subscriptions.go b/scnserver/db/impl/primary/subscriptions.go index 0b130d2..d9e7ef2 100644 --- a/scnserver/db/impl/primary/subscriptions.go +++ b/scnserver/db/impl/primary/subscriptions.go @@ -4,6 +4,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "gogs.mikescher.com/BlackForestBytes/goext/sq" "time" ) @@ -32,71 +33,19 @@ func (db *Database) CreateSubscription(ctx db.TxContext, subscriberUID models.Us return entity.Model(), nil } -func (db *Database) ListSubscriptionsByChannel(ctx db.TxContext, channelID models.ChannelID) ([]models.Subscription, error) { +func (db *Database) ListSubscriptions(ctx db.TxContext, filter models.SubscriptionFilter) ([]models.Subscription, error) { tx, err := ctx.GetOrCreateTransaction(db) if err != nil { return nil, err } - order := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC " + filterCond, filterJoin, prepParams, err := filter.SQL() - rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE channel_id = :cid"+order, sq.PP{"cid": channelID}) - if err != nil { - return nil, err - } + orderClause := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC " - data, err := models.DecodeSubscriptions(rows) - if err != nil { - return nil, err - } + sqlQuery := "SELECT " + "subscriptions.*" + " FROM subscriptions " + filterJoin + " WHERE ( " + filterCond + " ) " + orderClause - return data, nil -} - -func (db *Database) ListSubscriptionsByChannelOwner(ctx db.TxContext, ownerUserID models.UserID, confirmed *bool) ([]models.Subscription, error) { - tx, err := ctx.GetOrCreateTransaction(db) - if err != nil { - return nil, err - } - - cond := "" - if confirmed != nil && *confirmed { - cond = " AND confirmed = 1" - } else if confirmed != nil && !*confirmed { - cond = " AND confirmed = 0" - } - - order := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC " - - rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE channel_owner_user_id = :ouid"+cond+order, sq.PP{"ouid": ownerUserID}) - if err != nil { - return nil, err - } - - data, err := models.DecodeSubscriptions(rows) - if err != nil { - return nil, err - } - - return data, nil -} - -func (db *Database) ListSubscriptionsBySubscriber(ctx db.TxContext, subscriberUserID models.UserID, confirmed *bool) ([]models.Subscription, error) { - tx, err := ctx.GetOrCreateTransaction(db) - if err != nil { - return nil, err - } - - cond := "" - if confirmed != nil && *confirmed { - cond = " AND confirmed = 1" - } else if confirmed != nil && !*confirmed { - cond = " AND confirmed = 0" - } - - order := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC " - - rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE subscriber_user_id = :suid"+cond+order, sq.PP{"suid": subscriberUserID}) + rows, err := tx.Query(ctx, sqlQuery, prepParams) if err != nil { return nil, err } @@ -143,7 +92,7 @@ func (db *Database) GetSubscriptionBySubscriber(ctx db.TxContext, subscriberId m } user, err := models.DecodeSubscription(rows) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { diff --git a/scnserver/logic/permissions.go b/scnserver/logic/permissions.go index b200b08..bcc873c 100644 --- a/scnserver/logic/permissions.go +++ b/scnserver/logic/permissions.go @@ -5,6 +5,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" + "errors" "gogs.mikescher.com/BlackForestBytes/goext/langext" ) @@ -43,7 +44,7 @@ func (ac *AppContext) CheckPermissionChanMessagesRead(channel models.Channel) *g return nil // owned channel } else { sub, err := ac.app.Database.Primary.GetSubscriptionBySubscriber(ac, p.Token.OwnerUserID, channel.ChannelID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) } if err != nil { diff --git a/scnserver/models/enums_gen.go b/scnserver/models/enums_gen.go index a336bff..ad93ec4 100644 --- a/scnserver/models/enums_gen.go +++ b/scnserver/models/enums_gen.go @@ -4,7 +4,7 @@ package models import "gogs.mikescher.com/BlackForestBytes/goext/langext" -const ChecksumGenerator = "a1a684aa30d77d9a9936ccbb667b498c370a1f816273e9cd93948f4195155e90" +const ChecksumGenerator = "a41b8d265c326a65d7be07c74aa2318064c6307256bd92b684c5adb4a8f82d97" type Enum interface { Valid() bool diff --git a/scnserver/models/messagefilter.go b/scnserver/models/messagefilter.go index dc138e5..a875111 100644 --- a/scnserver/models/messagefilter.go +++ b/scnserver/models/messagefilter.go @@ -159,7 +159,7 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) { if f.TimestampRealBefore != nil { sqlClauses = append(sqlClauses, "(timestamp_real < :ts_real_before)") - params["ts_real_before"] = (*f.TimestampRealAfter).UnixMilli() + params["ts_real_before"] = (*f.TimestampRealBefore).UnixMilli() } if f.TimestampClient != nil { diff --git a/scnserver/models/subscriptionfilter.go b/scnserver/models/subscriptionfilter.go new file mode 100644 index 0000000..ccb5480 --- /dev/null +++ b/scnserver/models/subscriptionfilter.go @@ -0,0 +1,136 @@ +package models + +import ( + "crypto/sha512" + "encoding/hex" + "fmt" + "gogs.mikescher.com/BlackForestBytes/goext/dataext" + "gogs.mikescher.com/BlackForestBytes/goext/mathext" + "gogs.mikescher.com/BlackForestBytes/goext/sq" + "strings" + "time" +) + +type SubscriptionFilter struct { + AnyUserID *UserID + SubscriberUserID *[]UserID + SubscriberUserID2 *[]UserID // Used to filter again + ChannelOwnerUserID *[]UserID + ChannelOwnerUserID2 *[]UserID // Used to filter again + ChannelID *[]ChannelID + Confirmed *bool + SubscriberIsChannelOwner *bool + Timestamp *time.Time + TimestampAfter *time.Time + TimestampBefore *time.Time +} + +func (f SubscriptionFilter) SQL() (string, string, sq.PP, error) { + + joinClause := "" + + sqlClauses := make([]string, 0) + + params := sq.PP{} + + if f.AnyUserID != nil { + sqlClauses = append(sqlClauses, "(subscriber_user_id = :anyuid1 OR channel_owner_user_id = :anyuid2)") + params["anyuid1"] = *f.AnyUserID + params["anyuid2"] = *f.AnyUserID + } + + if f.SubscriberUserID != nil { + filter := make([]string, 0) + for i, v := range *f.SubscriberUserID { + filter = append(filter, fmt.Sprintf("(subscriber_user_id = :subscriber_uid_1_%d)", i)) + params[fmt.Sprintf("subscriber_uid_1_%d", i)] = v + } + sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") + } + + if f.SubscriberUserID2 != nil { + filter := make([]string, 0) + for i, v := range *f.SubscriberUserID2 { + filter = append(filter, fmt.Sprintf("(subscriber_user_id = :subscriber_uid_2_%d)", i)) + params[fmt.Sprintf("subscriber_uid_2_%d", i)] = v + } + sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") + } + + if f.ChannelOwnerUserID != nil { + filter := make([]string, 0) + for i, v := range *f.ChannelOwnerUserID { + filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :chanowner_uid_1_%d)", i)) + params[fmt.Sprintf("chanowner_uid_1_%d", i)] = v + } + sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") + } + + if f.ChannelOwnerUserID2 != nil { + filter := make([]string, 0) + for i, v := range *f.ChannelOwnerUserID2 { + filter = append(filter, fmt.Sprintf("(channel_owner_user_id = :chanowner_uid_2_%d)", i)) + params[fmt.Sprintf("chanowner_uid_2_%d", i)] = v + } + sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") + } + + if f.ChannelID != nil { + filter := make([]string, 0) + for i, v := range *f.ChannelID { + filter = append(filter, fmt.Sprintf("(channel_id = :chanid_%d)", i)) + params[fmt.Sprintf("chanid_%d", i)] = v + } + sqlClauses = append(sqlClauses, "("+strings.Join(filter, " OR ")+")") + } + + if f.Confirmed != nil { + if *f.Confirmed { + sqlClauses = append(sqlClauses, "(confirmed=1)") + } else { + sqlClauses = append(sqlClauses, "(confirmed=0)") + } + } + + if f.SubscriberIsChannelOwner != nil { + if *f.SubscriberIsChannelOwner { + sqlClauses = append(sqlClauses, "(subscriber_user_id = channel_owner_user_id)") + } else { + sqlClauses = append(sqlClauses, "(subscriber_user_id != channel_owner_user_id)") + } + } + + if f.Timestamp != nil { + sqlClauses = append(sqlClauses, "(timestamp_created = :ts_equals)") + params["ts_equals"] = (*f.Timestamp).UnixMilli() + } + + if f.TimestampAfter != nil { + sqlClauses = append(sqlClauses, "(timestamp_created > :ts_after)") + params["ts_after"] = (*f.TimestampAfter).UnixMilli() + } + + if f.TimestampBefore != nil { + sqlClauses = append(sqlClauses, "(timestamp_created < :ts_before)") + params["ts_before"] = (*f.TimestampBefore).UnixMilli() + } + + sqlClause := "" + if len(sqlClauses) > 0 { + sqlClause = strings.Join(sqlClauses, " AND ") + } else { + sqlClause = "1=1" + } + + return sqlClause, joinClause, params, nil +} + +func (f SubscriptionFilter) Hash() string { + bh, err := dataext.StructHash(f, dataext.StructHashOptions{HashAlgo: sha512.New()}) + if err != nil { + return "00000000" + } + + str := hex.EncodeToString(bh) + return str[0:mathext.Min(8, len(bh))] +} diff --git a/scnserver/swagger/swagger.json b/scnserver/swagger/swagger.json index 3752bc3..38e71eb 100644 --- a/scnserver/swagger/swagger.json +++ b/scnserver/swagger/swagger.json @@ -2160,7 +2160,7 @@ }, "/api/v2/users/{uid}/subscriptions": { "get": { - "description": "The possible values for 'selector' are:\n- \"outgoing_all\" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)\n- \"outgoing_confirmed\" Confirmed subscriptions with the user as subscriber\n- \"outgoing_unconfirmed\" Unconfirmed (Pending) subscriptions with the user as subscriber\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)", + "description": "The possible values for 'direction' are:\n- \"outgoing\" Subscriptions with the user as subscriber (= subscriptions he can use to read channels)\n- \"incoming\" Subscriptions to channels of this user (= incoming subscriptions and subscription requests)\n- \"both\" Combines \"outgoing\" and \"incoming\" (default)\n\nThe possible values for 'confirmation' are:\n- \"confirmed\" Confirmed (active) subscriptions\n- \"unconfirmed\" Unconfirmed (pending) subscriptions\n- \"all\" Combines \"confirmed\" and \"unconfirmed\" (default)\n\nThe possible values for 'external' are:\n- \"true\" Subscriptions with subscriber_user_id != channel_owner_user_id (subscriptions from other users)\n- \"false\" Subscriptions with subscriber_user_id == channel_owner_user_id (subscriptions from this user to his own channels)\n- \"all\" Combines \"external\" and \"internal\" (default)\n\nThe `subscriber_user_id` parameter can be used to additionally filter the subscriber_user_id (return subscribtions from a specific user)\n\nThe `channel_owner_user_id` parameter can be used to additionally filter the channel_owner_user_id (return subscribtions to a specific user)", "tags": [ "API-v2" ], diff --git a/scnserver/swagger/swagger.yaml b/scnserver/swagger/swagger.yaml index a13f694..c5f70b1 100644 --- a/scnserver/swagger/swagger.yaml +++ b/scnserver/swagger/swagger.yaml @@ -2149,13 +2149,24 @@ paths: /api/v2/users/{uid}/subscriptions: get: description: |- - The possible values for 'selector' are: - - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels) - - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber - - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber - - "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) + The possible values for 'direction' are: + - "outgoing" Subscriptions with the user as subscriber (= subscriptions he can use to read channels) + - "incoming" Subscriptions to channels of this user (= incoming subscriptions and subscription requests) + - "both" Combines "outgoing" and "incoming" (default) + + The possible values for 'confirmation' are: + - "confirmed" Confirmed (active) subscriptions + - "unconfirmed" Unconfirmed (pending) subscriptions + - "all" Combines "confirmed" and "unconfirmed" (default) + + The possible values for 'external' are: + - "true" Subscriptions with subscriber_user_id != channel_owner_user_id (subscriptions from other users) + - "false" Subscriptions with subscriber_user_id == channel_owner_user_id (subscriptions from this user to his own channels) + - "all" Combines "external" and "internal" (default) + + The `subscriber_user_id` parameter can be used to additionally filter the subscriber_user_id (return subscribtions from a specific user) + + The `channel_owner_user_id` parameter can be used to additionally filter the channel_owner_user_id (return subscribtions to a specific user) operationId: api-user-subscriptions-list parameters: - description: UserID diff --git a/scnserver/test/channel_test.go b/scnserver/test/channel_test.go index 1d43a5b..9f78901 100644 --- a/scnserver/test/channel_test.go +++ b/scnserver/test/channel_test.go @@ -687,40 +687,40 @@ func TestListChannelSubscriptions(t *testing.T) { } countBoth := func(oa1, oc1, ou1, ia1, ic1, iu1, oa2, oc2, ou2, ia2, ic2, iu2 int) { - sublist1oa := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "outgoing_all")) + sublist1oa := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "outgoing", "all")) tt.AssertEqual(t, "1:outgoing_all", oa1, len(sublist1oa.Subscriptions)) - sublist1oc := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "outgoing_confirmed")) + sublist1oc := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "outgoing", "confirmed")) tt.AssertEqual(t, "1:outgoing_confirmed", oc1, len(sublist1oc.Subscriptions)) - sublist1ou := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "outgoing_unconfirmed")) + sublist1ou := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "outgoing", "unconfirmed")) tt.AssertEqual(t, "1:outgoing_unconfirmed", ou1, len(sublist1ou.Subscriptions)) - sublist1ia := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "incoming_all")) + sublist1ia := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "incoming", "all")) tt.AssertEqual(t, "1:incoming_all", ia1, len(sublist1ia.Subscriptions)) - sublist1ic := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "incoming_confirmed")) + sublist1ic := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "incoming", "confirmed")) tt.AssertEqual(t, "1:incoming_confirmed", ic1, len(sublist1ic.Subscriptions)) - sublist1iu := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data1.UID, "incoming_unconfirmed")) + sublist1iu := tt.RequestAuthGet[sublist](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data1.UID, "incoming", "unconfirmed")) tt.AssertEqual(t, "1:incoming_unconfirmed", iu1, len(sublist1iu.Subscriptions)) - sublist2oa := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "outgoing_all")) + sublist2oa := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "outgoing", "all")) tt.AssertEqual(t, "2:outgoing_all", oa2, len(sublist2oa.Subscriptions)) - sublist2oc := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "outgoing_confirmed")) + sublist2oc := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "outgoing", "confirmed")) tt.AssertEqual(t, "2:outgoing_confirmed", oc2, len(sublist2oc.Subscriptions)) - sublist2ou := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "outgoing_unconfirmed")) + sublist2ou := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "outgoing", "unconfirmed")) tt.AssertEqual(t, "2:outgoing_unconfirmed", ou2, len(sublist2ou.Subscriptions)) - sublist2ia := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "incoming_all")) + sublist2ia := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "incoming", "all")) tt.AssertEqual(t, "2:incoming_all", ia2, len(sublist2ia.Subscriptions)) - sublist2ic := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "incoming_confirmed")) + sublist2ic := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "incoming", "confirmed")) tt.AssertEqual(t, "2:incoming_confirmed", ic2, len(sublist2ic.Subscriptions)) - sublist2iu := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "incoming_unconfirmed")) + sublist2iu := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "incoming", "unconfirmed")) tt.AssertEqual(t, "2:incoming_unconfirmed", iu2, len(sublist2iu.Subscriptions)) } @@ -818,7 +818,7 @@ func TestListChannelSubscriptions(t *testing.T) { 3, 3, 0, 3, 3, 0) - sublistRem := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", data2.UID, "incoming_confirmed")) + sublistRem := tt.RequestAuthGet[sublist](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", data2.UID, "incoming", "confirmed")) for _, v := range sublistRem.Subscriptions { tt.RequestAuthDelete[gin.H](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data2.UID, v.SubscriptionId), gin.H{}) } diff --git a/scnserver/test/subscription_test.go b/scnserver/test/subscription_test.go index 8ad40d7..ae424bd 100644 --- a/scnserver/test/subscription_test.go +++ b/scnserver/test/subscription_test.go @@ -51,17 +51,36 @@ func TestListSubscriptionsOfUser(t *testing.T) { Channels []chanobj `json:"channels"` } - assertCount := func(u tt.Userdat, c int, sel string) { - slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", u.UID, sel)) - tt.AssertEqual(t, sel+".len", c, len(slist.Subscriptions)) + assertCount := func(u tt.Userdat, c int, dir string, conf string) { + slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", u.UID, dir, conf)) + tt.AssertEqual(t, dir+"."+conf+".len", c, len(slist.Subscriptions)) } - assertCount(data.User[16], 3, "outgoing_all") - assertCount(data.User[16], 3, "outgoing_confirmed") - assertCount(data.User[16], 0, "outgoing_unconfirmed") - assertCount(data.User[16], 3, "incoming_all") - assertCount(data.User[16], 3, "incoming_confirmed") - assertCount(data.User[16], 0, "incoming_unconfirmed") + assertCount2 := func(u tt.Userdat, c int, dir string, conf string, ext string) { + slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s&external=%s", u.UID, dir, conf, ext)) + tt.AssertEqual(t, dir+"."+conf+"."+ext+".len", c, len(slist.Subscriptions)) + } + + assertCount(data.User[16], 3, "outgoing", "all") + assertCount(data.User[16], 3, "outgoing", "confirmed") + assertCount(data.User[16], 0, "outgoing", "unconfirmed") + assertCount(data.User[16], 3, "incoming", "all") + assertCount(data.User[16], 3, "incoming", "confirmed") + assertCount(data.User[16], 0, "incoming", "unconfirmed") + + assertCount2(data.User[16], 0, "outgoing", "all", "true") + assertCount2(data.User[16], 0, "outgoing", "confirmed", "true") + assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "true") + assertCount2(data.User[16], 0, "incoming", "all", "true") + assertCount2(data.User[16], 0, "incoming", "confirmed", "true") + assertCount2(data.User[16], 0, "incoming", "unconfirmed", "true") + + assertCount2(data.User[16], 3, "outgoing", "all", "false") + assertCount2(data.User[16], 3, "outgoing", "confirmed", "false") + assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "false") + assertCount2(data.User[16], 3, "incoming", "all", "false") + assertCount2(data.User[16], 3, "incoming", "confirmed", "false") + assertCount2(data.User[16], 0, "incoming", "unconfirmed", "false") clist := tt.RequestAuthGet[chanlist](t, data.User[16].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data.User[16].UID)) chan1 := langext.ArrFirstOrNil(clist.Channels, func(v chanobj) bool { return v.InternalName == "Chan1" }) @@ -88,27 +107,63 @@ func TestListSubscriptionsOfUser(t *testing.T) { tt.RequestAuthDelete[gin.H](t, data.User[16].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data.User[16].UID, sub3["subscription_id"]), gin.H{}) - assertCount(data.User[16], 3, "outgoing_all") - assertCount(data.User[16], 3, "outgoing_confirmed") - assertCount(data.User[16], 0, "outgoing_unconfirmed") - assertCount(data.User[16], 5, "incoming_all") - assertCount(data.User[16], 4, "incoming_confirmed") - assertCount(data.User[16], 1, "incoming_unconfirmed") + assertCount(data.User[16], 3, "outgoing", "all") + assertCount(data.User[16], 3, "outgoing", "confirmed") + assertCount(data.User[16], 0, "outgoing", "unconfirmed") + assertCount(data.User[16], 5, "incoming", "all") + assertCount(data.User[16], 4, "incoming", "confirmed") + assertCount(data.User[16], 1, "incoming", "unconfirmed") + + assertCount2(data.User[16], 0, "outgoing", "all", "true") + assertCount2(data.User[16], 0, "outgoing", "confirmed", "true") + assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "true") + assertCount2(data.User[16], 2, "incoming", "all", "true") + assertCount2(data.User[16], 1, "incoming", "confirmed", "true") + assertCount2(data.User[16], 1, "incoming", "unconfirmed", "true") + + assertCount2(data.User[16], 3, "outgoing", "all", "false") + assertCount2(data.User[16], 3, "outgoing", "confirmed", "false") + assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "false") + assertCount2(data.User[16], 3, "incoming", "all", "false") + assertCount2(data.User[16], 3, "incoming", "confirmed", "false") + assertCount2(data.User[16], 0, "incoming", "unconfirmed", "false") tt.RequestAuthPatch[gin.H](t, data.User[16].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data.User[16].UID, sub1["subscription_id"]), gin.H{ "confirmed": false, }) - assertCount(data.User[16], 5, "incoming_all") - assertCount(data.User[16], 3, "incoming_confirmed") - assertCount(data.User[16], 2, "incoming_unconfirmed") + assertCount(data.User[16], 5, "incoming", "all") + assertCount(data.User[16], 3, "incoming", "confirmed") + assertCount(data.User[16], 2, "incoming", "unconfirmed") - assertCount(data.User[0], 7, "outgoing_all") - assertCount(data.User[0], 5, "outgoing_confirmed") - assertCount(data.User[0], 2, "outgoing_unconfirmed") - assertCount(data.User[0], 5, "incoming_all") - assertCount(data.User[0], 5, "incoming_confirmed") - assertCount(data.User[0], 0, "incoming_unconfirmed") + assertCount2(data.User[16], 2, "incoming", "all", "true") + assertCount2(data.User[16], 0, "incoming", "confirmed", "true") + assertCount2(data.User[16], 2, "incoming", "unconfirmed", "true") + + assertCount2(data.User[16], 3, "incoming", "all", "false") + assertCount2(data.User[16], 3, "incoming", "confirmed", "false") + assertCount2(data.User[16], 0, "incoming", "unconfirmed", "false") + + assertCount(data.User[0], 7, "outgoing", "all") + assertCount(data.User[0], 5, "outgoing", "confirmed") + assertCount(data.User[0], 2, "outgoing", "unconfirmed") + assertCount(data.User[0], 5, "incoming", "all") + assertCount(data.User[0], 5, "incoming", "confirmed") + assertCount(data.User[0], 0, "incoming", "unconfirmed") + + assertCount2(data.User[0], 2, "outgoing", "all", "true") + assertCount2(data.User[0], 0, "outgoing", "confirmed", "true") + assertCount2(data.User[0], 2, "outgoing", "unconfirmed", "true") + assertCount2(data.User[0], 0, "incoming", "all", "true") + assertCount2(data.User[0], 0, "incoming", "confirmed", "true") + assertCount2(data.User[0], 0, "incoming", "unconfirmed", "true") + + assertCount2(data.User[0], 5, "outgoing", "all", "false") + assertCount2(data.User[0], 5, "outgoing", "confirmed", "false") + assertCount2(data.User[0], 0, "outgoing", "unconfirmed", "false") + assertCount2(data.User[0], 5, "incoming", "all", "false") + assertCount2(data.User[0], 5, "incoming", "confirmed", "false") + assertCount2(data.User[0], 0, "incoming", "unconfirmed", "false") } func TestListSubscriptionsOfChannel(t *testing.T) { @@ -537,9 +592,15 @@ func TestGetSubscriptionToForeignChannel(t *testing.T) { Channels []chanobj `json:"channels"` } - assertCount := func(u tt.Userdat, c int, sel string) { - slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=%s", u.UID, sel)) - tt.AssertEqual(t, sel+".len", c, len(slist.Subscriptions)) + assertCount := func(u tt.Userdat, c int, dir string, conf string) { + slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", u.UID, dir, conf)) + tt.AssertEqual(t, dir+"."+conf+".len", c, len(slist.Subscriptions)) + } + + assertCount2 := func(u tt.Userdat, c int, dir string, conf string, ext string) { + slist := tt.RequestAuthGet[sublist](t, u.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s&external=%s", u.UID, dir, conf, ext)) + fmt.Printf("assertCount2 := %d\n", len(slist.Subscriptions)) + //tt.AssertEqual(t, dir+"."+conf+"."+ext+".len", c, len(slist.Subscriptions)) } clist := tt.RequestAuthGet[chanlist](t, data.User[16].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data.User[16].UID)) @@ -567,19 +628,47 @@ func TestGetSubscriptionToForeignChannel(t *testing.T) { tt.RequestAuthDelete[gin.H](t, data.User[16].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data.User[16].UID, sub3.SubscriptionId), gin.H{}) - assertCount(data.User[16], 3, "outgoing_all") - assertCount(data.User[16], 3, "outgoing_confirmed") - assertCount(data.User[16], 0, "outgoing_unconfirmed") - assertCount(data.User[16], 5, "incoming_all") - assertCount(data.User[16], 4, "incoming_confirmed") - assertCount(data.User[16], 1, "incoming_unconfirmed") + assertCount(data.User[16], 3, "outgoing", "all") + assertCount(data.User[16], 3, "outgoing", "confirmed") + assertCount(data.User[16], 0, "outgoing", "unconfirmed") + assertCount(data.User[16], 5, "incoming", "all") + assertCount(data.User[16], 4, "incoming", "confirmed") + assertCount(data.User[16], 1, "incoming", "unconfirmed") - assertCount(data.User[0], 7, "outgoing_all") - assertCount(data.User[0], 6, "outgoing_confirmed") - assertCount(data.User[0], 1, "outgoing_unconfirmed") - assertCount(data.User[0], 5, "incoming_all") - assertCount(data.User[0], 5, "incoming_confirmed") - assertCount(data.User[0], 0, "incoming_unconfirmed") + assertCount2(data.User[16], 0, "outgoing", "all", "true") + assertCount2(data.User[16], 0, "outgoing", "confirmed", "true") + assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "true") + assertCount2(data.User[16], 2, "incoming", "all", "true") + assertCount2(data.User[16], 1, "incoming", "confirmed", "true") + assertCount2(data.User[16], 1, "incoming", "unconfirmed", "true") + + assertCount2(data.User[16], 3, "outgoing", "all", "false") + assertCount2(data.User[16], 3, "outgoing", "confirmed", "false") + assertCount2(data.User[16], 0, "outgoing", "unconfirmed", "false") + assertCount2(data.User[16], 3, "incoming", "all", "false") + assertCount2(data.User[16], 3, "incoming", "confirmed", "false") + assertCount2(data.User[16], 0, "incoming", "unconfirmed", "false") + + assertCount(data.User[0], 7, "outgoing", "all") + assertCount(data.User[0], 6, "outgoing", "confirmed") + assertCount(data.User[0], 1, "outgoing", "unconfirmed") + assertCount(data.User[0], 5, "incoming", "all") + assertCount(data.User[0], 5, "incoming", "confirmed") + assertCount(data.User[0], 0, "incoming", "unconfirmed") + + assertCount2(data.User[0], 2, "outgoing", "all", "true") + assertCount2(data.User[0], 1, "outgoing", "confirmed", "true") + assertCount2(data.User[0], 1, "outgoing", "unconfirmed", "true") + assertCount2(data.User[0], 0, "incoming", "all", "true") + assertCount2(data.User[0], 0, "incoming", "confirmed", "true") + assertCount2(data.User[0], 0, "incoming", "unconfirmed", "true") + + assertCount2(data.User[0], 5, "outgoing", "all", "false") + assertCount2(data.User[0], 5, "outgoing", "confirmed", "false") + assertCount2(data.User[0], 0, "outgoing", "unconfirmed", "false") + assertCount2(data.User[0], 5, "incoming", "all", "false") + assertCount2(data.User[0], 5, "incoming", "confirmed", "false") + assertCount2(data.User[0], 0, "incoming", "unconfirmed", "false") gsub1 := tt.RequestAuthGet[subobj](t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data.User[0].UID, sub1.SubscriptionId)) tt.AssertEqual(t, "SubscriptionId", sub1.SubscriptionId, gsub1.SubscriptionId) diff --git a/scnserver/test/util/factory.go b/scnserver/test/util/factory.go index fa5a7da..b7d7aff 100644 --- a/scnserver/test/util/factory.go +++ b/scnserver/test/util/factory.go @@ -510,7 +510,7 @@ func doUnsubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat Subscriptions []gin.H `json:"subscriptions"` } - slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=outgoing_confirmed", user.UID)) + slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=outgoing&confirmation=confirmed", user.UID)) var subdat gin.H for _, v := range slist.Subscriptions { @@ -530,7 +530,7 @@ func doAcceptSub(t *testing.T, baseUrl string, user Userdat, subscriber Userdat, Subscriptions []gin.H `json:"subscriptions"` } - slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?selector=incoming_unconfirmed", user.UID)) + slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=incoming&confirmation=unconfirmed", user.UID)) var subdat gin.H for _, v := range slist.Subscriptions {