integration.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package integration // import "miniflux.app/v2/internal/integration"
  4. import (
  5. "log/slog"
  6. "miniflux.app/v2/internal/config"
  7. "miniflux.app/v2/internal/integration/apprise"
  8. "miniflux.app/v2/internal/integration/betula"
  9. "miniflux.app/v2/internal/integration/espial"
  10. "miniflux.app/v2/internal/integration/instapaper"
  11. "miniflux.app/v2/internal/integration/linkace"
  12. "miniflux.app/v2/internal/integration/linkding"
  13. "miniflux.app/v2/internal/integration/linkwarden"
  14. "miniflux.app/v2/internal/integration/matrixbot"
  15. "miniflux.app/v2/internal/integration/notion"
  16. "miniflux.app/v2/internal/integration/ntfy"
  17. "miniflux.app/v2/internal/integration/nunuxkeeper"
  18. "miniflux.app/v2/internal/integration/omnivore"
  19. "miniflux.app/v2/internal/integration/pinboard"
  20. "miniflux.app/v2/internal/integration/pocket"
  21. "miniflux.app/v2/internal/integration/raindrop"
  22. "miniflux.app/v2/internal/integration/readeck"
  23. "miniflux.app/v2/internal/integration/readwise"
  24. "miniflux.app/v2/internal/integration/shaarli"
  25. "miniflux.app/v2/internal/integration/shiori"
  26. "miniflux.app/v2/internal/integration/telegrambot"
  27. "miniflux.app/v2/internal/integration/wallabag"
  28. "miniflux.app/v2/internal/integration/webhook"
  29. "miniflux.app/v2/internal/model"
  30. )
  31. // SendEntry sends the entry to third-party providers when the user click on "Save".
  32. func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
  33. if userIntegrations.BetulaEnabled {
  34. slog.Debug("Sending entry to Betula",
  35. slog.Int64("user_id", userIntegrations.UserID),
  36. slog.Int64("entry_id", entry.ID),
  37. slog.String("entry_url", entry.URL),
  38. )
  39. client := betula.NewClient(userIntegrations.BetulaURL, userIntegrations.BetulaToken)
  40. err := client.CreateBookmark(
  41. entry.URL,
  42. entry.Title,
  43. entry.Tags,
  44. )
  45. if err != nil {
  46. slog.Error("Unable to send entry to Betula",
  47. slog.Int64("user_id", userIntegrations.UserID),
  48. slog.Int64("entry_id", entry.ID),
  49. slog.String("entry_url", entry.URL),
  50. slog.Any("error", err),
  51. )
  52. }
  53. }
  54. if userIntegrations.PinboardEnabled {
  55. slog.Debug("Sending entry to Pinboard",
  56. slog.Int64("user_id", userIntegrations.UserID),
  57. slog.Int64("entry_id", entry.ID),
  58. slog.String("entry_url", entry.URL),
  59. )
  60. client := pinboard.NewClient(userIntegrations.PinboardToken)
  61. err := client.CreateBookmark(
  62. entry.URL,
  63. entry.Title,
  64. userIntegrations.PinboardTags,
  65. userIntegrations.PinboardMarkAsUnread,
  66. )
  67. if err != nil {
  68. slog.Error("Unable to send entry to Pinboard",
  69. slog.Int64("user_id", userIntegrations.UserID),
  70. slog.Int64("entry_id", entry.ID),
  71. slog.String("entry_url", entry.URL),
  72. slog.Any("error", err),
  73. )
  74. }
  75. }
  76. if userIntegrations.InstapaperEnabled {
  77. slog.Debug("Sending entry to Instapaper",
  78. slog.Int64("user_id", userIntegrations.UserID),
  79. slog.Int64("entry_id", entry.ID),
  80. slog.String("entry_url", entry.URL),
  81. )
  82. client := instapaper.NewClient(userIntegrations.InstapaperUsername, userIntegrations.InstapaperPassword)
  83. if err := client.AddURL(entry.URL, entry.Title); err != nil {
  84. slog.Error("Unable to send entry to Instapaper",
  85. slog.Int64("user_id", userIntegrations.UserID),
  86. slog.Int64("entry_id", entry.ID),
  87. slog.String("entry_url", entry.URL),
  88. slog.Any("error", err),
  89. )
  90. }
  91. }
  92. if userIntegrations.WallabagEnabled {
  93. slog.Debug("Sending entry to Wallabag",
  94. slog.Int64("user_id", userIntegrations.UserID),
  95. slog.Int64("entry_id", entry.ID),
  96. slog.String("entry_url", entry.URL),
  97. )
  98. client := wallabag.NewClient(
  99. userIntegrations.WallabagURL,
  100. userIntegrations.WallabagClientID,
  101. userIntegrations.WallabagClientSecret,
  102. userIntegrations.WallabagUsername,
  103. userIntegrations.WallabagPassword,
  104. userIntegrations.WallabagOnlyURL,
  105. )
  106. if err := client.CreateEntry(entry.URL, entry.Title, entry.Content); err != nil {
  107. slog.Error("Unable to send entry to Wallabag",
  108. slog.Int64("user_id", userIntegrations.UserID),
  109. slog.Int64("entry_id", entry.ID),
  110. slog.String("entry_url", entry.URL),
  111. slog.Any("error", err),
  112. )
  113. }
  114. }
  115. if userIntegrations.NotionEnabled {
  116. slog.Debug("Sending entry to Notion",
  117. slog.Int64("user_id", userIntegrations.UserID),
  118. slog.Int64("entry_id", entry.ID),
  119. slog.String("entry_url", entry.URL),
  120. )
  121. client := notion.NewClient(
  122. userIntegrations.NotionToken,
  123. userIntegrations.NotionPageID,
  124. )
  125. if err := client.UpdateDocument(entry.URL, entry.Title); err != nil {
  126. slog.Error("Unable to send entry to Notion",
  127. slog.Int64("user_id", userIntegrations.UserID),
  128. slog.Int64("entry_id", entry.ID),
  129. slog.String("entry_url", entry.URL),
  130. slog.Any("error", err),
  131. )
  132. }
  133. }
  134. if userIntegrations.NunuxKeeperEnabled {
  135. slog.Debug("Sending entry to NunuxKeeper",
  136. slog.Int64("user_id", userIntegrations.UserID),
  137. slog.Int64("entry_id", entry.ID),
  138. slog.String("entry_url", entry.URL),
  139. )
  140. client := nunuxkeeper.NewClient(
  141. userIntegrations.NunuxKeeperURL,
  142. userIntegrations.NunuxKeeperAPIKey,
  143. )
  144. if err := client.AddEntry(entry.URL, entry.Title, entry.Content); err != nil {
  145. slog.Error("Unable to send entry to NunuxKeeper",
  146. slog.Int64("user_id", userIntegrations.UserID),
  147. slog.Int64("entry_id", entry.ID),
  148. slog.String("entry_url", entry.URL),
  149. slog.Any("error", err),
  150. )
  151. }
  152. }
  153. if userIntegrations.EspialEnabled {
  154. slog.Debug("Sending entry to Espial",
  155. slog.Int64("user_id", userIntegrations.UserID),
  156. slog.Int64("entry_id", entry.ID),
  157. slog.String("entry_url", entry.URL),
  158. )
  159. client := espial.NewClient(
  160. userIntegrations.EspialURL,
  161. userIntegrations.EspialAPIKey,
  162. )
  163. if err := client.CreateLink(entry.URL, entry.Title, userIntegrations.EspialTags); err != nil {
  164. slog.Error("Unable to send entry to Espial",
  165. slog.Int64("user_id", userIntegrations.UserID),
  166. slog.Int64("entry_id", entry.ID),
  167. slog.String("entry_url", entry.URL),
  168. slog.Any("error", err),
  169. )
  170. }
  171. }
  172. if userIntegrations.PocketEnabled {
  173. slog.Debug("Sending entry to Pocket",
  174. slog.Int64("user_id", userIntegrations.UserID),
  175. slog.Int64("entry_id", entry.ID),
  176. slog.String("entry_url", entry.URL),
  177. )
  178. client := pocket.NewClient(config.Opts.PocketConsumerKey(userIntegrations.PocketConsumerKey), userIntegrations.PocketAccessToken)
  179. if err := client.AddURL(entry.URL, entry.Title); err != nil {
  180. slog.Error("Unable to send entry to Pocket",
  181. slog.Int64("user_id", userIntegrations.UserID),
  182. slog.Int64("entry_id", entry.ID),
  183. slog.String("entry_url", entry.URL),
  184. slog.Any("error", err),
  185. )
  186. }
  187. }
  188. if userIntegrations.LinkAceEnabled {
  189. slog.Debug("Sending entry to LinkAce",
  190. slog.Int64("user_id", userIntegrations.UserID),
  191. slog.Int64("entry_id", entry.ID),
  192. slog.String("entry_url", entry.URL),
  193. )
  194. client := linkace.NewClient(
  195. userIntegrations.LinkAceURL,
  196. userIntegrations.LinkAceAPIKey,
  197. userIntegrations.LinkAceTags,
  198. userIntegrations.LinkAcePrivate,
  199. userIntegrations.LinkAceCheckDisabled,
  200. )
  201. if err := client.AddURL(entry.URL, entry.Title); err != nil {
  202. slog.Error("Unable to send entry to LinkAce",
  203. slog.Int64("user_id", userIntegrations.UserID),
  204. slog.Int64("entry_id", entry.ID),
  205. slog.String("entry_url", entry.URL),
  206. slog.Any("error", err),
  207. )
  208. }
  209. }
  210. if userIntegrations.LinkdingEnabled {
  211. slog.Debug("Sending entry to Linkding",
  212. slog.Int64("user_id", userIntegrations.UserID),
  213. slog.Int64("entry_id", entry.ID),
  214. slog.String("entry_url", entry.URL),
  215. )
  216. client := linkding.NewClient(
  217. userIntegrations.LinkdingURL,
  218. userIntegrations.LinkdingAPIKey,
  219. userIntegrations.LinkdingTags,
  220. userIntegrations.LinkdingMarkAsUnread,
  221. )
  222. if err := client.CreateBookmark(entry.URL, entry.Title); err != nil {
  223. slog.Error("Unable to send entry to Linkding",
  224. slog.Int64("user_id", userIntegrations.UserID),
  225. slog.Int64("entry_id", entry.ID),
  226. slog.String("entry_url", entry.URL),
  227. slog.Any("error", err),
  228. )
  229. }
  230. }
  231. if userIntegrations.LinkwardenEnabled {
  232. slog.Debug("Sending entry to linkwarden",
  233. slog.Int64("user_id", userIntegrations.UserID),
  234. slog.Int64("entry_id", entry.ID),
  235. slog.String("entry_url", entry.URL),
  236. )
  237. client := linkwarden.NewClient(
  238. userIntegrations.LinkwardenURL,
  239. userIntegrations.LinkwardenAPIKey,
  240. )
  241. if err := client.CreateBookmark(entry.URL, entry.Title); err != nil {
  242. slog.Error("Unable to send entry to Linkwarden",
  243. slog.Int64("user_id", userIntegrations.UserID),
  244. slog.Int64("entry_id", entry.ID),
  245. slog.String("entry_url", entry.URL),
  246. slog.Any("error", err),
  247. )
  248. }
  249. }
  250. if userIntegrations.ReadeckEnabled {
  251. slog.Debug("Sending entry to Readeck",
  252. slog.Int64("user_id", userIntegrations.UserID),
  253. slog.Int64("entry_id", entry.ID),
  254. slog.String("entry_url", entry.URL),
  255. )
  256. client := readeck.NewClient(
  257. userIntegrations.ReadeckURL,
  258. userIntegrations.ReadeckAPIKey,
  259. userIntegrations.ReadeckLabels,
  260. userIntegrations.ReadeckOnlyURL,
  261. )
  262. if err := client.CreateBookmark(entry.URL, entry.Title, entry.Content); err != nil {
  263. slog.Error("Unable to send entry to Readeck",
  264. slog.Int64("user_id", userIntegrations.UserID),
  265. slog.Int64("entry_id", entry.ID),
  266. slog.String("entry_url", entry.URL),
  267. slog.Any("error", err),
  268. )
  269. }
  270. }
  271. if userIntegrations.ReadwiseEnabled {
  272. slog.Debug("Sending entry to Readwise",
  273. slog.Int64("user_id", userIntegrations.UserID),
  274. slog.Int64("entry_id", entry.ID),
  275. slog.String("entry_url", entry.URL),
  276. )
  277. client := readwise.NewClient(
  278. userIntegrations.ReadwiseAPIKey,
  279. )
  280. if err := client.CreateDocument(entry.URL); err != nil {
  281. slog.Error("Unable to send entry to Readwise",
  282. slog.Int64("user_id", userIntegrations.UserID),
  283. slog.Int64("entry_id", entry.ID),
  284. slog.String("entry_url", entry.URL),
  285. slog.Any("error", err),
  286. )
  287. }
  288. }
  289. if userIntegrations.ShioriEnabled {
  290. slog.Debug("Sending entry to Shiori",
  291. slog.Int64("user_id", userIntegrations.UserID),
  292. slog.Int64("entry_id", entry.ID),
  293. slog.String("entry_url", entry.URL),
  294. )
  295. client := shiori.NewClient(
  296. userIntegrations.ShioriURL,
  297. userIntegrations.ShioriUsername,
  298. userIntegrations.ShioriPassword,
  299. )
  300. if err := client.CreateBookmark(entry.URL, entry.Title); err != nil {
  301. slog.Error("Unable to send entry to Shiori",
  302. slog.Int64("user_id", userIntegrations.UserID),
  303. slog.Int64("entry_id", entry.ID),
  304. slog.String("entry_url", entry.URL),
  305. slog.Any("error", err),
  306. )
  307. }
  308. }
  309. if userIntegrations.ShaarliEnabled {
  310. slog.Debug("Sending entry to Shaarli",
  311. slog.Int64("user_id", userIntegrations.UserID),
  312. slog.Int64("entry_id", entry.ID),
  313. slog.String("entry_url", entry.URL),
  314. )
  315. client := shaarli.NewClient(
  316. userIntegrations.ShaarliURL,
  317. userIntegrations.ShaarliAPISecret,
  318. )
  319. if err := client.CreateLink(entry.URL, entry.Title); err != nil {
  320. slog.Error("Unable to send entry to Shaarli",
  321. slog.Int64("user_id", userIntegrations.UserID),
  322. slog.Int64("entry_id", entry.ID),
  323. slog.String("entry_url", entry.URL),
  324. slog.Any("error", err),
  325. )
  326. }
  327. }
  328. if userIntegrations.WebhookEnabled {
  329. slog.Debug("Sending entry to Webhook",
  330. slog.Int64("user_id", userIntegrations.UserID),
  331. slog.Int64("entry_id", entry.ID),
  332. slog.String("entry_url", entry.URL),
  333. slog.String("webhook_url", userIntegrations.WebhookURL),
  334. )
  335. webhookClient := webhook.NewClient(userIntegrations.WebhookURL, userIntegrations.WebhookSecret)
  336. if err := webhookClient.SendSaveEntryWebhookEvent(entry); err != nil {
  337. slog.Error("Unable to send entry to Webhook",
  338. slog.Int64("user_id", userIntegrations.UserID),
  339. slog.Int64("entry_id", entry.ID),
  340. slog.String("entry_url", entry.URL),
  341. slog.String("webhook_url", userIntegrations.WebhookURL),
  342. slog.Any("error", err),
  343. )
  344. }
  345. }
  346. if userIntegrations.OmnivoreEnabled {
  347. slog.Debug("Sending entry to Omnivore",
  348. slog.Int64("user_id", userIntegrations.UserID),
  349. slog.Int64("entry_id", entry.ID),
  350. slog.String("entry_url", entry.URL),
  351. )
  352. client := omnivore.NewClient(userIntegrations.OmnivoreAPIKey, userIntegrations.OmnivoreURL)
  353. if err := client.SaveUrl(entry.URL); err != nil {
  354. slog.Error("Unable to send entry to Omnivore",
  355. slog.Int64("user_id", userIntegrations.UserID),
  356. slog.Int64("entry_id", entry.ID),
  357. slog.String("entry_url", entry.URL),
  358. slog.Any("error", err),
  359. )
  360. }
  361. }
  362. if userIntegrations.RaindropEnabled {
  363. slog.Debug("Sending entry to Raindrop",
  364. slog.Int64("user_id", userIntegrations.UserID),
  365. slog.Int64("entry_id", entry.ID),
  366. slog.String("entry_url", entry.URL),
  367. )
  368. client := raindrop.NewClient(userIntegrations.RaindropToken, userIntegrations.RaindropCollectionID, userIntegrations.RaindropTags)
  369. if err := client.CreateRaindrop(entry.URL, entry.Title); err != nil {
  370. slog.Error("Unable to send entry to Raindrop",
  371. slog.Int64("user_id", userIntegrations.UserID),
  372. slog.Int64("entry_id", entry.ID),
  373. slog.String("entry_url", entry.URL),
  374. slog.Any("error", err),
  375. )
  376. }
  377. }
  378. }
  379. // PushEntries pushes a list of entries to activated third-party providers during feed refreshes.
  380. func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *model.Integration) {
  381. if userIntegrations.MatrixBotEnabled {
  382. slog.Debug("Sending new entries to Matrix",
  383. slog.Int64("user_id", userIntegrations.UserID),
  384. slog.Int("nb_entries", len(entries)),
  385. slog.Int64("feed_id", feed.ID),
  386. )
  387. err := matrixbot.PushEntries(
  388. feed,
  389. entries,
  390. userIntegrations.MatrixBotURL,
  391. userIntegrations.MatrixBotUser,
  392. userIntegrations.MatrixBotPassword,
  393. userIntegrations.MatrixBotChatID,
  394. )
  395. if err != nil {
  396. slog.Error("Unable to send new entries to Matrix",
  397. slog.Int64("user_id", userIntegrations.UserID),
  398. slog.Int("nb_entries", len(entries)),
  399. slog.Int64("feed_id", feed.ID),
  400. slog.Any("error", err),
  401. )
  402. }
  403. }
  404. if userIntegrations.WebhookEnabled {
  405. slog.Debug("Sending new entries to Webhook",
  406. slog.Int64("user_id", userIntegrations.UserID),
  407. slog.Int("nb_entries", len(entries)),
  408. slog.Int64("feed_id", feed.ID),
  409. slog.String("webhook_url", userIntegrations.WebhookURL),
  410. )
  411. webhookClient := webhook.NewClient(userIntegrations.WebhookURL, userIntegrations.WebhookSecret)
  412. if err := webhookClient.SendNewEntriesWebhookEvent(feed, entries); err != nil {
  413. slog.Debug("Unable to send new entries to Webhook",
  414. slog.Int64("user_id", userIntegrations.UserID),
  415. slog.Int("nb_entries", len(entries)),
  416. slog.Int64("feed_id", feed.ID),
  417. slog.String("webhook_url", userIntegrations.WebhookURL),
  418. slog.Any("error", err),
  419. )
  420. }
  421. }
  422. if userIntegrations.NtfyEnabled && feed.NtfyEnabled {
  423. slog.Debug("Sending new entries to Ntfy",
  424. slog.Int64("user_id", userIntegrations.UserID),
  425. slog.Int("nb_entries", len(entries)),
  426. slog.Int64("feed_id", feed.ID),
  427. )
  428. client := ntfy.NewClient(
  429. userIntegrations.NtfyURL,
  430. userIntegrations.NtfyTopic,
  431. userIntegrations.NtfyAPIToken,
  432. userIntegrations.NtfyUsername,
  433. userIntegrations.NtfyPassword,
  434. userIntegrations.NtfyIconURL,
  435. feed.NtfyPriority,
  436. )
  437. if err := client.SendMessages(feed, entries); err != nil {
  438. slog.Warn("Unable to send new entries to Ntfy", slog.Any("error", err))
  439. }
  440. }
  441. // Integrations that only support sending individual entries
  442. if userIntegrations.TelegramBotEnabled || userIntegrations.AppriseEnabled {
  443. for _, entry := range entries {
  444. if userIntegrations.TelegramBotEnabled {
  445. slog.Debug("Sending a new entry to Telegram",
  446. slog.Int64("user_id", userIntegrations.UserID),
  447. slog.Int64("entry_id", entry.ID),
  448. slog.String("entry_url", entry.URL),
  449. )
  450. if err := telegrambot.PushEntry(
  451. feed,
  452. entry,
  453. userIntegrations.TelegramBotToken,
  454. userIntegrations.TelegramBotChatID,
  455. userIntegrations.TelegramBotTopicID,
  456. userIntegrations.TelegramBotDisableWebPagePreview,
  457. userIntegrations.TelegramBotDisableNotification,
  458. userIntegrations.TelegramBotDisableButtons,
  459. ); err != nil {
  460. slog.Error("Unable to send entry to Telegram",
  461. slog.Int64("user_id", userIntegrations.UserID),
  462. slog.Int64("entry_id", entry.ID),
  463. slog.String("entry_url", entry.URL),
  464. slog.Any("error", err),
  465. )
  466. }
  467. }
  468. if userIntegrations.AppriseEnabled {
  469. slog.Debug("Sending a new entry to Apprise",
  470. slog.Int64("user_id", userIntegrations.UserID),
  471. slog.Int64("entry_id", entry.ID),
  472. slog.String("entry_url", entry.URL),
  473. slog.String("apprise_url", userIntegrations.AppriseURL),
  474. )
  475. appriseServiceURLs := userIntegrations.AppriseServicesURL
  476. if feed.AppriseServiceURLs != "" {
  477. appriseServiceURLs = feed.AppriseServiceURLs
  478. }
  479. client := apprise.NewClient(
  480. appriseServiceURLs,
  481. userIntegrations.AppriseURL,
  482. )
  483. if err := client.SendNotification(entry); err != nil {
  484. slog.Error("Unable to send entry to Apprise",
  485. slog.Int64("user_id", userIntegrations.UserID),
  486. slog.Int64("entry_id", entry.ID),
  487. slog.String("entry_url", entry.URL),
  488. slog.String("apprise_url", userIntegrations.AppriseURL),
  489. slog.Any("error", err),
  490. )
  491. }
  492. }
  493. }
  494. }
  495. }