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