]> Untitled Git - lemmy.git/commitdiff
Adding RSS feeds for inbox and subscribed. Refactored RSS code.
authorDessalines <tyhou13@gmx.com>
Sun, 8 Dec 2019 00:39:43 +0000 (16:39 -0800)
committerDessalines <tyhou13@gmx.com>
Sun, 8 Dec 2019 00:39:43 +0000 (16:39 -0800)
- Fixes #349

server/src/feeds.rs
ui/src/components/inbox.tsx
ui/src/components/main.tsx

index 8a1db28bc599b6e0e5febdfa9041a4b6f34c90bc..737207fae5e9c535827508562714c363cc5978db 100644 (file)
@@ -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<Params>) -> HttpResponse<Body> {
@@ -33,27 +35,40 @@ pub fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
     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<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) {
     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,89 @@ fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
   SortType::from_str(&sort_query)
 }
 
-fn get_feed_internal(
-  sort_type: &SortType,
-  request_type: RequestType,
-  name: Option<String>,
-) -> Result<String, Error> {
+fn get_feed_all_data(sort_type: &SortType) -> Result<String, Error> {
   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 posts = PostView::list(
+    &conn,
+    ListingType::All,
+    sort_type,
+    None,
+    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!("{} - 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);
 
-  // TODO do channel image, need to externalize
+  Ok(channel_builder.build().unwrap().to_string())
+}
 
-  match request_type {
-    RequestType::All => {
-      channel_builder
-        .title(htmlescape::encode_minimal(&site_view.name))
-        .link(format!("https://{}", Settings::get().hostname));
+fn get_feed_community(sort_type: &SortType, community_name: String) -> Result<String, Error> {
+  let conn = establish_connection();
 
-      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 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,
-    community_id,
-    creator_id,
+    Some(community.id),
+    None,
     None,
     None,
     None,
@@ -140,12 +174,171 @@ fn get_feed_internal(
     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();
 
   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::<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);
     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
 }
index bcde9363b2b55155a91ae497b0f1ed65c7462a97..39109a5d1213e902d682d64154bb45e6e7773863 100644 (file)
@@ -92,11 +92,23 @@ export class Inbox extends Component<any, InboxState> {
         <div class="row">
           <div class="col-12">
             <h5 class="mb-0">
-              <span>
-                <T i18nKey="inbox_for" interpolation={{ user: user.username }}>
-                  #<Link to={`/u/${user.username}`}>#</Link>
-                </T>
-              </span>
+              <T
+                class="d-inline"
+                i18nKey="inbox_for"
+                interpolation={{ user: user.username }}
+              >
+                #<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>
             {this.state.replies.length + this.state.mentions.length > 0 &&
               this.state.unreadOrAll == UnreadOrAll.Unread && (
index f4ec779fc2ba03666c0b5e528ef91dd05c3f6db2..0d6be91dc9319d6dad5e8bd1a71754db203c1c6c 100644 (file)
@@ -444,6 +444,19 @@ export class Main extends Component<any, MainState> {
             </svg>
           </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>
     );
   }