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