feed.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. // Copyright 2017 Frédéric Guillot. All rights reserved.
  2. // Use of this source code is governed by the Apache 2.0
  3. // license that can be found in the LICENSE file.
  4. package storage // import "miniflux.app/storage"
  5. import (
  6. "database/sql"
  7. "errors"
  8. "fmt"
  9. "miniflux.app/model"
  10. "miniflux.app/timezone"
  11. )
  12. var feedListQuery = `
  13. SELECT
  14. f.id,
  15. f.feed_url,
  16. f.site_url,
  17. f.title,
  18. f.etag_header,
  19. f.last_modified_header,
  20. f.user_id,
  21. f.checked_at at time zone u.timezone,
  22. f.parsing_error_count,
  23. f.parsing_error_msg,
  24. f.scraper_rules,
  25. f.rewrite_rules,
  26. f.blocklist_rules,
  27. f.keeplist_rules,
  28. f.crawler,
  29. f.user_agent,
  30. f.username,
  31. f.password,
  32. f.ignore_http_cache,
  33. f.fetch_via_proxy,
  34. f.disabled,
  35. f.category_id,
  36. c.title as category_title,
  37. fi.icon_id,
  38. u.timezone
  39. FROM
  40. feeds f
  41. LEFT JOIN
  42. categories c ON c.id=f.category_id
  43. LEFT JOIN
  44. feed_icons fi ON fi.feed_id=f.id
  45. LEFT JOIN
  46. users u ON u.id=f.user_id
  47. WHERE
  48. f.user_id=$1
  49. ORDER BY
  50. f.parsing_error_count DESC, lower(f.title) ASC
  51. `
  52. // FeedExists checks if the given feed exists.
  53. func (s *Storage) FeedExists(userID, feedID int64) bool {
  54. var result bool
  55. query := `SELECT true FROM feeds WHERE user_id=$1 AND id=$2`
  56. s.db.QueryRow(query, userID, feedID).Scan(&result)
  57. return result
  58. }
  59. // FeedURLExists checks if feed URL already exists.
  60. func (s *Storage) FeedURLExists(userID int64, feedURL string) bool {
  61. var result bool
  62. query := `SELECT true FROM feeds WHERE user_id=$1 AND feed_url=$2`
  63. s.db.QueryRow(query, userID, feedURL).Scan(&result)
  64. return result
  65. }
  66. // AnotherFeedURLExists checks if the user a duplicated feed.
  67. func (s *Storage) AnotherFeedURLExists(userID, feedID int64, feedURL string) bool {
  68. var result bool
  69. query := `SELECT true FROM feeds WHERE id <> $1 AND user_id=$2 AND feed_url=$3`
  70. s.db.QueryRow(query, feedID, userID, feedURL).Scan(&result)
  71. return result
  72. }
  73. // CountAllFeeds returns the number of feeds in the database.
  74. func (s *Storage) CountAllFeeds() map[string]int64 {
  75. rows, err := s.db.Query(`SELECT disabled, count(*) FROM feeds GROUP BY disabled`)
  76. if err != nil {
  77. return nil
  78. }
  79. defer rows.Close()
  80. results := make(map[string]int64)
  81. results["enabled"] = 0
  82. results["disabled"] = 0
  83. for rows.Next() {
  84. var disabled bool
  85. var count int64
  86. if err := rows.Scan(&disabled, &count); err != nil {
  87. continue
  88. }
  89. if disabled {
  90. results["disabled"] = count
  91. } else {
  92. results["enabled"] = count
  93. }
  94. }
  95. results["total"] = results["disabled"] + results["enabled"]
  96. return results
  97. }
  98. // CountFeeds returns the number of feeds that belongs to the given user.
  99. func (s *Storage) CountFeeds(userID int64) int {
  100. var result int
  101. err := s.db.QueryRow(`SELECT count(*) FROM feeds WHERE user_id=$1`, userID).Scan(&result)
  102. if err != nil {
  103. return 0
  104. }
  105. return result
  106. }
  107. // CountUserFeedsWithErrors returns the number of feeds with parsing errors that belong to the given user.
  108. func (s *Storage) CountUserFeedsWithErrors(userID int64) int {
  109. query := `SELECT count(*) FROM feeds WHERE user_id=$1 AND parsing_error_count >= $2`
  110. var result int
  111. err := s.db.QueryRow(query, userID, maxParsingError).Scan(&result)
  112. if err != nil {
  113. return 0
  114. }
  115. return result
  116. }
  117. // CountAllFeedsWithErrors returns the number of feeds with parsing errors.
  118. func (s *Storage) CountAllFeedsWithErrors() int {
  119. query := `SELECT count(*) FROM feeds WHERE parsing_error_count >= $1`
  120. var result int
  121. err := s.db.QueryRow(query, maxParsingError).Scan(&result)
  122. if err != nil {
  123. return 0
  124. }
  125. return result
  126. }
  127. // Feeds returns all feeds that belongs to the given user.
  128. func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
  129. return s.fetchFeeds(feedListQuery, "", userID)
  130. }
  131. // FeedsWithCounters returns all feeds of the given user with counters of read and unread entries.
  132. func (s *Storage) FeedsWithCounters(userID int64) (model.Feeds, error) {
  133. counterQuery := `
  134. SELECT
  135. feed_id,
  136. status,
  137. count(*)
  138. FROM
  139. entries
  140. WHERE
  141. user_id=$1 AND status IN ('read', 'unread')
  142. GROUP BY
  143. feed_id, status
  144. `
  145. return s.fetchFeeds(feedListQuery, counterQuery, userID)
  146. }
  147. // FeedsByCategoryWithCounters returns all feeds of the given user/category with counters of read and unread entries.
  148. func (s *Storage) FeedsByCategoryWithCounters(userID, categoryID int64) (model.Feeds, error) {
  149. feedQuery := `
  150. SELECT
  151. f.id,
  152. f.feed_url,
  153. f.site_url,
  154. f.title,
  155. f.etag_header,
  156. f.last_modified_header,
  157. f.user_id,
  158. f.checked_at at time zone u.timezone,
  159. f.parsing_error_count,
  160. f.parsing_error_msg,
  161. f.scraper_rules,
  162. f.rewrite_rules,
  163. f.blocklist_rules,
  164. f.keeplist_rules,
  165. f.crawler,
  166. f.user_agent,
  167. f.username,
  168. f.password,
  169. f.ignore_http_cache,
  170. f.fetch_via_proxy,
  171. f.disabled,
  172. f.category_id,
  173. c.title as category_title,
  174. fi.icon_id,
  175. u.timezone
  176. FROM
  177. feeds f
  178. LEFT JOIN
  179. categories c ON c.id=f.category_id
  180. LEFT JOIN
  181. feed_icons fi ON fi.feed_id=f.id
  182. LEFT JOIN
  183. users u ON u.id=f.user_id
  184. WHERE
  185. f.user_id=$1 AND f.category_id=$2
  186. ORDER BY
  187. f.parsing_error_count DESC, lower(f.title) ASC
  188. `
  189. counterQuery := `
  190. SELECT
  191. e.feed_id,
  192. e.status,
  193. count(*)
  194. FROM
  195. entries e
  196. LEFT JOIN
  197. feeds f ON f.id=e.feed_id
  198. WHERE
  199. e.user_id=$1 AND f.category_id=$2 AND e.status IN ('read', 'unread')
  200. GROUP BY
  201. e.feed_id, e.status
  202. `
  203. return s.fetchFeeds(feedQuery, counterQuery, userID, categoryID)
  204. }
  205. func (s *Storage) fetchFeedCounter(query string, args ...interface{}) (unreadCounters map[int64]int, readCounters map[int64]int, err error) {
  206. rows, err := s.db.Query(query, args...)
  207. if err != nil {
  208. return nil, nil, fmt.Errorf(`store: unable to fetch feed counts: %v`, err)
  209. }
  210. defer rows.Close()
  211. readCounters = make(map[int64]int)
  212. unreadCounters = make(map[int64]int)
  213. for rows.Next() {
  214. var feedID int64
  215. var status string
  216. var count int
  217. if err := rows.Scan(&feedID, &status, &count); err != nil {
  218. return nil, nil, fmt.Errorf(`store: unable to fetch feed counter row: %v`, err)
  219. }
  220. if status == "read" {
  221. readCounters[feedID] = count
  222. } else if status == "unread" {
  223. unreadCounters[feedID] = count
  224. }
  225. }
  226. return readCounters, unreadCounters, nil
  227. }
  228. func (s *Storage) fetchFeeds(feedQuery, counterQuery string, args ...interface{}) (model.Feeds, error) {
  229. var (
  230. readCounters map[int64]int
  231. unreadCounters map[int64]int
  232. )
  233. if counterQuery != "" {
  234. var err error
  235. readCounters, unreadCounters, err = s.fetchFeedCounter(counterQuery, args...)
  236. if err != nil {
  237. return nil, err
  238. }
  239. }
  240. feeds := make(model.Feeds, 0)
  241. rows, err := s.db.Query(feedQuery, args...)
  242. if err != nil {
  243. return nil, fmt.Errorf(`store: unable to fetch feeds: %v`, err)
  244. }
  245. defer rows.Close()
  246. for rows.Next() {
  247. var feed model.Feed
  248. var iconID interface{}
  249. var tz string
  250. feed.Category = &model.Category{}
  251. err := rows.Scan(
  252. &feed.ID,
  253. &feed.FeedURL,
  254. &feed.SiteURL,
  255. &feed.Title,
  256. &feed.EtagHeader,
  257. &feed.LastModifiedHeader,
  258. &feed.UserID,
  259. &feed.CheckedAt,
  260. &feed.ParsingErrorCount,
  261. &feed.ParsingErrorMsg,
  262. &feed.ScraperRules,
  263. &feed.RewriteRules,
  264. &feed.BlocklistRules,
  265. &feed.KeeplistRules,
  266. &feed.Crawler,
  267. &feed.UserAgent,
  268. &feed.Username,
  269. &feed.Password,
  270. &feed.IgnoreHTTPCache,
  271. &feed.FetchViaProxy,
  272. &feed.Disabled,
  273. &feed.Category.ID,
  274. &feed.Category.Title,
  275. &iconID,
  276. &tz,
  277. )
  278. if err != nil {
  279. return nil, fmt.Errorf(`store: unable to fetch feeds row: %v`, err)
  280. }
  281. if iconID != nil {
  282. feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.(int64)}
  283. }
  284. if counterQuery != "" {
  285. if count, found := readCounters[feed.ID]; found {
  286. feed.ReadCount = count
  287. }
  288. if count, found := unreadCounters[feed.ID]; found {
  289. feed.UnreadCount = count
  290. }
  291. }
  292. feed.CheckedAt = timezone.Convert(tz, feed.CheckedAt)
  293. feed.Category.UserID = feed.UserID
  294. feeds = append(feeds, &feed)
  295. }
  296. return feeds, nil
  297. }
  298. // WeeklyFeedEntryCount returns the weekly entry count for a feed.
  299. func (s *Storage) WeeklyFeedEntryCount(userID, feedID int64) (int, error) {
  300. query := `
  301. SELECT
  302. count(*)
  303. FROM
  304. entries
  305. WHERE
  306. entries.user_id=$1 AND
  307. entries.feed_id=$2 AND
  308. entries.published_at BETWEEN (now() - interval '1 week') AND now();
  309. `
  310. var weeklyCount int
  311. err := s.db.QueryRow(query, userID, feedID).Scan(&weeklyCount)
  312. switch {
  313. case err == sql.ErrNoRows:
  314. return 0, nil
  315. case err != nil:
  316. return 0, fmt.Errorf(`store: unable to fetch weekly count for feed #%d: %v`, feedID, err)
  317. }
  318. return weeklyCount, nil
  319. }
  320. // FeedByID returns a feed by the ID.
  321. func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
  322. var feed model.Feed
  323. var iconID interface{}
  324. var tz string
  325. feed.Category = &model.Category{UserID: userID}
  326. query := `
  327. SELECT
  328. f.id,
  329. f.feed_url,
  330. f.site_url,
  331. f.title,
  332. f.etag_header,
  333. f.last_modified_header,
  334. f.user_id, f.checked_at at time zone u.timezone,
  335. f.parsing_error_count,
  336. f.parsing_error_msg,
  337. f.scraper_rules,
  338. f.rewrite_rules,
  339. f.blocklist_rules,
  340. f.keeplist_rules,
  341. f.crawler,
  342. f.user_agent,
  343. f.username,
  344. f.password,
  345. f.ignore_http_cache,
  346. f.fetch_via_proxy,
  347. f.disabled,
  348. f.category_id,
  349. c.title as category_title,
  350. fi.icon_id,
  351. u.timezone
  352. FROM feeds f
  353. LEFT JOIN categories c ON c.id=f.category_id
  354. LEFT JOIN feed_icons fi ON fi.feed_id=f.id
  355. LEFT JOIN users u ON u.id=f.user_id
  356. WHERE
  357. f.user_id=$1 AND f.id=$2
  358. `
  359. err := s.db.QueryRow(query, userID, feedID).Scan(
  360. &feed.ID,
  361. &feed.FeedURL,
  362. &feed.SiteURL,
  363. &feed.Title,
  364. &feed.EtagHeader,
  365. &feed.LastModifiedHeader,
  366. &feed.UserID,
  367. &feed.CheckedAt,
  368. &feed.ParsingErrorCount,
  369. &feed.ParsingErrorMsg,
  370. &feed.ScraperRules,
  371. &feed.RewriteRules,
  372. &feed.BlocklistRules,
  373. &feed.KeeplistRules,
  374. &feed.Crawler,
  375. &feed.UserAgent,
  376. &feed.Username,
  377. &feed.Password,
  378. &feed.IgnoreHTTPCache,
  379. &feed.FetchViaProxy,
  380. &feed.Disabled,
  381. &feed.Category.ID,
  382. &feed.Category.Title,
  383. &iconID,
  384. &tz,
  385. )
  386. switch {
  387. case err == sql.ErrNoRows:
  388. return nil, nil
  389. case err != nil:
  390. return nil, fmt.Errorf(`store: unable to fetch feed #%d: %v`, feedID, err)
  391. }
  392. if iconID != nil {
  393. feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.(int64)}
  394. }
  395. feed.CheckedAt = timezone.Convert(tz, feed.CheckedAt)
  396. return &feed, nil
  397. }
  398. // CreateFeed creates a new feed.
  399. func (s *Storage) CreateFeed(feed *model.Feed) error {
  400. sql := `
  401. INSERT INTO feeds (
  402. feed_url,
  403. site_url,
  404. title,
  405. category_id,
  406. user_id,
  407. etag_header,
  408. last_modified_header,
  409. crawler,
  410. user_agent,
  411. username,
  412. password,
  413. disabled,
  414. scraper_rules,
  415. rewrite_rules,
  416. blocklist_rules,
  417. keeplist_rules,
  418. ignore_http_cache,
  419. fetch_via_proxy
  420. )
  421. VALUES
  422. ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
  423. RETURNING
  424. id
  425. `
  426. err := s.db.QueryRow(
  427. sql,
  428. feed.FeedURL,
  429. feed.SiteURL,
  430. feed.Title,
  431. feed.Category.ID,
  432. feed.UserID,
  433. feed.EtagHeader,
  434. feed.LastModifiedHeader,
  435. feed.Crawler,
  436. feed.UserAgent,
  437. feed.Username,
  438. feed.Password,
  439. feed.Disabled,
  440. feed.ScraperRules,
  441. feed.RewriteRules,
  442. feed.BlocklistRules,
  443. feed.KeeplistRules,
  444. feed.IgnoreHTTPCache,
  445. feed.FetchViaProxy,
  446. ).Scan(&feed.ID)
  447. if err != nil {
  448. return fmt.Errorf(`store: unable to create feed %q: %v`, feed.FeedURL, err)
  449. }
  450. for i := 0; i < len(feed.Entries); i++ {
  451. feed.Entries[i].FeedID = feed.ID
  452. feed.Entries[i].UserID = feed.UserID
  453. tx, err := s.db.Begin()
  454. if err != nil {
  455. return fmt.Errorf(`store: unable to start transaction: %v`, err)
  456. }
  457. if !s.entryExists(tx, feed.Entries[i]) {
  458. if err := s.createEntry(tx, feed.Entries[i]); err != nil {
  459. tx.Rollback()
  460. return err
  461. }
  462. }
  463. if err := tx.Commit(); err != nil {
  464. return fmt.Errorf(`store: unable to commit transaction: %v`, err)
  465. }
  466. }
  467. return nil
  468. }
  469. // UpdateFeed updates an existing feed.
  470. func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
  471. query := `
  472. UPDATE
  473. feeds
  474. SET
  475. feed_url=$1,
  476. site_url=$2,
  477. title=$3,
  478. category_id=$4,
  479. etag_header=$5,
  480. last_modified_header=$6,
  481. checked_at=$7,
  482. parsing_error_msg=$8,
  483. parsing_error_count=$9,
  484. scraper_rules=$10,
  485. rewrite_rules=$11,
  486. blocklist_rules=$12,
  487. keeplist_rules=$13,
  488. crawler=$14,
  489. user_agent=$15,
  490. username=$16,
  491. password=$17,
  492. disabled=$18,
  493. next_check_at=$19,
  494. ignore_http_cache=$20,
  495. fetch_via_proxy=$21
  496. WHERE
  497. id=$22 AND user_id=$23
  498. `
  499. _, err = s.db.Exec(query,
  500. feed.FeedURL,
  501. feed.SiteURL,
  502. feed.Title,
  503. feed.Category.ID,
  504. feed.EtagHeader,
  505. feed.LastModifiedHeader,
  506. feed.CheckedAt,
  507. feed.ParsingErrorMsg,
  508. feed.ParsingErrorCount,
  509. feed.ScraperRules,
  510. feed.RewriteRules,
  511. feed.BlocklistRules,
  512. feed.KeeplistRules,
  513. feed.Crawler,
  514. feed.UserAgent,
  515. feed.Username,
  516. feed.Password,
  517. feed.Disabled,
  518. feed.NextCheckAt,
  519. feed.IgnoreHTTPCache,
  520. feed.FetchViaProxy,
  521. feed.ID,
  522. feed.UserID,
  523. )
  524. if err != nil {
  525. return fmt.Errorf(`store: unable to update feed #%d (%s): %v`, feed.ID, feed.FeedURL, err)
  526. }
  527. return nil
  528. }
  529. // UpdateFeedError updates feed errors.
  530. func (s *Storage) UpdateFeedError(feed *model.Feed) (err error) {
  531. query := `
  532. UPDATE
  533. feeds
  534. SET
  535. parsing_error_msg=$1,
  536. parsing_error_count=$2,
  537. checked_at=$3,
  538. next_check_at=$4
  539. WHERE
  540. id=$5 AND user_id=$6
  541. `
  542. _, err = s.db.Exec(query,
  543. feed.ParsingErrorMsg,
  544. feed.ParsingErrorCount,
  545. feed.CheckedAt,
  546. feed.NextCheckAt,
  547. feed.ID,
  548. feed.UserID,
  549. )
  550. if err != nil {
  551. return fmt.Errorf(`store: unable to update feed error #%d (%s): %v`, feed.ID, feed.FeedURL, err)
  552. }
  553. return nil
  554. }
  555. // RemoveFeed removes a feed.
  556. func (s *Storage) RemoveFeed(userID, feedID int64) error {
  557. query := `DELETE FROM feeds WHERE id = $1 AND user_id = $2`
  558. result, err := s.db.Exec(query, feedID, userID)
  559. if err != nil {
  560. return fmt.Errorf(`store: unable to remove feed #%d: %v`, feedID, err)
  561. }
  562. count, err := result.RowsAffected()
  563. if err != nil {
  564. return fmt.Errorf(`store: unable to remove feed #%d: %v`, feedID, err)
  565. }
  566. if count == 0 {
  567. return errors.New(`store: no feed has been removed`)
  568. }
  569. return nil
  570. }
  571. // ResetFeedErrors removes all feed errors.
  572. func (s *Storage) ResetFeedErrors() error {
  573. _, err := s.db.Exec(`UPDATE feeds SET parsing_error_count=0, parsing_error_msg=''`)
  574. return err
  575. }