Adding RSS feeds for inbox and subscribed. Refactored RSS code.

- Fixes #349
This commit is contained in:
Dessalines 2019-12-07 16:39:43 -08:00
parent 88402909ff
commit a29af98e4b
3 changed files with 292 additions and 79 deletions

View file

@ -1,16 +1,17 @@
extern crate htmlescape;
extern crate rss; extern crate rss;
use super::*; use super::*;
use crate::db::comment_view::ReplyView;
use crate::db::community::Community; use crate::db::community::Community;
use crate::db::community_view::SiteView; use crate::db::community_view::SiteView;
use crate::db::post_view::PostView; use crate::db::post_view::PostView;
use crate::db::user::User_; use crate::db::user::User_;
use crate::db::user_mention_view::UserMentionView;
use crate::db::{establish_connection, ListingType, SortType}; use crate::db::{establish_connection, ListingType, SortType};
use crate::Settings; use crate::Settings;
use actix_web::body::Body; use actix_web::body::Body;
use actix_web::{web, HttpResponse, Result}; use actix_web::{web, HttpResponse, Result};
use diesel::result::Error; use failure::Error;
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder}; use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
use serde::Deserialize; use serde::Deserialize;
use std::str::FromStr; use std::str::FromStr;
@ -22,9 +23,10 @@ pub struct Params {
} }
enum RequestType { enum RequestType {
All,
Community, Community,
User, User,
Front,
Inbox,
} }
pub fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> { pub fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
@ -33,27 +35,40 @@ pub fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
Err(_) => return HttpResponse::BadRequest().finish(), 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() Ok(rss) => HttpResponse::Ok()
.content_type("application/rss+xml") .content_type("application/rss+xml")
.body(rss), .body(rss),
Err(_) => HttpResponse::InternalServerError().finish(), Err(_) => HttpResponse::NotFound().finish(),
} }
} }
pub fn get_feed(path: web::Path<(char, String)>, info: web::Query<Params>) -> HttpResponse<Body> { pub fn get_feed(path: web::Path<(String, String)>, info: web::Query<Params>) -> HttpResponse<Body> {
let sort_type = match get_sort_type(info) { let sort_type = match get_sort_type(info) {
Ok(sort_type) => sort_type, Ok(sort_type) => sort_type,
Err(_) => return HttpResponse::BadRequest().finish(), Err(_) => return HttpResponse::BadRequest().finish(),
}; };
let request_type = match path.0 { let request_type = match path.0.as_ref() {
'u' => RequestType::User, "u" => RequestType::User,
'c' => RequestType::Community, "c" => RequestType::Community,
"front" => RequestType::Front,
"inbox" => RequestType::Inbox,
_ => return HttpResponse::NotFound().finish(), _ => 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() Ok(rss) => HttpResponse::Ok()
.content_type("application/rss+xml") .content_type("application/rss+xml")
.body(rss), .body(rss),
@ -66,70 +81,17 @@ fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
SortType::from_str(&sort_query) SortType::from_str(&sort_query)
} }
fn get_feed_internal( fn get_feed_all_data(sort_type: &SortType) -> Result<String, Error> {
sort_type: &SortType,
request_type: RequestType,
name: Option<String>,
) -> Result<String, Error> {
let conn = establish_connection(); let conn = establish_connection();
let mut community_id: Option<i32> = None;
let mut creator_id: Option<i32> = None;
let site_view = SiteView::read(&conn)?; 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( let posts = PostView::list(
&conn, &conn,
ListingType::All, ListingType::All,
sort_type, sort_type,
community_id, None,
creator_id, None,
None, None,
None, None,
None, None,
@ -140,12 +102,243 @@ fn get_feed_internal(
None, 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<String, Error> {
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<String, Error> {
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<String, Error> {
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<String, Error> {
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<ReplyView>,
mentions: Vec<UserMentionView>,
) -> Vec<Item> {
let mut items: Vec<Item> = 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/{} <a href=\"{}\">(link)</a>",
r.creator_name, author_url
));
let dt = DateTime::<Utc>::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/{} <a href=\"{}\">(link)</a>",
m.creator_name, author_url
));
let dt = DateTime::<Utc>::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<PostView>) -> Vec<Item> {
let mut items: Vec<Item> = Vec::new(); let mut items: Vec<Item> = Vec::new();
for p in posts { for p in posts {
let mut i = ItemBuilder::default(); 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); let author_url = format!("https://{}/u/{}", Settings::get().hostname, p.creator_name);
i.author(format!( i.author(format!(
@ -154,7 +347,7 @@ fn get_feed_internal(
)); ));
let dt = DateTime::<Utc>::from_utc(p.published, Utc); let dt = DateTime::<Utc>::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); let post_url = format!("https://{}/post/{}", Settings::get().hostname, p.id);
i.comments(post_url.to_owned()); i.comments(post_url.to_owned());
@ -203,10 +396,5 @@ fn get_feed_internal(
items.push(i.build().unwrap()); items.push(i.build().unwrap());
} }
channel_builder.items(items); items
let channel = channel_builder.build().unwrap();
channel.write_to(::std::io::sink()).unwrap();
Ok(channel.to_string())
} }

View file

@ -92,11 +92,23 @@ export class Inbox extends Component<any, InboxState> {
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h5 class="mb-0"> <h5 class="mb-0">
<span> <T
<T i18nKey="inbox_for" interpolation={{ user: user.username }}> class="d-inline"
#<Link to={`/u/${user.username}`}>#</Link> i18nKey="inbox_for"
</T> interpolation={{ user: user.username }}
</span> >
#<Link to={`/u/${user.username}`}>#</Link>
</T>
<small>
<a
href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
target="_blank"
>
<svg class="icon mx-2 text-muted small">
<use xlinkHref="#icon-rss">#</use>
</svg>
</a>
</small>
</h5> </h5>
{this.state.replies.length + this.state.mentions.length > 0 && {this.state.replies.length + this.state.mentions.length > 0 &&
this.state.unreadOrAll == UnreadOrAll.Unread && ( this.state.unreadOrAll == UnreadOrAll.Unread && (

View file

@ -444,6 +444,19 @@ export class Main extends Component<any, MainState> {
</svg> </svg>
</a> </a>
)} )}
{UserService.Instance.user &&
this.state.type_ == ListingType.Subscribed && (
<a
href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
SortType[this.state.sort]
}`}
target="_blank"
>
<svg class="icon mx-1 text-muted small">
<use xlinkHref="#icon-rss">#</use>
</svg>
</a>
)}
</div> </div>
); );
} }