1use std::collections::HashSet;
2use std::sync::LazyLock;
3use std::{sync::Arc, time::Duration};
4
5use chrono::Utc;
6use feed_rs::model::Feed;
7use irc::client::Client;
8use log::{debug, error, info, trace};
9use tokio::sync::RwLock;
10
11use miette::{IntoDiagnostic, WrapErr, miette};
12
13use crate::bot::Subscriptions;
14use crate::{APP_USER_AGENT, FEED_REFRESH_INTERVAL_SECONDS};
15
16pub(crate) async fn feed_watcher(subs: Arc<RwLock<Subscriptions>>, client: Arc<Client>) {
18 let mut feed_interval = tokio::time::interval(tokio::time::Duration::from_secs(
19 FEED_REFRESH_INTERVAL_SECONDS as u64,
20 ));
21
22 loop {
23 feed_interval.tick().await;
24 if Utc::now()
26 < subs.read().await.last_check
27 + Duration::from_secs(FEED_REFRESH_INTERVAL_SECONDS as u64 / 2)
28 {
29 continue;
30 }
31 if let Err(e) = update_feeds(subs.clone(), &client).await {
32 error!("{:?}", e);
33 }
34 }
35}
36
37pub(crate) async fn update_feeds(
39 subscriptions: Arc<RwLock<Subscriptions>>,
40 client: &Client,
41) -> miette::Result<()> {
42 info!("Checking feeds updates…");
43 let subs = subscriptions.read().await;
44 let subs_clone = subs.subs.clone();
45 drop(subs);
46 for (sub_url, sub_users) in &subs_clone {
47 debug!("Checking feed {sub_url}");
48 match update_feed(client, subscriptions.clone(), sub_url, sub_users).await {
49 Ok(_) => {
50 }
52 Err(e) => {
53 error!("Error while updating feed {sub_url}: {e}");
54 }
55 }
56 }
57 debug!("Feed checking finished, writing last check");
58 let mut subs = subscriptions.write().await;
59 subs.last_check = Utc::now();
60 subs.save().await?;
61 Ok(())
62}
63
64async fn update_feed(
65 client: &Client,
66 subs: Arc<RwLock<Subscriptions>>,
67 sub_url: &String,
68 sub_users: &HashSet<String>,
69) -> Result<(), miette::Error> {
70 let feed = get_feed(sub_url).await?;
71 trace!("Feed parsed successfully, locking subscriptions…");
72 let sub = subs.read().await;
73 let new_articles = feed.entries.iter().filter(|entry| {
74 if let Some(published) = entry.published
75 && published > sub.last_check
76 {
77 true
78 } else {
79 false
80 }
81 });
82 trace!("filtered articles…");
83 for new in new_articles {
84 let s = format!(
85 "{}: {} {}",
86 feed.title
87 .as_ref()
88 .map(|t| t.content.as_str())
89 .unwrap_or("No title"),
90 new.title
91 .as_ref()
92 .map(|t| t.content.as_str())
93 .unwrap_or("New entry"),
94 new.links
95 .first()
96 .ok_or(miette!(
97 "Could not find a link in entry {new:?} for feed {sub_url}"
98 ))?
99 .href
100 );
101 info!("Sending \"{s}\" to clients {sub_users:?}");
102 for u in sub_users {
103 client.send_privmsg(u, &s).into_diagnostic()?;
104 }
105 }
106 Ok(())
107}
108
109pub(crate) async fn get_feed(feed_url: &str) -> miette::Result<Feed> {
110 static HTTP_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
111 reqwest::Client::builder()
112 .user_agent(APP_USER_AGENT)
113 .build()
114 .into_diagnostic()
115 .expect("Could not build http client")
116 });
117 trace!("update_feed {feed_url} making request…");
118 let response = HTTP_CLIENT
119 .get(feed_url)
120 .send()
121 .await
122 .into_diagnostic()
123 .wrap_err(format!("unable to fetch {feed_url}"))?
124 .bytes()
125 .await
126 .into_diagnostic()?;
127 trace!("update_feed: got response; parsing…");
128 let feed = feed_rs::parser::parse(response.as_ref()).into_diagnostic()?;
129 Ok(feed)
130}