Skip to content

Commit

Permalink
Allow multiple categories per feed
Browse files Browse the repository at this point in the history
  • Loading branch information
darkdragon-001 committed Sep 22, 2024
1 parent c326d55 commit cc98fd3
Show file tree
Hide file tree
Showing 42 changed files with 488 additions and 352 deletions.
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

0 comments on commit cc98fd3

Please sign in to comment.