]> Untitled Git - lemmy.git/blob - src/routes/feeds.rs
Move websocket code into workspace (#107)
[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 pub 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(format!("https://{}", Settings::get().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(format!("https://{}", Settings::get().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!("https://{}/inbox", Settings::get().hostname))
229     .items(items);
230
231   if let Some(site_desc) = site_view.description {
232     channel_builder.description(&site_desc);
233   }
234
235   Ok(channel_builder)
236 }
237
238 fn create_reply_and_mention_items(
239   replies: Vec<ReplyView>,
240   mentions: Vec<UserMentionView>,
241 ) -> Result<Vec<Item>, LemmyError> {
242   let mut reply_items: Vec<Item> = replies
243     .iter()
244     .map(|r| {
245       let reply_url = format!(
246         "https://{}/post/{}/comment/{}",
247         Settings::get().hostname,
248         r.post_id,
249         r.id
250       );
251       build_item(&r.creator_name, &r.published, &reply_url, &r.content)
252     })
253     .collect::<Result<Vec<Item>, LemmyError>>()?;
254
255   let mut mention_items: Vec<Item> = mentions
256     .iter()
257     .map(|m| {
258       let mention_url = format!(
259         "https://{}/post/{}/comment/{}",
260         Settings::get().hostname,
261         m.post_id,
262         m.id
263       );
264       build_item(&m.creator_name, &m.published, &mention_url, &m.content)
265     })
266     .collect::<Result<Vec<Item>, LemmyError>>()?;
267
268   reply_items.append(&mut mention_items);
269   Ok(reply_items)
270 }
271
272 fn build_item(
273   creator_name: &str,
274   published: &NaiveDateTime,
275   url: &str,
276   content: &str,
277 ) -> Result<Item, LemmyError> {
278   let mut i = ItemBuilder::default();
279   i.title(format!("Reply from {}", creator_name));
280   let author_url = format!("https://{}/u/{}", Settings::get().hostname, creator_name);
281   i.author(format!(
282     "/u/{} <a href=\"{}\">(link)</a>",
283     creator_name, author_url
284   ));
285   let dt = DateTime::<Utc>::from_utc(*published, Utc);
286   i.pub_date(dt.to_rfc2822());
287   i.comments(url.to_owned());
288   let guid = GuidBuilder::default()
289     .permalink(true)
290     .value(url)
291     .build()
292     .map_err(|e| anyhow!(e))?;
293   i.guid(guid);
294   i.link(url.to_owned());
295   // TODO add images
296   let html = markdown_to_html(&content.to_string());
297   i.description(html);
298   Ok(i.build().map_err(|e| anyhow!(e))?)
299 }
300
301 fn create_post_items(posts: Vec<PostView>) -> Result<Vec<Item>, LemmyError> {
302   let mut items: Vec<Item> = Vec::new();
303
304   for p in posts {
305     let mut i = ItemBuilder::default();
306
307     i.title(p.name);
308
309     let author_url = format!("https://{}/u/{}", Settings::get().hostname, p.creator_name);
310     i.author(format!(
311       "/u/{} <a href=\"{}\">(link)</a>",
312       p.creator_name, author_url
313     ));
314
315     let dt = DateTime::<Utc>::from_utc(p.published, Utc);
316     i.pub_date(dt.to_rfc2822());
317
318     let post_url = format!("https://{}/post/{}", Settings::get().hostname, p.id);
319     i.comments(post_url.to_owned());
320     let guid = GuidBuilder::default()
321       .permalink(true)
322       .value(&post_url)
323       .build()
324       .map_err(|e| anyhow!(e))?;
325     i.guid(guid);
326
327     let community_url = format!(
328       "https://{}/c/{}",
329       Settings::get().hostname,
330       p.community_name
331     );
332
333     let category = CategoryBuilder::default()
334       .name(format!(
335         "/c/{} <a href=\"{}\">(link)</a>",
336         p.community_name, community_url
337       ))
338       .domain(Settings::get().hostname.to_owned())
339       .build()
340       .map_err(|e| anyhow!(e))?;
341
342     i.categories(vec![category]);
343
344     if let Some(url) = p.url {
345       i.link(url);
346     }
347
348     // TODO add images
349     let mut description = format!("submitted by <a href=\"{}\">{}</a> to <a href=\"{}\">{}</a><br>{} points | <a href=\"{}\">{} comments</a>",
350     author_url,
351     p.creator_name,
352     community_url,
353     p.community_name,
354     p.score,
355     post_url,
356     p.number_of_comments);
357
358     if let Some(body) = p.body {
359       let html = markdown_to_html(&body);
360       description.push_str(&html);
361     }
362
363     i.description(description);
364
365     items.push(i.build().map_err(|e| anyhow!(e))?);
366   }
367
368   Ok(items)
369 }