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