feed.go 12 KB

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