]> Untitled Git - lemmy.git/blob - src/routes/feeds.rs
Generate valid RSS feed (fixes #1274)
[lemmy.git] / src / routes / feeds.rs
1 use actix_web::{error::ErrorBadRequest, *};
2 use anyhow::anyhow;
3 use chrono::{DateTime, NaiveDateTime, Utc};
4 use diesel::PgConnection;
5 use lemmy_api::claims::Claims;
6 use lemmy_db::{
7   comment_view::{ReplyQueryBuilder, ReplyView},
8   community::Community,
9   post_view::{PostQueryBuilder, PostView},
10   site_view::SiteView,
11   user::User_,
12   user_mention_view::{UserMentionQueryBuilder, UserMentionView},
13   ListingType,
14   SortType,
15 };
16 use lemmy_structs::blocking;
17 use lemmy_utils::{settings::Settings, utils::markdown_to_html, LemmyError};
18 use lemmy_websocket::LemmyContext;
19 use rss::{
20   extension::dublincore::DublinCoreExtensionBuilder,
21   ChannelBuilder,
22   GuidBuilder,
23   Item,
24   ItemBuilder,
25 };
26 use serde::Deserialize;
27 use std::{collections::HashMap, str::FromStr};
28 use strum::ParseError;
29
30 #[derive(Deserialize)]
31 struct Params {
32   sort: Option<String>,
33 }
34
35 enum RequestType {
36   Community,
37   User,
38   Front,
39   Inbox,
40 }
41
42 pub fn config(cfg: &mut web::ServiceConfig) {
43   cfg
44     .route("/feeds/{type}/{name}.xml", web::get().to(get_feed))
45     .route("/feeds/all.xml", web::get().to(get_all_feed));
46 }
47
48 lazy_static! {
49   static ref RSS_NAMESPACE: HashMap<String, String> = {
50     let mut h = HashMap::new();
51     h.insert(
52       "dc".to_string(),
53       rss::extension::dublincore::NAMESPACE.to_string(),
54     );
55     h
56   };
57 }
58
59 async fn get_all_feed(
60   info: web::Query<Params>,
61   context: web::Data<LemmyContext>,
62 ) -> Result<HttpResponse, Error> {
63   let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?;
64
65   let rss = blocking(context.pool(), move |conn| {
66     get_feed_all_data(conn, &sort_type)
67   })
68   .await?
69   .map_err(ErrorBadRequest)?;
70
71   Ok(
72     HttpResponse::Ok()
73       .content_type("application/rss+xml")
74       .body(rss),
75   )
76 }
77
78 fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String, LemmyError> {
79   let site_view = SiteView::read(&conn)?;
80
81   let posts = PostQueryBuilder::create(&conn)
82     .listing_type(ListingType::All)
83     .sort(sort_type)
84     .list()?;
85
86   let items = create_post_items(posts)?;
87
88   let mut channel_builder = ChannelBuilder::default();
89   channel_builder
90     .namespaces(RSS_NAMESPACE.to_owned())
91     .title(&format!("{} - All", site_view.name))
92     .link(Settings::get().get_protocol_and_hostname())
93     .items(items);
94
95   if let Some(site_desc) = site_view.description {
96     channel_builder.description(&site_desc);
97   }
98
99   Ok(channel_builder.build().map_err(|e| anyhow!(e))?.to_string())
100 }
101
102 async fn get_feed(
103   web::Path((req_type, param)): web::Path<(String, String)>,
104   info: web::Query<Params>,
105   context: web::Data<LemmyContext>,
106 ) -> Result<HttpResponse, Error> {
107   let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?;
108
109   let request_type = match req_type.as_str() {
110     "u" => RequestType::User,
111     "c" => RequestType::Community,
112     "front" => RequestType::Front,
113     "inbox" => RequestType::Inbox,
114     _ => return Err(ErrorBadRequest(LemmyError::from(anyhow!("wrong_type")))),
115   };
116
117   let builder = blocking(context.pool(), move |conn| match request_type {
118     RequestType::User => get_feed_user(conn, &sort_type, param),
119     RequestType::Community => get_feed_community(conn, &sort_type, param),
120     RequestType::Front => get_feed_front(conn, &sort_type, param),
121     RequestType::Inbox => get_feed_inbox(conn, param),
122   })
123   .await?
124   .map_err(ErrorBadRequest)?;
125
126   let rss = builder.build().map_err(ErrorBadRequest)?.to_string();
127
128   Ok(
129     HttpResponse::Ok()
130       .content_type("application/rss+xml")
131       .body(rss),
132   )
133 }
134
135 fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
136   let sort_query = info
137     .sort
138     .to_owned()
139     .unwrap_or_else(|| SortType::Hot.to_string());
140   SortType::from_str(&sort_query)
141 }
142
143 fn get_feed_user(
144   conn: &PgConnection,
145   sort_type: &SortType,
146   user_name: String,
147 ) -> Result<ChannelBuilder, LemmyError> {
148   let site_view = SiteView::read(&conn)?;
149   let user = User_::find_by_username(&conn, &user_name)?;
150   let user_url = user.get_profile_url(&Settings::get().hostname);
151
152   let posts = PostQueryBuilder::create(&conn)
153     .listing_type(ListingType::All)
154     .sort(sort_type)
155     .for_creator_id(user.id)
156     .list()?;
157
158   let items = create_post_items(posts)?;
159
160   let mut channel_builder = ChannelBuilder::default();
161   channel_builder
162     .namespaces(RSS_NAMESPACE.to_owned())
163     .title(&format!("{} - {}", site_view.name, user.name))
164     .link(user_url)
165     .items(items);
166
167   Ok(channel_builder)
168 }
169
170 fn get_feed_community(
171   conn: &PgConnection,
172   sort_type: &SortType,
173   community_name: String,
174 ) -> Result<ChannelBuilder, LemmyError> {
175   let site_view = SiteView::read(&conn)?;
176   let community = Community::read_from_name(&conn, &community_name)?;
177
178   let posts = PostQueryBuilder::create(&conn)
179     .listing_type(ListingType::All)
180     .sort(sort_type)
181     .for_community_id(community.id)
182     .list()?;
183
184   let items = create_post_items(posts)?;
185
186   let mut channel_builder = ChannelBuilder::default();
187   channel_builder
188     .namespaces(RSS_NAMESPACE.to_owned())
189     .title(&format!("{} - {}", site_view.name, community.name))
190     .link(community.actor_id)
191     .items(items);
192
193   if let Some(community_desc) = community.description {
194     channel_builder.description(&community_desc);
195   }
196
197   Ok(channel_builder)
198 }
199
200 fn get_feed_front(
201   conn: &PgConnection,
202   sort_type: &SortType,
203   jwt: String,
204 ) -> Result<ChannelBuilder, LemmyError> {
205   let site_view = SiteView::read(&conn)?;
206   let user_id = Claims::decode(&jwt)?.claims.id;
207
208   let posts = PostQueryBuilder::create(&conn)
209     .listing_type(ListingType::Subscribed)
210     .sort(sort_type)
211     .my_user_id(user_id)
212     .list()?;
213
214   let items = create_post_items(posts)?;
215
216   let mut channel_builder = ChannelBuilder::default();
217   channel_builder
218     .namespaces(RSS_NAMESPACE.to_owned())
219     .title(&format!("{} - Subscribed", site_view.name))
220     .link(Settings::get().get_protocol_and_hostname())
221     .items(items);
222
223   if let Some(site_desc) = site_view.description {
224     channel_builder.description(&site_desc);
225   }
226
227   Ok(channel_builder)
228 }
229
230 fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, LemmyError> {
231   let site_view = SiteView::read(&conn)?;
232   let user_id = Claims::decode(&jwt)?.claims.id;
233
234   let sort = SortType::New;
235
236   let replies = ReplyQueryBuilder::create(&conn, user_id)
237     .sort(&sort)
238     .list()?;
239
240   let mentions = UserMentionQueryBuilder::create(&conn, user_id)
241     .sort(&sort)
242     .list()?;
243
244   let items = create_reply_and_mention_items(replies, mentions)?;
245
246   let mut channel_builder = ChannelBuilder::default();
247   channel_builder
248     .namespaces(RSS_NAMESPACE.to_owned())
249     .title(&format!("{} - Inbox", site_view.name))
250     .link(format!(
251       "{}/inbox",
252       Settings::get().get_protocol_and_hostname()
253     ))
254     .items(items);
255
256   if let Some(site_desc) = site_view.description {
257     channel_builder.description(&site_desc);
258   }
259
260   Ok(channel_builder)
261 }
262
263 fn create_reply_and_mention_items(
264   replies: Vec<ReplyView>,
265   mentions: Vec<UserMentionView>,
266 ) -> Result<Vec<Item>, LemmyError> {
267   let mut reply_items: Vec<Item> = replies
268     .iter()
269     .map(|r| {
270       let reply_url = format!(
271         "{}/post/{}/comment/{}",
272         Settings::get().get_protocol_and_hostname(),
273         r.post_id,
274         r.id
275       );
276       build_item(&r.creator_name, &r.published, &reply_url, &r.content)
277     })
278     .collect::<Result<Vec<Item>, LemmyError>>()?;
279
280   let mut mention_items: Vec<Item> = mentions
281     .iter()
282     .map(|m| {
283       let mention_url = format!(
284         "{}/post/{}/comment/{}",
285         Settings::get().get_protocol_and_hostname(),
286         m.post_id,
287         m.id
288       );
289       build_item(&m.creator_name, &m.published, &mention_url, &m.content)
290     })
291     .collect::<Result<Vec<Item>, LemmyError>>()?;
292
293   reply_items.append(&mut mention_items);
294   Ok(reply_items)
295 }
296
297 fn build_item(
298   creator_name: &str,
299   published: &NaiveDateTime,
300   url: &str,
301   content: &str,
302 ) -> Result<Item, LemmyError> {
303   let mut i = ItemBuilder::default();
304   i.title(format!("Reply from {}", creator_name));
305   let author_url = format!(
306     "{}/u/{}",
307     Settings::get().get_protocol_and_hostname(),
308     creator_name
309   );
310   i.author(format!(
311     "/u/{} <a href=\"{}\">(link)</a>",
312     creator_name, author_url
313   ));
314   let dt = DateTime::<Utc>::from_utc(*published, Utc);
315   i.pub_date(dt.to_rfc2822());
316   i.comments(url.to_owned());
317   let guid = GuidBuilder::default()
318     .permalink(true)
319     .value(url)
320     .build()
321     .map_err(|e| anyhow!(e))?;
322   i.guid(guid);
323   i.link(url.to_owned());
324   // TODO add images
325   let html = markdown_to_html(&content.to_string());
326   i.description(html);
327   Ok(i.build().map_err(|e| anyhow!(e))?)
328 }
329
330 fn create_post_items(posts: Vec<PostView>) -> Result<Vec<Item>, LemmyError> {
331   let mut items: Vec<Item> = Vec::new();
332
333   for p in posts {
334     let mut i = ItemBuilder::default();
335     let mut dc_extension = DublinCoreExtensionBuilder::default();
336
337     i.title(p.name);
338
339     dc_extension.creators(vec![p.creator_actor_id.to_owned()]);
340
341     let dt = DateTime::<Utc>::from_utc(p.published, Utc);
342     i.pub_date(dt.to_rfc2822());
343
344     let post_url = format!(
345       "{}/post/{}",
346       Settings::get().get_protocol_and_hostname(),
347       p.id
348     );
349     i.comments(post_url.to_owned());
350     let guid = GuidBuilder::default()
351       .permalink(true)
352       .value(&post_url)
353       .build()
354       .map_err(|e| anyhow!(e))?;
355     i.guid(guid);
356
357     let community_url = format!(
358       "{}/c/{}",
359       Settings::get().get_protocol_and_hostname(),
360       p.community_name
361     );
362
363     // TODO: for category we should just put the name of the category, but then we would have
364     //       to read each community from the db
365
366     if let Some(url) = p.url {
367       i.link(url);
368     }
369
370     // TODO add images
371     let mut description = format!("submitted by <a href=\"{}\">{}</a> to <a href=\"{}\">{}</a><br>{} points | <a href=\"{}\">{} comments</a>",
372     p.creator_actor_id,
373     p.creator_name,
374     community_url,
375     p.community_name,
376     p.score,
377     post_url,
378     p.number_of_comments);
379
380     if let Some(body) = p.body {
381       let html = markdown_to_html(&body);
382       description.push_str(&html);
383     }
384
385     i.description(description);
386
387     i.dublin_core_ext(dc_extension.build().map_err(|e| anyhow!(e))?);
388     items.push(i.build().map_err(|e| anyhow!(e))?);
389   }
390
391   Ok(items)
392 }