feed.go 12 KB

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