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