integration.go 18 KB

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