| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
- // SPDX-License-Identifier: Apache-2.0
- package storage // import "miniflux.app/v2/internal/storage"
- import (
- "database/sql"
- "fmt"
- "strconv"
- "strings"
- "miniflux.app/v2/internal/model"
- "miniflux.app/v2/internal/timezone"
- )
- // feedQueryBuilder builds a SQL query to fetch feeds.
- type feedQueryBuilder struct {
- store *Storage
- args []any
- conditions []string
- sortExpressions []string
- limit int
- offset int
- withCounters bool
- counterJoinFeeds bool
- counterArgs []any
- counterConditions []string
- }
- // NewFeedQueryBuilder returns a new FeedQueryBuilder.
- func NewFeedQueryBuilder(store *Storage, userID int64) *feedQueryBuilder {
- return &feedQueryBuilder{
- store: store,
- args: []any{userID},
- conditions: []string{"f.user_id = $1"},
- counterArgs: []any{userID, model.EntryStatusRead, model.EntryStatusUnread},
- counterConditions: []string{"e.user_id = $1", "e.status IN ($2, $3)"},
- }
- }
- // WithCategoryID filter by category ID.
- func (f *feedQueryBuilder) WithCategoryID(categoryID int64) *feedQueryBuilder {
- if categoryID > 0 {
- f.conditions = append(f.conditions, "f.category_id = $"+strconv.Itoa(len(f.args)+1))
- f.args = append(f.args, categoryID)
- f.counterConditions = append(f.counterConditions, "f.category_id = $"+strconv.Itoa(len(f.counterArgs)+1))
- f.counterArgs = append(f.counterArgs, categoryID)
- f.counterJoinFeeds = true
- }
- return f
- }
- // WithFeedID filter by feed ID.
- func (f *feedQueryBuilder) WithFeedID(feedID int64) *feedQueryBuilder {
- if feedID > 0 {
- f.conditions = append(f.conditions, "f.id = $"+strconv.Itoa(len(f.args)+1))
- f.args = append(f.args, feedID)
- }
- return f
- }
- // WithCounters let the builder return feeds with counters of statuses of entries.
- func (f *feedQueryBuilder) WithCounters() *feedQueryBuilder {
- f.withCounters = true
- return f
- }
- // WithSorting add a sort expression.
- func (f *feedQueryBuilder) WithSorting(column, direction string) *feedQueryBuilder {
- f.sortExpressions = append(f.sortExpressions, column+" "+direction)
- return f
- }
- // WithLimit set the limit.
- func (f *feedQueryBuilder) WithLimit(limit int) *feedQueryBuilder {
- f.limit = limit
- return f
- }
- // WithOffset set the offset.
- func (f *feedQueryBuilder) WithOffset(offset int) *feedQueryBuilder {
- f.offset = offset
- return f
- }
- func (f *feedQueryBuilder) buildCondition() string {
- return strings.Join(f.conditions, " AND ")
- }
- func (f *feedQueryBuilder) buildCounterCondition() string {
- return strings.Join(f.counterConditions, " AND ")
- }
- func (f *feedQueryBuilder) buildSorting() string {
- var parts string
- if len(f.sortExpressions) > 0 {
- parts += " ORDER BY " + strings.Join(f.sortExpressions, ", ")
- }
- if len(parts) > 0 {
- parts += ", lower(f.title) ASC"
- }
- if f.limit > 0 {
- parts += " LIMIT " + strconv.Itoa(f.limit)
- }
- if f.offset > 0 {
- parts += " OFFSET " + strconv.Itoa(f.offset)
- }
- return parts
- }
- // GetFeed returns a single feed that match the condition.
- func (f *feedQueryBuilder) GetFeed() (*model.Feed, error) {
- f.limit = 1
- feeds, err := f.GetFeeds()
- if err != nil {
- return nil, err
- }
- if len(feeds) != 1 {
- return nil, nil
- }
- return feeds[0], nil
- }
- // GetFeeds returns a list of feeds that match the condition.
- func (f *feedQueryBuilder) GetFeeds() (model.Feeds, error) {
- var query = `
- SELECT
- f.id,
- f.feed_url,
- f.site_url,
- f.title,
- f.description,
- f.etag_header,
- f.last_modified_header,
- f.user_id,
- f.checked_at at time zone u.timezone,
- f.next_check_at at time zone u.timezone,
- f.parsing_error_count,
- f.parsing_error_msg,
- f.scraper_rules,
- f.rewrite_rules,
- f.url_rewrite_rules,
- f.blocklist_rules,
- f.keeplist_rules,
- f.block_filter_entry_rules,
- f.keep_filter_entry_rules,
- f.crawler,
- f.user_agent,
- f.cookie,
- f.username,
- f.password,
- f.ignore_http_cache,
- f.allow_self_signed_certificates,
- f.fetch_via_proxy,
- f.disabled,
- f.no_media_player,
- f.hide_globally,
- f.category_id,
- c.title as category_title,
- c.hide_globally as category_hidden,
- fi.icon_id,
- i.external_id,
- u.timezone,
- f.apprise_service_urls,
- f.webhook_url,
- f.disable_http2,
- f.ntfy_enabled,
- f.ntfy_priority,
- f.ntfy_topic,
- f.pushover_enabled,
- f.pushover_priority,
- f.proxy_url,
- f.ignore_entry_updates
- FROM
- feeds f
- LEFT JOIN
- categories c ON c.id=f.category_id
- LEFT JOIN
- feed_icons fi ON fi.feed_id=f.id
- LEFT JOIN
- icons i ON i.id=fi.icon_id
- LEFT JOIN
- users u ON u.id=f.user_id
- WHERE %s
- %s
- `
- query = fmt.Sprintf(query, f.buildCondition(), f.buildSorting())
- readCounters, unreadCounters, err := f.fetchFeedCounter()
- if err != nil {
- return nil, err
- }
- rows, err := f.store.db.Query(query, f.args...)
- if err != nil {
- return nil, fmt.Errorf(`store: unable to fetch feeds: %w`, err)
- }
- defer rows.Close()
- feeds := make(model.Feeds, 0)
- for rows.Next() {
- var feed model.Feed
- var iconID sql.NullInt64
- var externalIconID sql.NullString
- var tz string
- feed.Category = &model.Category{}
- err := rows.Scan(
- &feed.ID,
- &feed.FeedURL,
- &feed.SiteURL,
- &feed.Title,
- &feed.Description,
- &feed.EtagHeader,
- &feed.LastModifiedHeader,
- &feed.UserID,
- &feed.CheckedAt,
- &feed.NextCheckAt,
- &feed.ParsingErrorCount,
- &feed.ParsingErrorMsg,
- &feed.ScraperRules,
- &feed.RewriteRules,
- &feed.UrlRewriteRules,
- &feed.BlocklistRules,
- &feed.KeeplistRules,
- &feed.BlockFilterEntryRules,
- &feed.KeepFilterEntryRules,
- &feed.Crawler,
- &feed.UserAgent,
- &feed.Cookie,
- &feed.Username,
- &feed.Password,
- &feed.IgnoreHTTPCache,
- &feed.AllowSelfSignedCertificates,
- &feed.FetchViaProxy,
- &feed.Disabled,
- &feed.NoMediaPlayer,
- &feed.HideGlobally,
- &feed.Category.ID,
- &feed.Category.Title,
- &feed.Category.HideGlobally,
- &iconID,
- &externalIconID,
- &tz,
- &feed.AppriseServiceURLs,
- &feed.WebhookURL,
- &feed.DisableHTTP2,
- &feed.NtfyEnabled,
- &feed.NtfyPriority,
- &feed.NtfyTopic,
- &feed.PushoverEnabled,
- &feed.PushoverPriority,
- &feed.ProxyURL,
- &feed.IgnoreEntryUpdates,
- )
- if err != nil {
- return nil, fmt.Errorf(`store: unable to fetch feeds row: %w`, err)
- }
- if iconID.Valid && externalIconID.Valid {
- feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.Int64, ExternalIconID: externalIconID.String}
- } else {
- feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: 0, ExternalIconID: ""}
- }
- if readCounters != nil {
- if count, found := readCounters[feed.ID]; found {
- feed.ReadCount = count
- }
- }
- if unreadCounters != nil {
- if count, found := unreadCounters[feed.ID]; found {
- feed.UnreadCount = count
- }
- }
- feed.NumberOfVisibleEntries = feed.ReadCount + feed.UnreadCount
- feed.CheckedAt = timezone.Convert(tz, feed.CheckedAt)
- feed.NextCheckAt = timezone.Convert(tz, feed.NextCheckAt)
- feed.Category.UserID = feed.UserID
- feeds = append(feeds, &feed)
- }
- return feeds, nil
- }
- func (f *feedQueryBuilder) fetchFeedCounter() (unreadCounters map[int64]int, readCounters map[int64]int, err error) {
- if !f.withCounters {
- return nil, nil, nil
- }
- query := `
- SELECT
- e.feed_id,
- e.status,
- count(*)
- FROM
- entries e
- %s
- WHERE
- %s
- GROUP BY
- e.feed_id, e.status
- `
- join := ""
- if f.counterJoinFeeds {
- join = "LEFT JOIN feeds f ON f.id=e.feed_id"
- }
- query = fmt.Sprintf(query, join, f.buildCounterCondition())
- rows, err := f.store.db.Query(query, f.counterArgs...)
- if err != nil {
- return nil, nil, fmt.Errorf(`store: unable to fetch feed counts: %w`, err)
- }
- defer rows.Close()
- readCounters = make(map[int64]int)
- unreadCounters = make(map[int64]int)
- for rows.Next() {
- var feedID int64
- var status string
- var count int
- if err := rows.Scan(&feedID, &status, &count); err != nil {
- return nil, nil, fmt.Errorf(`store: unable to fetch feed counter row: %w`, err)
- }
- switch status {
- case model.EntryStatusRead:
- readCounters[feedID] = count
- case model.EntryStatusUnread:
- unreadCounters[feedID] = count
- }
- }
- return readCounters, unreadCounters, nil
- }
|