1 use actix_web::{error::ErrorBadRequest, *};
3 use chrono::{DateTime, NaiveDateTime, Utc};
4 use diesel::PgConnection;
5 use lemmy_api::claims::Claims;
7 comment_view::{ReplyQueryBuilder, ReplyView},
9 post_view::{PostQueryBuilder, PostView},
12 user_mention_view::{UserMentionQueryBuilder, UserMentionView},
16 use lemmy_structs::blocking;
17 use lemmy_utils::{settings::Settings, utils::markdown_to_html, LemmyError};
18 use lemmy_websocket::LemmyContext;
20 extension::dublincore::DublinCoreExtensionBuilder,
26 use serde::Deserialize;
27 use std::{collections::HashMap, str::FromStr};
28 use strum::ParseError;
30 #[derive(Deserialize)]
42 pub fn config(cfg: &mut web::ServiceConfig) {
44 .route("/feeds/{type}/{name}.xml", web::get().to(get_feed))
45 .route("/feeds/all.xml", web::get().to(get_all_feed))
46 .route("/feeds/local.xml", web::get().to(get_local_feed));
50 static ref RSS_NAMESPACE: HashMap<String, String> = {
51 let mut h = HashMap::new();
54 rss::extension::dublincore::NAMESPACE.to_string(),
60 async fn get_all_feed(
61 info: web::Query<Params>,
62 context: web::Data<LemmyContext>,
63 ) -> Result<HttpResponse, Error> {
64 let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?;
65 Ok(get_feed_data(&context, ListingType::All, sort_type).await?)
68 async fn get_local_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::Local, sort_type).await?)
76 async fn get_feed_data(
77 context: &LemmyContext,
78 listing_type: ListingType,
80 ) -> Result<HttpResponse, LemmyError> {
81 let site_view = blocking(context.pool(), move |conn| SiteView::read(&conn)).await??;
83 let listing_type_ = listing_type.clone();
84 let posts = blocking(context.pool(), move |conn| {
85 PostQueryBuilder::create(&conn)
86 .listing_type(&listing_type_)
92 let items = create_post_items(posts)?;
94 let mut channel_builder = ChannelBuilder::default();
96 .namespaces(RSS_NAMESPACE.to_owned())
100 listing_type.to_string()
102 .link(Settings::get().get_protocol_and_hostname())
105 if let Some(site_desc) = site_view.description {
106 channel_builder.description(&site_desc);
109 let rss = channel_builder.build().map_err(|e| anyhow!(e))?.to_string();
112 .content_type("application/rss+xml")
118 web::Path((req_type, param)): web::Path<(String, String)>,
119 info: web::Query<Params>,
120 context: web::Data<LemmyContext>,
121 ) -> Result<HttpResponse, Error> {
122 let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?;
124 let request_type = match req_type.as_str() {
125 "u" => RequestType::User,
126 "c" => RequestType::Community,
127 "front" => RequestType::Front,
128 "inbox" => RequestType::Inbox,
129 _ => return Err(ErrorBadRequest(LemmyError::from(anyhow!("wrong_type")))),
132 let builder = blocking(context.pool(), move |conn| match request_type {
133 RequestType::User => get_feed_user(conn, &sort_type, param),
134 RequestType::Community => get_feed_community(conn, &sort_type, param),
135 RequestType::Front => get_feed_front(conn, &sort_type, param),
136 RequestType::Inbox => get_feed_inbox(conn, param),
139 .map_err(ErrorBadRequest)?;
141 let rss = builder.build().map_err(ErrorBadRequest)?.to_string();
145 .content_type("application/rss+xml")
150 fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
151 let sort_query = info
154 .unwrap_or_else(|| SortType::Hot.to_string());
155 SortType::from_str(&sort_query)
160 sort_type: &SortType,
162 ) -> Result<ChannelBuilder, LemmyError> {
163 let site_view = SiteView::read(&conn)?;
164 let user = User_::find_by_username(&conn, &user_name)?;
165 let user_url = user.get_profile_url(&Settings::get().hostname);
167 let posts = PostQueryBuilder::create(&conn)
168 .listing_type(&ListingType::All)
170 .for_creator_id(user.id)
173 let items = create_post_items(posts)?;
175 let mut channel_builder = ChannelBuilder::default();
177 .namespaces(RSS_NAMESPACE.to_owned())
178 .title(&format!("{} - {}", site_view.name, user.name))
185 fn get_feed_community(
187 sort_type: &SortType,
188 community_name: String,
189 ) -> Result<ChannelBuilder, LemmyError> {
190 let site_view = SiteView::read(&conn)?;
191 let community = Community::read_from_name(&conn, &community_name)?;
193 let posts = PostQueryBuilder::create(&conn)
194 .listing_type(&ListingType::All)
196 .for_community_id(community.id)
199 let items = create_post_items(posts)?;
201 let mut channel_builder = ChannelBuilder::default();
203 .namespaces(RSS_NAMESPACE.to_owned())
204 .title(&format!("{} - {}", site_view.name, community.name))
205 .link(community.actor_id)
208 if let Some(community_desc) = community.description {
209 channel_builder.description(&community_desc);
217 sort_type: &SortType,
219 ) -> Result<ChannelBuilder, LemmyError> {
220 let site_view = SiteView::read(&conn)?;
221 let user_id = Claims::decode(&jwt)?.claims.id;
223 let posts = PostQueryBuilder::create(&conn)
224 .listing_type(&ListingType::Subscribed)
229 let items = create_post_items(posts)?;
231 let mut channel_builder = ChannelBuilder::default();
233 .namespaces(RSS_NAMESPACE.to_owned())
234 .title(&format!("{} - Subscribed", site_view.name))
235 .link(Settings::get().get_protocol_and_hostname())
238 if let Some(site_desc) = site_view.description {
239 channel_builder.description(&site_desc);
245 fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, LemmyError> {
246 let site_view = SiteView::read(&conn)?;
247 let user_id = Claims::decode(&jwt)?.claims.id;
249 let sort = SortType::New;
251 let replies = ReplyQueryBuilder::create(&conn, user_id)
255 let mentions = UserMentionQueryBuilder::create(&conn, user_id)
259 let items = create_reply_and_mention_items(replies, mentions)?;
261 let mut channel_builder = ChannelBuilder::default();
263 .namespaces(RSS_NAMESPACE.to_owned())
264 .title(&format!("{} - Inbox", site_view.name))
267 Settings::get().get_protocol_and_hostname()
271 if let Some(site_desc) = site_view.description {
272 channel_builder.description(&site_desc);
278 fn create_reply_and_mention_items(
279 replies: Vec<ReplyView>,
280 mentions: Vec<UserMentionView>,
281 ) -> Result<Vec<Item>, LemmyError> {
282 let mut reply_items: Vec<Item> = replies
285 let reply_url = format!(
286 "{}/post/{}/comment/{}",
287 Settings::get().get_protocol_and_hostname(),
291 build_item(&r.creator_name, &r.published, &reply_url, &r.content)
293 .collect::<Result<Vec<Item>, LemmyError>>()?;
295 let mut mention_items: Vec<Item> = mentions
298 let mention_url = format!(
299 "{}/post/{}/comment/{}",
300 Settings::get().get_protocol_and_hostname(),
304 build_item(&m.creator_name, &m.published, &mention_url, &m.content)
306 .collect::<Result<Vec<Item>, LemmyError>>()?;
308 reply_items.append(&mut mention_items);
314 published: &NaiveDateTime,
317 ) -> Result<Item, LemmyError> {
318 let mut i = ItemBuilder::default();
319 i.title(format!("Reply from {}", creator_name));
320 let author_url = format!(
322 Settings::get().get_protocol_and_hostname(),
326 "/u/{} <a href=\"{}\">(link)</a>",
327 creator_name, author_url
329 let dt = DateTime::<Utc>::from_utc(*published, Utc);
330 i.pub_date(dt.to_rfc2822());
331 i.comments(url.to_owned());
332 let guid = GuidBuilder::default()
336 .map_err(|e| anyhow!(e))?;
338 i.link(url.to_owned());
340 let html = markdown_to_html(&content.to_string());
342 Ok(i.build().map_err(|e| anyhow!(e))?)
345 fn create_post_items(posts: Vec<PostView>) -> Result<Vec<Item>, LemmyError> {
346 let mut items: Vec<Item> = Vec::new();
349 let mut i = ItemBuilder::default();
350 let mut dc_extension = DublinCoreExtensionBuilder::default();
354 dc_extension.creators(vec![p.creator_actor_id.to_owned()]);
356 let dt = DateTime::<Utc>::from_utc(p.published, Utc);
357 i.pub_date(dt.to_rfc2822());
359 let post_url = format!(
361 Settings::get().get_protocol_and_hostname(),
364 i.comments(post_url.to_owned());
365 let guid = GuidBuilder::default()
369 .map_err(|e| anyhow!(e))?;
372 let community_url = format!(
374 Settings::get().get_protocol_and_hostname(),
378 // TODO: for category we should just put the name of the category, but then we would have
379 // to read each community from the db
381 if let Some(url) = p.url {
386 let mut description = format!("submitted by <a href=\"{}\">{}</a> to <a href=\"{}\">{}</a><br>{} points | <a href=\"{}\">{} comments</a>",
393 p.number_of_comments);
395 if let Some(body) = p.body {
396 let html = markdown_to_html(&body);
397 description.push_str(&html);
400 i.description(description);
402 i.dublin_core_ext(dc_extension.build().map_err(|e| anyhow!(e))?);
403 items.push(i.build().map_err(|e| anyhow!(e))?);