integration.go 15 KB

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