Refactor API of `/api/v2/users/{uid}/subscriptions`

This commit is contained in:
Mike Schwörer 2023-07-30 15:58:37 +02:00
parent 8a6719fc19
commit 165c6d8614
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
23 changed files with 430 additions and 218 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ package models
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
const ChecksumGenerator = "a1a684aa30d77d9a9936ccbb667b498c370a1f816273e9cd93948f4195155e90"
const ChecksumGenerator = "a41b8d265c326a65d7be07c74aa2318064c6307256bd92b684c5adb4a8f82d97"
type Enum interface {
Valid() bool

View File

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

View File

@ -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 <SubscriberUserID> again
ChannelOwnerUserID *[]UserID
ChannelOwnerUserID2 *[]UserID // Used to filter <ChannelOwnerUserID> 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))]
}

View File

@ -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"
],

View File

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

View File

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

View File

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

View File

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