Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiple categories per feed #2859

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 44 additions & 44 deletions client/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,53 +121,53 @@ type Subscriptions []*Subscription

// Feed represents a Miniflux feed.
type Feed struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
FeedURL string `json:"feed_url"`
SiteURL string `json:"site_url"`
Title string `json:"title"`
CheckedAt time.Time `json:"checked_at,omitempty"`
EtagHeader string `json:"etag_header,omitempty"`
LastModifiedHeader string `json:"last_modified_header,omitempty"`
ParsingErrorMsg string `json:"parsing_error_message,omitempty"`
ParsingErrorCount int `json:"parsing_error_count,omitempty"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
Crawler bool `json:"crawler"`
UserAgent string `json:"user_agent"`
Cookie string `json:"cookie"`
Username string `json:"username"`
Password string `json:"password"`
Category *Category `json:"category,omitempty"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
FeedURL string `json:"feed_url"`
SiteURL string `json:"site_url"`
Title string `json:"title"`
CheckedAt time.Time `json:"checked_at,omitempty"`
EtagHeader string `json:"etag_header,omitempty"`
LastModifiedHeader string `json:"last_modified_header,omitempty"`
ParsingErrorMsg string `json:"parsing_error_message,omitempty"`
ParsingErrorCount int `json:"parsing_error_count,omitempty"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
Crawler bool `json:"crawler"`
UserAgent string `json:"user_agent"`
Cookie string `json:"cookie"`
Username string `json:"username"`
Password string `json:"password"`
Categories []*Category `json:"categories,omitempty"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
}

// FeedCreationRequest represents the request to create a feed.
type FeedCreationRequest struct {
FeedURL string `json:"feed_url"`
CategoryID int64 `json:"category_id"`
UserAgent string `json:"user_agent"`
Cookie string `json:"cookie"`
Username string `json:"username"`
Password string `json:"password"`
Crawler bool `json:"crawler"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
FeedURL string `json:"feed_url"`
CategoryIDs []int64 `json:"category_ids"`
UserAgent string `json:"user_agent"`
Cookie string `json:"cookie"`
Username string `json:"username"`
Password string `json:"password"`
Crawler bool `json:"crawler"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
}

// FeedModificationRequest represents the request to update a feed.
Expand All @@ -184,7 +184,7 @@ type FeedModificationRequest struct {
Cookie *string `json:"cookie"`
Username *string `json:"username"`
Password *string `json:"password"`
CategoryID *int64 `json:"category_id"`
CategoryIDs []int64 `json:"category_ids"`
Disabled *bool `json:"disabled"`
IgnoreHTTPCache *bool `json:"ignore_http_cache"`
AllowSelfSignedCertificates *bool `json:"allow_self_signed_certificates"`
Expand Down
24 changes: 12 additions & 12 deletions internal/api/api_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -973,8 +973,8 @@ func TestMarkCategoryAsReadEndpoint(t *testing.T) {
}

feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
FeedURL: testConfig.testFeedURL,
CategoryID: category.ID,
FeedURL: testConfig.testFeedURL,
CategoryIDs: []int64{category.ID},
})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -1017,8 +1017,8 @@ func TestCreateFeedEndpoint(t *testing.T) {
}

feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
FeedURL: testConfig.testFeedURL,
CategoryID: category.ID,
FeedURL: testConfig.testFeedURL,
CategoryIDs: []int64{category.ID},
})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -1081,8 +1081,8 @@ func TestCreateFeedWithInexistingCategory(t *testing.T) {
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)

_, err = regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
FeedURL: testConfig.testFeedURL,
CategoryID: 123456789,
FeedURL: testConfig.testFeedURL,
CategoryIDs: []int64{123456789},
})

if err == nil {
Expand Down Expand Up @@ -1319,7 +1319,7 @@ func TestUpdateFeedWithInvalidCategory(t *testing.T) {
}

feedUpdateRequest := &miniflux.FeedModificationRequest{
CategoryID: miniflux.SetOptionalField(int64(123456789)),
CategoryIDs: []int64{int64(123456789)},
}

if _, err := regularUserClient.UpdateFeed(feedID, feedUpdateRequest); err == nil {
Expand Down Expand Up @@ -1659,8 +1659,8 @@ func TestGetCategoryFeedsEndpoint(t *testing.T) {
}

feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
FeedURL: testConfig.testFeedURL,
CategoryID: category.ID,
FeedURL: testConfig.testFeedURL,
CategoryIDs: []int64{category.ID},
})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -1870,8 +1870,8 @@ func TestGetAllCategoryEntriesEndpoint(t *testing.T) {
}

feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{
FeedURL: testConfig.testFeedURL,
CategoryID: category.ID,
FeedURL: testConfig.testFeedURL,
CategoryIDs: []int64{category.ID},
})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -2214,7 +2214,7 @@ func TestGetEntryEndpoints(t *testing.T) {
t.Fatalf(`Invalid entryID, got %d`, entry.ID)
}

entry, err = regularUserClient.CategoryEntry(result.Entries[0].Feed.Category.ID, result.Entries[0].ID)
entry, err = regularUserClient.CategoryEntry(result.Entries[0].Feed.Categories[0].ID, result.Entries[0].ID)
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/api/category.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (h *handler) removeCategory(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
categoryID := request.RouteInt64Param(r, "categoryID")

if !h.store.CategoryIDExists(userID, categoryID) {
if !h.store.CategoryIDsExists(userID, []int64{categoryID}) {
json.NotFound(w, r)
return
}
Expand Down
2 changes: 1 addition & 1 deletion internal/api/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int

userID := request.UserID(r)
categoryID = request.QueryInt64Param(r, "category_id", categoryID)
if categoryID > 0 && !h.store.CategoryIDExists(userID, categoryID) {
if categoryID > 0 && !h.store.CategoryIDsExists(userID, []int64{categoryID}) {
json.BadRequest(w, r, errors.New("invalid category ID"))
return
}
Expand Down
10 changes: 0 additions & 10 deletions internal/api/feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,6 @@ func (h *handler) createFeed(w http.ResponseWriter, r *http.Request) {
return
}

// Make the feed category optional for clients who don't support categories.
if feedCreationRequest.CategoryID == 0 {
category, err := h.store.FirstCategory(userID)
if err != nil {
json.ServerError(w, r, err)
return
}
feedCreationRequest.CategoryID = category.ID
}

if validationErr := validator.ValidateFeedCreation(h.store, userID, &feedCreationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
Expand Down
17 changes: 17 additions & 0 deletions internal/database/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -942,4 +942,21 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
CREATE TABLE feed_categories (
feed_id bigint not null,
category_id bigint not null,
primary key(feed_id, category_id),
foreign key (feed_id) references feeds(id) on delete cascade,
foreign key (category_id) references categories(id) on delete cascade
);

INSERT INTO feed_categories (feed_id, category_id) SELECT id AS feed_id, category_id FROM feeds;

ALTER TABLE feeds DROP COLUMN category_id;
`
_, err = tx.Exec(sql)
return err
},
}
4 changes: 3 additions & 1 deletion internal/fever/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,9 @@ A feeds_group object has the following members:
func (h *handler) buildFeedGroups(feeds model.Feeds) []feedsGroups {
feedsGroupedByCategory := make(map[int64][]string)
for _, feed := range feeds {
feedsGroupedByCategory[feed.Category.ID] = append(feedsGroupedByCategory[feed.Category.ID], strconv.FormatInt(feed.ID, 10))
for _, category := range feed.Categories {
feedsGroupedByCategory[category.ID] = append(feedsGroupedByCategory[category.ID], strconv.FormatInt(feed.ID, 10))
}
}

result := make([]feedsGroups, 0)
Expand Down
16 changes: 10 additions & 6 deletions internal/googlereader/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,8 +756,8 @@ func subscribe(newFeed Stream, category Stream, title string, store *storage.Sto
}

feedRequest := model.FeedCreationRequest{
FeedURL: newFeed.ID,
CategoryID: destCategory.ID,
FeedURL: newFeed.ID,
CategoryIDs: []int64{destCategory.ID},
}
verr := validator.ValidateFeedCreation(store, userID, &feedRequest)
if verr != nil {
Expand Down Expand Up @@ -821,7 +821,7 @@ func move(stream Stream, destination Stream, store *storage.Storage, userID int6
return err
}
feedModification := model.FeedModificationRequest{
CategoryID: &category.ID,
CategoryIDs: []int64{category.ID},
}
feedModification.Patch(feed)
return store.UpdateFeed(feed)
Expand Down Expand Up @@ -991,8 +991,8 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
}
categories := make([]string, 0)
categories = append(categories, userReadingList)
if entry.Feed.Category.Title != "" {
categories = append(categories, fmt.Sprintf(UserLabelPrefix, userID)+entry.Feed.Category.Title)
for _, category := range entry.Feed.Categories {
categories = append(categories, fmt.Sprintf(UserLabelPrefix, userID)+category.Title)
}
if entry.Status == model.EntryStatusRead {
categories = append(categories, userRead)
Expand Down Expand Up @@ -1209,11 +1209,15 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
}
result.Subscriptions = make([]subscription, 0)
for _, feed := range feeds {
var categories []subscriptionCategory
for _, category := range feed.Categories {
categories = append(categories, subscriptionCategory{fmt.Sprintf(UserLabelPrefix, userID) + category.Title, category.Title, "folder"})
}
result.Subscriptions = append(result.Subscriptions, subscription{
ID: fmt.Sprintf(FeedPrefix+"%d", feed.ID),
Title: feed.Title,
URL: feed.FeedURL,
Categories: []subscriptionCategory{{fmt.Sprintf(UserLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}},
Categories: categories,
HTMLURL: feed.SiteURL,
IconURL: "", // TODO: Icons are base64 encoded in the DB.
})
Expand Down
60 changes: 36 additions & 24 deletions internal/integration/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ func NewClient(webhookURL, webhookSecret string) *Client {
}

func (c *Client) SendSaveEntryWebhookEvent(entry *model.Entry) error {
var categoryIDs []int64
var categories []*WebhookCategory
for _, category := range entry.Feed.Categories {
categoryIDs = append(categoryIDs, category.ID)
categories = append(categories, &WebhookCategory{ID: category.ID, Title: category.Title})
}
return c.makeRequest(SaveEntryEventType, &WebhookSaveEntryEvent{
EventType: SaveEntryEventType,
Entry: &WebhookEntry{
Expand All @@ -54,14 +60,14 @@ func (c *Client) SendSaveEntryWebhookEvent(entry *model.Entry) error {
Enclosures: entry.Enclosures,
Tags: entry.Tags,
Feed: &WebhookFeed{
ID: entry.Feed.ID,
UserID: entry.Feed.UserID,
CategoryID: entry.Feed.Category.ID,
Category: &WebhookCategory{ID: entry.Feed.Category.ID, Title: entry.Feed.Category.Title},
FeedURL: entry.Feed.FeedURL,
SiteURL: entry.Feed.SiteURL,
Title: entry.Feed.Title,
CheckedAt: entry.Feed.CheckedAt,
ID: entry.Feed.ID,
UserID: entry.Feed.UserID,
CategoryIDs: categoryIDs,
Categories: categories,
FeedURL: entry.Feed.FeedURL,
SiteURL: entry.Feed.SiteURL,
Title: entry.Feed.Title,
CheckedAt: entry.Feed.CheckedAt,
},
},
})
Expand Down Expand Up @@ -95,17 +101,23 @@ func (c *Client) SendNewEntriesWebhookEvent(feed *model.Feed, entries model.Entr
Tags: entry.Tags,
})
}
var categoryIDs []int64
var categories []*WebhookCategory
for _, category := range feed.Categories {
categoryIDs = append(categoryIDs, category.ID)
categories = append(categories, &WebhookCategory{ID: category.ID, Title: category.Title})
}
return c.makeRequest(NewEntriesEventType, &WebhookNewEntriesEvent{
EventType: NewEntriesEventType,
Feed: &WebhookFeed{
ID: feed.ID,
UserID: feed.UserID,
CategoryID: feed.Category.ID,
Category: &WebhookCategory{ID: feed.Category.ID, Title: feed.Category.Title},
FeedURL: feed.FeedURL,
SiteURL: feed.SiteURL,
Title: feed.Title,
CheckedAt: feed.CheckedAt,
ID: feed.ID,
UserID: feed.UserID,
CategoryIDs: categoryIDs,
Categories: categories,
FeedURL: feed.FeedURL,
SiteURL: feed.SiteURL,
Title: feed.Title,
CheckedAt: feed.CheckedAt,
},
Entries: webhookEntries,
})
Expand Down Expand Up @@ -146,14 +158,14 @@ func (c *Client) makeRequest(eventType string, payload any) error {
}

type WebhookFeed struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
CategoryID int64 `json:"category_id"`
Category *WebhookCategory `json:"category,omitempty"`
FeedURL string `json:"feed_url"`
SiteURL string `json:"site_url"`
Title string `json:"title"`
CheckedAt time.Time `json:"checked_at"`
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
CategoryIDs []int64 `json:"category_ids"`
Categories []*WebhookCategory `json:"categories,omitempty"`
FeedURL string `json:"feed_url"`
SiteURL string `json:"site_url"`
Title string `json:"title"`
CheckedAt time.Time `json:"checked_at"`
}

type WebhookCategory struct {
Expand Down
4 changes: 2 additions & 2 deletions internal/model/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ func NewEntry() *Entry {
Enclosures: make(EnclosureList, 0),
Tags: make([]string, 0),
Feed: &Feed{
Category: &Category{},
Icon: &FeedIcon{},
Categories: nil,
Icon: &FeedIcon{},
},
}
}
Expand Down
Loading
Loading