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