]> Untitled Git - lemmy.git/blob - crates/routes/src/feeds.rs
Merge crates db_schema and db_queries
[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_schema::{
7   newtypes::LocalUserId,
8   source::{community::Community, local_user::LocalUser, person::Person},
9   traits::Crud,
10   ListingType,
11   SortType,
12 };
13 use lemmy_db_views::{
14   comment_view::{CommentQueryBuilder, CommentView},
15   post_view::{PostQueryBuilder, PostView},
16   site_view::SiteView,
17 };
18 use lemmy_db_views_actor::person_mention_view::{PersonMentionQueryBuilder, PersonMentionView};
19 use lemmy_utils::{claims::Claims, 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(), SiteView::read).await??;
84
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, &context.settings().get_protocol_and_hostname())?;
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(context.settings().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   req: HttpRequest,
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 req_type: String = req.match_info().get("type").unwrap_or("none").parse()?;
126   let param: String = req.match_info().get("name").unwrap_or("none").parse()?;
127
128   let request_type = match req_type.as_str() {
129     "u" => RequestType::User,
130     "c" => RequestType::Community,
131     "front" => RequestType::Front,
132     "inbox" => RequestType::Inbox,
133     _ => return Err(ErrorBadRequest(LemmyError::from(anyhow!("wrong_type")))),
134   };
135
136   let jwt_secret = context.secret().jwt_secret.to_owned();
137   let protocol_and_hostname = context.settings().get_protocol_and_hostname();
138
139   let builder = blocking(context.pool(), move |conn| match request_type {
140     RequestType::User => get_feed_user(conn, &sort_type, &param, &protocol_and_hostname),
141     RequestType::Community => get_feed_community(conn, &sort_type, &param, &protocol_and_hostname),
142     RequestType::Front => get_feed_front(
143       conn,
144       &jwt_secret,
145       &sort_type,
146       &param,
147       &protocol_and_hostname,
148     ),
149     RequestType::Inbox => get_feed_inbox(conn, &jwt_secret, &param, &protocol_and_hostname),
150   })
151   .await?
152   .map_err(ErrorBadRequest)?;
153
154   let rss = builder.build().map_err(ErrorBadRequest)?.to_string();
155
156   Ok(
157     HttpResponse::Ok()
158       .content_type("application/rss+xml")
159       .body(rss),
160   )
161 }
162
163 fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
164   let sort_query = info
165     .sort
166     .to_owned()
167     .unwrap_or_else(|| SortType::Hot.to_string());
168   SortType::from_str(&sort_query)
169 }
170
171 fn get_feed_user(
172   conn: &PgConnection,
173   sort_type: &SortType,
174   user_name: &str,
175   protocol_and_hostname: &str,
176 ) -> Result<ChannelBuilder, LemmyError> {
177   let site_view = SiteView::read(conn)?;
178   let person = Person::find_by_name(conn, user_name)?;
179
180   let posts = PostQueryBuilder::create(conn)
181     .listing_type(ListingType::All)
182     .sort(*sort_type)
183     .creator_id(person.id)
184     .list()?;
185
186   let items = create_post_items(posts, protocol_and_hostname)?;
187
188   let mut channel_builder = ChannelBuilder::default();
189   channel_builder
190     .namespaces(RSS_NAMESPACE.to_owned())
191     .title(&format!("{} - {}", site_view.site.name, person.name))
192     .link(person.actor_id.to_string())
193     .items(items);
194
195   Ok(channel_builder)
196 }
197
198 fn get_feed_community(
199   conn: &PgConnection,
200   sort_type: &SortType,
201   community_name: &str,
202   protocol_and_hostname: &str,
203 ) -> Result<ChannelBuilder, LemmyError> {
204   let site_view = SiteView::read(conn)?;
205   let community = Community::read_from_name(conn, community_name)?;
206
207   let posts = PostQueryBuilder::create(conn)
208     .listing_type(ListingType::All)
209     .sort(*sort_type)
210     .community_id(community.id)
211     .list()?;
212
213   let items = create_post_items(posts, protocol_and_hostname)?;
214
215   let mut channel_builder = ChannelBuilder::default();
216   channel_builder
217     .namespaces(RSS_NAMESPACE.to_owned())
218     .title(&format!("{} - {}", site_view.site.name, community.name))
219     .link(community.actor_id.to_string())
220     .items(items);
221
222   if let Some(community_desc) = community.description {
223     channel_builder.description(&community_desc);
224   }
225
226   Ok(channel_builder)
227 }
228
229 fn get_feed_front(
230   conn: &PgConnection,
231   jwt_secret: &str,
232   sort_type: &SortType,
233   jwt: &str,
234   protocol_and_hostname: &str,
235 ) -> Result<ChannelBuilder, LemmyError> {
236   let site_view = SiteView::read(conn)?;
237   let local_user_id = LocalUserId(Claims::decode(jwt, jwt_secret)?.claims.sub);
238   let local_user = LocalUser::read(conn, local_user_id)?;
239
240   let posts = PostQueryBuilder::create(conn)
241     .listing_type(ListingType::Subscribed)
242     .my_person_id(local_user.person_id)
243     .show_bot_accounts(local_user.show_bot_accounts)
244     .show_read_posts(local_user.show_read_posts)
245     .sort(*sort_type)
246     .list()?;
247
248   let items = create_post_items(posts, protocol_and_hostname)?;
249
250   let mut channel_builder = ChannelBuilder::default();
251   channel_builder
252     .namespaces(RSS_NAMESPACE.to_owned())
253     .title(&format!("{} - Subscribed", site_view.site.name))
254     .link(protocol_and_hostname)
255     .items(items);
256
257   if let Some(site_desc) = site_view.site.description {
258     channel_builder.description(&site_desc);
259   }
260
261   Ok(channel_builder)
262 }
263
264 fn get_feed_inbox(
265   conn: &PgConnection,
266   jwt_secret: &str,
267   jwt: &str,
268   protocol_and_hostname: &str,
269 ) -> Result<ChannelBuilder, LemmyError> {
270   let site_view = SiteView::read(conn)?;
271   let local_user_id = LocalUserId(Claims::decode(jwt, jwt_secret)?.claims.sub);
272   let local_user = LocalUser::read(conn, local_user_id)?;
273   let person_id = local_user.person_id;
274   let show_bot_accounts = local_user.show_bot_accounts;
275
276   let sort = SortType::New;
277
278   let replies = CommentQueryBuilder::create(conn)
279     .recipient_id(person_id)
280     .my_person_id(person_id)
281     .show_bot_accounts(show_bot_accounts)
282     .sort(sort)
283     .list()?;
284
285   let mentions = PersonMentionQueryBuilder::create(conn)
286     .recipient_id(person_id)
287     .my_person_id(person_id)
288     .sort(sort)
289     .list()?;
290
291   let items = create_reply_and_mention_items(replies, mentions, protocol_and_hostname)?;
292
293   let mut channel_builder = ChannelBuilder::default();
294   channel_builder
295     .namespaces(RSS_NAMESPACE.to_owned())
296     .title(&format!("{} - Inbox", site_view.site.name))
297     .link(format!("{}/inbox", protocol_and_hostname,))
298     .items(items);
299
300   if let Some(site_desc) = site_view.site.description {
301     channel_builder.description(&site_desc);
302   }
303
304   Ok(channel_builder)
305 }
306
307 fn create_reply_and_mention_items(
308   replies: Vec<CommentView>,
309   mentions: Vec<PersonMentionView>,
310   protocol_and_hostname: &str,
311 ) -> Result<Vec<Item>, LemmyError> {
312   let mut reply_items: Vec<Item> = replies
313     .iter()
314     .map(|r| {
315       let reply_url = format!(
316         "{}/post/{}/comment/{}",
317         protocol_and_hostname, r.post.id, r.comment.id
318       );
319       build_item(
320         &r.creator.name,
321         &r.comment.published,
322         &reply_url,
323         &r.comment.content,
324         protocol_and_hostname,
325       )
326     })
327     .collect::<Result<Vec<Item>, LemmyError>>()?;
328
329   let mut mention_items: Vec<Item> = mentions
330     .iter()
331     .map(|m| {
332       let mention_url = format!(
333         "{}/post/{}/comment/{}",
334         protocol_and_hostname, m.post.id, m.comment.id
335       );
336       build_item(
337         &m.creator.name,
338         &m.comment.published,
339         &mention_url,
340         &m.comment.content,
341         protocol_and_hostname,
342       )
343     })
344     .collect::<Result<Vec<Item>, LemmyError>>()?;
345
346   reply_items.append(&mut mention_items);
347   Ok(reply_items)
348 }
349
350 fn build_item(
351   creator_name: &str,
352   published: &NaiveDateTime,
353   url: &str,
354   content: &str,
355   protocol_and_hostname: &str,
356 ) -> Result<Item, LemmyError> {
357   let mut i = ItemBuilder::default();
358   i.title(format!("Reply from {}", creator_name));
359   let author_url = format!("{}/u/{}", protocol_and_hostname, creator_name);
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(
381   posts: Vec<PostView>,
382   protocol_and_hostname: &str,
383 ) -> Result<Vec<Item>, LemmyError> {
384   let mut items: Vec<Item> = Vec::new();
385
386   for p in posts {
387     let mut i = ItemBuilder::default();
388     let mut dc_extension = DublinCoreExtensionBuilder::default();
389
390     i.title(p.post.name);
391
392     dc_extension.creators(vec![p.creator.actor_id.to_string()]);
393
394     let dt = DateTime::<Utc>::from_utc(p.post.published, Utc);
395     i.pub_date(dt.to_rfc2822());
396
397     let post_url = format!("{}/post/{}", protocol_and_hostname, p.post.id);
398     i.link(post_url.to_owned());
399     i.comments(post_url.to_owned());
400     let guid = GuidBuilder::default()
401       .permalink(true)
402       .value(&post_url)
403       .build()
404       .map_err(|e| anyhow!(e))?;
405     i.guid(guid);
406
407     let community_url = format!("{}/c/{}", protocol_and_hostname, p.community.name);
408
409     // TODO add images
410     let mut description = format!("submitted by <a href=\"{}\">{}</a> to <a href=\"{}\">{}</a><br>{} points | <a href=\"{}\">{} comments</a>",
411     p.creator.actor_id,
412     p.creator.name,
413     community_url,
414     p.community.name,
415     p.counts.score,
416     post_url,
417     p.counts.comments);
418
419     // If its a url post, add it to the description
420     if let Some(url) = p.post.url {
421       let link_html = format!("<br><a href=\"{url}\">{url}</a>", url = url);
422       description.push_str(&link_html);
423     }
424
425     if let Some(body) = p.post.body {
426       let html = markdown_to_html(&body);
427       description.push_str(&html);
428     }
429
430     i.description(description);
431
432     i.dublin_core_ext(dc_extension.build().map_err(|e| anyhow!(e))?);
433     items.push(i.build().map_err(|e| anyhow!(e))?);
434   }
435
436   Ok(items)
437 }