integration.go 14 KB

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