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