integration.go 16 KB

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