From 2152d2eaf53aa403a775583e920f824a7fcafc84 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 7 Dec 2019 16:39:43 -0800 Subject: [PATCH] Adding RSS feeds for inbox and subscribed. Refactored RSS code. - Fixes #349 --- server/src/feeds.rs | 336 ++++++++++++++++++++++++++++-------- ui/src/components/inbox.tsx | 22 ++- ui/src/components/main.tsx | 13 ++ 3 files changed, 292 insertions(+), 79 deletions(-) diff --git a/server/src/feeds.rs b/server/src/feeds.rs index 8a1db28b..737207fa 100644 --- a/server/src/feeds.rs +++ b/server/src/feeds.rs @@ -1,16 +1,17 @@ -extern crate htmlescape; extern crate rss; use super::*; +use crate::db::comment_view::ReplyView; use crate::db::community::Community; use crate::db::community_view::SiteView; use crate::db::post_view::PostView; use crate::db::user::User_; +use crate::db::user_mention_view::UserMentionView; use crate::db::{establish_connection, ListingType, SortType}; use crate::Settings; use actix_web::body::Body; use actix_web::{web, HttpResponse, Result}; -use diesel::result::Error; +use failure::Error; use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder}; use serde::Deserialize; use std::str::FromStr; @@ -22,9 +23,10 @@ pub struct Params { } enum RequestType { - All, Community, User, + Front, + Inbox, } pub fn get_all_feed(info: web::Query) -> HttpResponse { @@ -33,27 +35,40 @@ pub fn get_all_feed(info: web::Query) -> HttpResponse { Err(_) => return HttpResponse::BadRequest().finish(), }; - match get_feed_internal(&sort_type, RequestType::All, None) { + let feed_result = get_feed_all_data(&sort_type); + + match feed_result { Ok(rss) => HttpResponse::Ok() .content_type("application/rss+xml") .body(rss), - Err(_) => HttpResponse::InternalServerError().finish(), + Err(_) => HttpResponse::NotFound().finish(), } } -pub fn get_feed(path: web::Path<(char, String)>, info: web::Query) -> HttpResponse { +pub fn get_feed(path: web::Path<(String, String)>, info: web::Query) -> HttpResponse { let sort_type = match get_sort_type(info) { Ok(sort_type) => sort_type, Err(_) => return HttpResponse::BadRequest().finish(), }; - let request_type = match path.0 { - 'u' => RequestType::User, - 'c' => RequestType::Community, + let request_type = match path.0.as_ref() { + "u" => RequestType::User, + "c" => RequestType::Community, + "front" => RequestType::Front, + "inbox" => RequestType::Inbox, _ => return HttpResponse::NotFound().finish(), }; - match get_feed_internal(&sort_type, request_type, Some(path.1.to_owned())) { + let param = path.1.to_owned(); + + let feed_result = match request_type { + RequestType::User => get_feed_user(&sort_type, param), + RequestType::Community => get_feed_community(&sort_type, param), + RequestType::Front => get_feed_front(&sort_type, param), + RequestType::Inbox => get_feed_inbox(param), + }; + + match feed_result { Ok(rss) => HttpResponse::Ok() .content_type("application/rss+xml") .body(rss), @@ -66,70 +81,17 @@ fn get_sort_type(info: web::Query) -> Result { SortType::from_str(&sort_query) } -fn get_feed_internal( - sort_type: &SortType, - request_type: RequestType, - name: Option, -) -> Result { +fn get_feed_all_data(sort_type: &SortType) -> Result { let conn = establish_connection(); - let mut community_id: Option = None; - let mut creator_id: Option = None; - let site_view = SiteView::read(&conn)?; - let mut channel_builder = ChannelBuilder::default(); - - // TODO do channel image, need to externalize - - match request_type { - RequestType::All => { - channel_builder - .title(htmlescape::encode_minimal(&site_view.name)) - .link(format!("https://{}", Settings::get().hostname)); - - if let Some(site_desc) = site_view.description { - channel_builder.description(htmlescape::encode_minimal(&site_desc)); - } - } - RequestType::Community => { - let community = Community::read_from_name(&conn, name.unwrap())?; - community_id = Some(community.id); - - let community_url = format!("https://{}/c/{}", Settings::get().hostname, community.name); - - channel_builder - .title(htmlescape::encode_minimal(&format!( - "{} - {}", - site_view.name, community.name - ))) - .link(community_url); - - if let Some(community_desc) = community.description { - channel_builder.description(htmlescape::encode_minimal(&community_desc)); - } - } - RequestType::User => { - let creator = User_::find_by_email_or_username(&conn, &name.unwrap())?; - creator_id = Some(creator.id); - - let creator_url = format!("https://{}/u/{}", Settings::get().hostname, creator.name); - - channel_builder - .title(htmlescape::encode_minimal(&format!( - "{} - {}", - site_view.name, creator.name - ))) - .link(creator_url); - } - } - let posts = PostView::list( &conn, ListingType::All, sort_type, - community_id, - creator_id, + None, + None, None, None, None, @@ -140,12 +102,243 @@ fn get_feed_internal( None, )?; + let items = create_post_items(posts); + + let mut channel_builder = ChannelBuilder::default(); + channel_builder + .title(&format!("{} - All", site_view.name)) + .link(format!("https://{}", Settings::get().hostname)) + .items(items); + + if let Some(site_desc) = site_view.description { + channel_builder.description(&site_desc); + } + + Ok(channel_builder.build().unwrap().to_string()) +} + +fn get_feed_user(sort_type: &SortType, user_name: String) -> Result { + let conn = establish_connection(); + + let site_view = SiteView::read(&conn)?; + let user = User_::find_by_email_or_username(&conn, &user_name)?; + let user_url = format!("https://{}/u/{}", Settings::get().hostname, user.name); + + let posts = PostView::list( + &conn, + ListingType::All, + sort_type, + None, + Some(user.id), + None, + None, + None, + true, + false, + false, + None, + None, + )?; + + let items = create_post_items(posts); + + let mut channel_builder = ChannelBuilder::default(); + channel_builder + .title(&format!("{} - {}", site_view.name, user.name)) + .link(user_url) + .items(items); + + Ok(channel_builder.build().unwrap().to_string()) +} + +fn get_feed_community(sort_type: &SortType, community_name: String) -> Result { + let conn = establish_connection(); + + let site_view = SiteView::read(&conn)?; + let community = Community::read_from_name(&conn, community_name)?; + let community_url = format!("https://{}/c/{}", Settings::get().hostname, community.name); + + let posts = PostView::list( + &conn, + ListingType::All, + sort_type, + Some(community.id), + None, + None, + None, + None, + true, + false, + false, + None, + None, + )?; + + let items = create_post_items(posts); + + let mut channel_builder = ChannelBuilder::default(); + channel_builder + .title(&format!("{} - {}", site_view.name, community.name)) + .link(community_url) + .items(items); + + if let Some(community_desc) = community.description { + channel_builder.description(&community_desc); + } + + Ok(channel_builder.build().unwrap().to_string()) +} + +fn get_feed_front(sort_type: &SortType, jwt: String) -> Result { + let conn = establish_connection(); + + let site_view = SiteView::read(&conn)?; + let user_id = db::user::Claims::decode(&jwt)?.claims.id; + + let posts = PostView::list( + &conn, + ListingType::Subscribed, + sort_type, + None, + None, + None, + None, + Some(user_id), + true, + false, + false, + None, + None, + )?; + + let items = create_post_items(posts); + + let mut channel_builder = ChannelBuilder::default(); + channel_builder + .title(&format!("{} - Subscribed", site_view.name)) + .link(format!("https://{}", Settings::get().hostname)) + .items(items); + + if let Some(site_desc) = site_view.description { + channel_builder.description(&site_desc); + } + + Ok(channel_builder.build().unwrap().to_string()) +} + +fn get_feed_inbox(jwt: String) -> Result { + let conn = establish_connection(); + + let site_view = SiteView::read(&conn)?; + let user_id = db::user::Claims::decode(&jwt)?.claims.id; + + let sort = SortType::New; + + let replies = ReplyView::get_replies(&conn, user_id, &sort, false, None, None)?; + + let mentions = UserMentionView::get_mentions(&conn, user_id, &sort, false, None, None)?; + + let items = create_reply_and_mention_items(replies, mentions); + + let mut channel_builder = ChannelBuilder::default(); + channel_builder + .title(&format!("{} - Inbox", site_view.name)) + .link(format!("https://{}/inbox", Settings::get().hostname)) + .items(items); + + if let Some(site_desc) = site_view.description { + channel_builder.description(&site_desc); + } + + Ok(channel_builder.build().unwrap().to_string()) +} + +fn create_reply_and_mention_items( + replies: Vec, + mentions: Vec, +) -> Vec { + let mut items: Vec = Vec::new(); + + for r in replies { + let mut i = ItemBuilder::default(); + + i.title(format!("Reply from {}", r.creator_name)); + + let author_url = format!("https://{}/u/{}", Settings::get().hostname, r.creator_name); + i.author(format!( + "/u/{} (link)", + r.creator_name, author_url + )); + + let dt = DateTime::::from_utc(r.published, Utc); + i.pub_date(dt.to_rfc2822()); + + let reply_url = format!( + "https://{}/post/{}/comment/{}", + Settings::get().hostname, + r.post_id, + r.id + ); + i.comments(reply_url.to_owned()); + let guid = GuidBuilder::default() + .permalink(true) + .value(&reply_url) + .build(); + i.guid(guid.unwrap()); + + i.link(reply_url); + + // TODO find a markdown to html parser here, do images, etc + i.description(r.content); + + items.push(i.build().unwrap()); + } + + for m in mentions { + let mut i = ItemBuilder::default(); + + i.title(format!("Mention from {}", m.creator_name)); + + let author_url = format!("https://{}/u/{}", Settings::get().hostname, m.creator_name); + i.author(format!( + "/u/{} (link)", + m.creator_name, author_url + )); + + let dt = DateTime::::from_utc(m.published, Utc); + i.pub_date(dt.to_rfc2822()); + + let mention_url = format!( + "https://{}/post/{}/comment/{}", + Settings::get().hostname, + m.post_id, + m.id + ); + i.comments(mention_url.to_owned()); + let guid = GuidBuilder::default() + .permalink(true) + .value(&mention_url) + .build(); + i.guid(guid.unwrap()); + + i.link(mention_url); + + // TODO find a markdown to html parser here, do images, etc + i.description(m.content); + + items.push(i.build().unwrap()); + } + + items +} + +fn create_post_items(posts: Vec) -> Vec { let mut items: Vec = Vec::new(); for p in posts { let mut i = ItemBuilder::default(); - i.title(htmlescape::encode_minimal(&p.name)); + i.title(p.name); let author_url = format!("https://{}/u/{}", Settings::get().hostname, p.creator_name); i.author(format!( @@ -154,7 +347,7 @@ fn get_feed_internal( )); let dt = DateTime::::from_utc(p.published, Utc); - i.pub_date(htmlescape::encode_minimal(&dt.to_rfc2822())); + i.pub_date(dt.to_rfc2822()); let post_url = format!("https://{}/post/{}", Settings::get().hostname, p.id); i.comments(post_url.to_owned()); @@ -203,10 +396,5 @@ fn get_feed_internal( items.push(i.build().unwrap()); } - channel_builder.items(items); - - let channel = channel_builder.build().unwrap(); - channel.write_to(::std::io::sink()).unwrap(); - - Ok(channel.to_string()) + items } diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index bcde9363..39109a5d 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -92,11 +92,23 @@ export class Inbox extends Component {
- - - ## - - + + ## + + + + + # + + +
{this.state.replies.length + this.state.mentions.length > 0 && this.state.unreadOrAll == UnreadOrAll.Unread && ( diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index f4ec779f..0d6be91d 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -444,6 +444,19 @@ export class Main extends Component { )} + {UserService.Instance.user && + this.state.type_ == ListingType.Subscribed && ( + + + # + + + )}
); }