diff --git a/scnserver/api/handler/apiMessage.go b/scnserver/api/handler/apiMessage.go index c793d92..a28f936 100644 --- a/scnserver/api/handler/apiMessage.go +++ b/scnserver/api/handler/apiMessage.go @@ -24,6 +24,7 @@ import ( // @Description Simply start the pagination without a next_page_token and get the next page by calling this endpoint with the returned next_page_token of the last query // @Description If there are no more entries the token "@end" will be returned // @Description By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size) +// @Description By default returns only messages with an [active+confirmed] subscription, can supply subscription_status=all to als include inactive subscriptions or owned messages without subscriptions // @ID api-messages-list // @Tags API-v2 // @@ -37,19 +38,21 @@ import ( // @Router /api/v2/messages [GET] func (h APIHandler) ListMessages(pctx ginext.PreContext) ginext.HTTPResponse { type query struct { - PageSize *int `json:"page_size" form:"page_size"` - NextPageToken *string `json:"next_page_token" form:"next_page_token"` - Search []string `json:"search" form:"search"` - StringSearch []string `json:"string_search" form:"string_search"` - Trimmed *bool `json:"trimmed" form:"trimmed"` - Channels []string `json:"channel" form:"channel"` - ChannelIDs []string `json:"channel_id" form:"channel_id"` - Senders []string `json:"sender" form:"sender"` - TimeBefore *string `json:"before" form:"before"` // RFC3339 - TimeAfter *string `json:"after" form:"after"` // RFC3339 - Priority []int `json:"priority" form:"priority"` - KeyTokens []string `json:"used_key" form:"used_key"` - HasSender *bool `json:"has_sender" form:"has_sender"` + PageSize *int `json:"page_size" form:"page_size"` + NextPageToken *string `json:"next_page_token" form:"next_page_token"` + Search []string `json:"search" form:"search"` + StringSearch []string `json:"string_search" form:"string_search"` + Trimmed *bool `json:"trimmed" form:"trimmed"` + Channels []string `json:"channel" form:"channel"` + ChannelIDs []string `json:"channel_id" form:"channel_id"` + Senders []string `json:"sender" form:"sender"` + TimeBefore *string `json:"before" form:"before"` // RFC3339 + TimeAfter *string `json:"after" form:"after"` // RFC3339 + Priority []int `json:"priority" form:"priority"` + KeyTokens []string `json:"used_key" form:"used_key"` + HasSender *bool `json:"has_sender" form:"has_sender"` + SenderUserID []string `json:"sender_user_id" form:"sender_user_id"` + SubscriptionStatus *string `json:"subscription_status" form:"subscription_status" enums:"subscribed,all"` } type response struct { Messages []models.Message `json:"messages"` @@ -89,8 +92,30 @@ func (h APIHandler) ListMessages(pctx ginext.PreContext) ginext.HTTPResponse { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update last-read", err) } - filter := models.MessageFilter{ - ConfirmedAndActiveSubscriptionBy: langext.Ptr(userid), + filter := models.MessageFilter{} + + if q.SubscriptionStatus != nil { + if *q.SubscriptionStatus == "subscribed" { + filter.ConfirmedAndActiveSubscriptionBy = langext.Ptr(userid) + } else if *q.SubscriptionStatus == "all" { + filter.ConfirmedSubscriptionOrOwnedBy = langext.Ptr(userid) + } else { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'subscription_status'", nil) + } + } else { + filter.ConfirmedAndActiveSubscriptionBy = langext.Ptr(userid) // default + } + + if len(q.SenderUserID) != 0 { + uids := make([]models.UserID, 0, len(q.SenderUserID)) + for _, v := range q.SenderUserID { + uid := models.UserID(v) + if err = uid.Valid(); err != nil { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid sender-user-id", err) + } + uids = append(uids, uid) + } + filter.Sender = &uids } if len(q.Search) != 0 { diff --git a/scnserver/models/messagefilter.go b/scnserver/models/messagefilter.go index 188fd04..1ddffc7 100644 --- a/scnserver/models/messagefilter.go +++ b/scnserver/models/messagefilter.go @@ -15,6 +15,7 @@ import ( type MessageFilter struct { ConfirmedAndActiveSubscriptionBy *UserID + ConfirmedSubscriptionOrOwnedBy *UserID SearchStringFTS *[]string SearchStringPlain *[]string Sender *[]UserID @@ -49,7 +50,10 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) { joinClause := "" if f.ConfirmedAndActiveSubscriptionBy != nil { - joinClause += fmt.Sprintf(" LEFT JOIN subscriptions AS subs ON (messages.channel_id = subs.channel_id AND subs.subscriber_user_id = :%s AND subs.confirmed=1 AND subs.active=1 AND subs.deleted=0) ", params.Add(*f.ConfirmedAndActiveSubscriptionBy)) + joinClause += fmt.Sprintf(" LEFT JOIN subscriptions AS __filter_subs_1 ON (messages.channel_id = __filter_subs_1.channel_id AND __filter_subs_1.subscriber_user_id = :%s AND __filter_subs_1.confirmed=1 AND __filter_subs_1.active=1 AND __filter_subs_1.deleted=0) ", params.Add(*f.ConfirmedAndActiveSubscriptionBy)) + } + if f.ConfirmedSubscriptionOrOwnedBy != nil { + joinClause += fmt.Sprintf(" LEFT JOIN subscriptions AS __filter_subs_2 ON (messages.channel_id = __filter_subs_2.channel_id AND __filter_subs_2.subscriber_user_id = :%s AND __filter_subs_2.confirmed=1 AND __filter_subs_2.deleted=0) ", params.Add(*f.ConfirmedSubscriptionOrOwnedBy)) } if f.SearchStringFTS != nil { joinClause += " JOIN messages_fts AS mfts ON (mfts.rowid = messages.rowid) " @@ -66,7 +70,11 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) { } if f.ConfirmedAndActiveSubscriptionBy != nil { - sqlClauses = append(sqlClauses, "(subs.confirmed=1 AND subs.active=1 AND subs.deleted=0)") + sqlClauses = append(sqlClauses, "(__filter_subs_1.confirmed=1 AND __filter_subs_1.active=1 AND __filter_subs_1.deleted=0)") + } + + if f.ConfirmedSubscriptionOrOwnedBy != nil { + sqlClauses = append(sqlClauses, fmt.Sprintf("((__filter_subs_2.confirmed=1 AND __filter_subs_2.deleted=0) OR (messages.channel_owner_user_id = :%s) OR (messages.sender_user_id = :%s))", params.Add(*f.ConfirmedSubscriptionOrOwnedBy), params.Add(*f.ConfirmedSubscriptionOrOwnedBy))) } if f.Sender != nil { diff --git a/scnserver/test/message_test.go b/scnserver/test/message_test.go index ace0a28..cf2b953 100644 --- a/scnserver/test/message_test.go +++ b/scnserver/test/message_test.go @@ -1192,3 +1192,124 @@ func TestUnconfirmedSubscriptionListMessages(t *testing.T) { } tt.AssertFalse(t, "foundActivatedMessage", foundActivatedMessage) } + +func TestListMessagesSenderUserID(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + data := tt.InitDefaultData(t, ws) + + user16 := data.User[16] + + type msg struct { + MessageId string `json:"message_id"` + } + type mglist struct { + Messages []msg `json:"messages"` + TotalCount int `json:"total_count"` + } + + allMessages := tt.RequestAuthGet[mglist](t, user16.AdminKey, baseUrl, "/api/v2/messages") + tt.AssertTrue(t, "allMessages count > 0", allMessages.TotalCount > 0) + + filteredMessages := tt.RequestAuthGet[mglist](t, user16.AdminKey, baseUrl, fmt.Sprintf("/api/v2/messages?sender_user_id=%s", user16.UID)) + + tt.AssertEqual(t, "Filtered message count should equal total count", allMessages.TotalCount, filteredMessages.TotalCount) + tt.AssertEqual(t, "Filtered message len should equal total len", len(allMessages.Messages), len(filteredMessages.Messages)) +} + +func TestListMessagesSubscriptionStatusAllInactiveSubscription(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + data := tt.InitDefaultData(t, ws) + + user14 := data.User[14] // Subscriber + user15 := data.User[15] // Owner + chanName := "chan_other_accepted" + + subscriptionID, _ := tt.FindSubscriptionByChanName(t, baseUrl, user14, user15.UID, chanName) + + newMessageTitle := langext.RandBase62(48) + tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{"key": user15.AdminKey, "user_id": user15.UID, "channel": chanName, "title": newMessageTitle}) + + type msg struct { + MessageId string `json:"message_id"` + ChannelId string `json:"channel_id"` + Title string `json:"title"` + } + type mglist struct { + Messages []msg `json:"messages"` + TotalCount int `json:"total_count"` + } + + { + messages := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages") + foundInitial := langext.ArrAny(messages.Messages, func(m msg) bool { return m.Title == newMessageTitle }) + tt.AssertTrue(t, "foundInitial", foundInitial) + } + + tt.RequestAuthPatch[gin.H](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{"active": false}) + + { + messages := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages") + foundInactive := langext.ArrAny(messages.Messages, func(m msg) bool { return m.Title == newMessageTitle }) + tt.AssertFalse(t, "foundInactive", foundInactive) + } + + { + messages := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages?subscription_status=all") + foundAllStatus := langext.ArrAny(messages.Messages, func(m msg) bool { return m.Title == newMessageTitle }) + tt.AssertTrue(t, "foundAllStatus", foundAllStatus) + } +} + +func TestListMessagesSubscriptionStatusAllNoSubscription(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + data := tt.InitDefaultData(t, ws) + + user0 := data.User[0] + + type msg struct { + MessageId string `json:"message_id"` + ChannelId string `json:"channel_id"` + Title string `json:"title"` + } + type mglist struct { + Messages []msg `json:"messages"` + TotalCount int `json:"total_count"` + } + + chan2 := data.User[0].Channels[2] + + newMessageTitle := langext.RandBase62(48) + tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{"key": user0.AdminKey, "user_id": user0.UID, "channel": chan2.InternalName, "title": newMessageTitle}) + + { + messages := tt.RequestAuthGet[mglist](t, user0.AdminKey, baseUrl, "/api/v2/messages") + foundInitial := langext.ArrAny(messages.Messages, func(m msg) bool { return m.Title == newMessageTitle }) + tt.AssertTrue(t, "foundInitial", foundInitial) + } + + { + messages := tt.RequestAuthGet[mglist](t, user0.AdminKey, baseUrl, "/api/v2/messages?subscription_status=all") + foundAllStatusAndSubscribed := langext.ArrAny(messages.Messages, func(m msg) bool { return m.Title == newMessageTitle }) + tt.AssertTrue(t, "foundAllStatusAndSubscribed", foundAllStatusAndSubscribed) + } + + subscriptionID, _ := tt.FindSubscriptionByChanName(t, baseUrl, user0, user0.UID, chan2.InternalName) + + tt.RequestAuthDelete[gin.H](t, user0.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user0.UID, subscriptionID), gin.H{}) + + { + messages := tt.RequestAuthGet[mglist](t, user0.AdminKey, baseUrl, "/api/v2/messages") + foundNoSub := langext.ArrAny(messages.Messages, func(m msg) bool { return m.Title == newMessageTitle }) + tt.AssertFalse(t, "foundNoSub", foundNoSub) + } + + { + messages := tt.RequestAuthGet[mglist](t, user0.AdminKey, baseUrl, "/api/v2/messages?subscription_status=all") + foundAllStatus := langext.ArrAny(messages.Messages, func(m msg) bool { return m.Title == newMessageTitle }) + tt.AssertTrue(t, "foundAllStatus", foundAllStatus) + } + +}