integration.go 16 KB

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