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