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