~vpzom/hitide

34126a8421eab043202b693fb55df13d3334a8f7 — Colin Reeder 10 months ago 1c9e1a9
Split off more modules
A src/components/mod.rs => src/components/mod.rs +238 -0
@@ 0,0 1,238 @@
use std::borrow::Cow;

use crate::resp_types::{
    RespMinimalAuthorInfo, RespMinimalCommunityInfo, RespPostCommentInfo, RespPostListPost,
};
use crate::util::{abbreviate_link, author_is_me};
use crate::PageBaseData;

#[render::component]
pub fn Comment<'comment, 'base_data>(
    comment: &'comment RespPostCommentInfo<'comment>,
    base_data: &'base_data PageBaseData,
) {
    render::rsx! {
        <li>
            <small><cite><UserLink user={comment.author.as_ref()} /></cite>{":"}</small>
            <Content src={comment} />
            <div class={"actionList"}>
                {
                    if base_data.login.is_some() {
                        Some(render::rsx! {
                            <>
                                <form method={"POST"} action={format!("/comments/{}/like", comment.id)} style={"display: inline"}>
                                    <button r#type={"submit"}>{"Like"}</button>
                                </form>
                                <a href={format!("/comments/{}", comment.id)}>{"reply"}</a>
                            </>
                        })
                    } else {
                        None
                    }
                }
                {
                    if author_is_me(&comment.author, &base_data.login) {
                        Some(render::rsx! {
                            <a href={format!("/comments/{}/delete", comment.id)}>{"delete"}</a>
                        })
                    } else {
                        None
                    }
                }
            </div>

            {
                match &comment.replies {
                    Some(replies) => {
                        Some(render::rsx! {
                            <ul>
                                {
                                    replies.iter().map(|reply| {
                                        render::rsx! {
                                            <Comment comment={reply} base_data />
                                        }
                                    })
                                    .collect::<Vec<_>>()
                                }
                            </ul>
                        })
                    },
                    None => None,
                }
            }
        </li>
    }
}

pub struct CommunityLink<'community> {
    pub community: &'community RespMinimalCommunityInfo<'community>,
}
impl<'community> render::Render for CommunityLink<'community> {
    fn render_into<W: std::fmt::Write>(self, writer: &mut W) -> std::fmt::Result {
        let community = &self.community;

        let href = format!("/communities/{}", community.id);
        (render::rsx! {
            <a href={&href}>
            {
                (if community.local {
                    community.name.as_ref().into()
                } else {
                    Cow::Owned(format!("{}@{}", community.name, community.host))
                }).as_ref()
            }
            </a>
        })
        .render_into(writer)
    }
}

pub trait HavingContent {
    fn content_text(&self) -> Option<&str>;
    fn content_html(&self) -> Option<&str>;
}

impl<'a> HavingContent for RespPostCommentInfo<'a> {
    fn content_text(&self) -> Option<&str> {
        self.content_text.as_deref()
    }
    fn content_html(&self) -> Option<&str> {
        self.content_html.as_deref()
    }
}

impl<'a> HavingContent for RespPostListPost<'a> {
    fn content_text(&self) -> Option<&str> {
        self.content_text.as_deref()
    }
    fn content_html(&self) -> Option<&str> {
        self.content_html.as_deref()
    }
}

pub struct Content<'a, T: HavingContent + 'a> {
    pub src: &'a T,
}

impl<'a, T: HavingContent + 'a> render::Render for Content<'a, T> {
    fn render_into<W: std::fmt::Write>(self, writer: &mut W) -> std::fmt::Result {
        match self.src.content_html() {
            Some(html) => {
                let cleaned = ammonia::clean(&html);
                writer.write_str("<p>")?;
                render::raw!(cleaned.as_ref()).render_into(writer)?;
                writer.write_str("</p>")?;
            }
            None => match self.src.content_text() {
                Some(text) => {
                    writer.write_str("<p>")?;
                    text.render_into(writer)?;
                    writer.write_str("</p>")?;
                }
                None => {}
            },
        }

        Ok(())
    }
}

#[render::component]
pub fn HTPage<'base_data, Children: render::Render>(
    base_data: &'base_data PageBaseData,
    children: Children,
) {
    render::rsx! {
        <>
            <render::html::HTML5Doctype />
            <html>
                <head>
                    <meta charset={"utf-8"} />
                    <link rel={"stylesheet"} href={"/static/main.css"} />
                </head>
                <body>
                    <header class={"mainHeader"}>
                        <div class={"left actionList"}>
                            <a href={"/"} class={"siteName"}>{"lotide"}</a>
                            <a href={"/communities"}>{"Communities"}</a>
                        </div>
                        <div class={"right actionList"}>
                            {
                                match base_data.login {
                                    Some(_) => None,
                                    None => {
                                        Some(render::rsx! {
                                            <a href={"/login"}>{"Login"}</a>
                                        })
                                    }
                                }
                            }
                        </div>
                    </header>
                    {children}
                </body>
            </html>
        </>
    }
}

#[render::component]
pub fn PostItem<'post>(post: &'post RespPostListPost<'post>, in_community: bool) {
    render::rsx! {
        <li>
            <a href={format!("/posts/{}", post.id)}>
                {post.title.as_ref()}
            </a>
            {
                if let Some(href) = &post.href {
                    Some(render::rsx! {
                        <>
                            {" "}
                            <em><a href={href.as_ref()}>{abbreviate_link(&href)}{" ↗"}</a></em>
                        </>
                    })
                } else {
                    None
                }
            }
            <br />
            {"Submitted by "}<UserLink user={post.author.as_ref()} />
            {
                if !in_community {
                    Some(render::rsx! {
                        <>{" to "}<CommunityLink community={&post.community} /></>
                    })
                } else {
                    None
                }
            }
        </li>
    }
}

pub struct UserLink<'user> {
    pub user: Option<&'user RespMinimalAuthorInfo<'user>>,
}

impl<'user> render::Render for UserLink<'user> {
    fn render_into<W: std::fmt::Write>(self, writer: &mut W) -> std::fmt::Result {
        match self.user {
            None => "[unknown]".render_into(writer),
            Some(user) => {
                let href = format!("/users/{}", user.id);
                (render::rsx! {
                    <a href={&href}>
                        {
                            (if user.local {
                                user.username.as_ref().into()
                            } else {
                                Cow::Owned(format!("{}@{}", user.username, user.host))
                            }).as_ref()
                        }
                    </a>
                })
                .render_into(writer)
            }
        }
    }
}

M src/main.rs => src/main.rs +9 -0
@@ 1,10 1,14 @@
#![feature(proc_macro_hygiene)]
#![allow(unused_braces)]

use crate::resp_types::RespLoginInfo;
use std::sync::Arc;
use trout::hyper::RoutingFailureExtHyper;

mod components;
mod resp_types;
mod routes;
mod util;

pub type HttpClient = hyper::Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>;



@@ 36,6 40,11 @@ impl<T: 'static + std::error::Error + Send> From<T> for Error {
    }
}

#[derive(Debug)]
pub struct PageBaseData {
    pub login: Option<RespLoginInfo>,
}

pub fn simple_response(
    code: hyper::StatusCode,
    text: impl Into<hyper::Body>,

A src/resp_types.rs => src/resp_types.rs +69 -0
@@ 0,0 1,69 @@
use serde_derive::Deserialize;
use std::borrow::Cow;

#[derive(Deserialize, Debug)]
pub struct RespMinimalAuthorInfo<'a> {
    pub id: i64,
    pub username: Cow<'a, str>,
    pub local: bool,
    pub host: Cow<'a, str>,
}

#[derive(Deserialize, Debug)]
pub struct RespPostListPost<'a> {
    pub id: i64,
    pub title: Cow<'a, str>,
    pub href: Option<Cow<'a, str>>,
    pub content_text: Option<Cow<'a, str>>,
    pub content_html: Option<Cow<'a, str>>,
    #[serde(borrow)]
    pub author: Option<RespMinimalAuthorInfo<'a>>,
    pub created: Cow<'a, str>,
    #[serde(borrow)]
    pub community: RespMinimalCommunityInfo<'a>,
}

#[derive(Deserialize, Debug)]
pub struct RespPostCommentInfo<'a> {
    pub id: i64,
    #[serde(borrow)]
    pub author: Option<RespMinimalAuthorInfo<'a>>,
    pub created: Cow<'a, str>,
    pub content_text: Option<Cow<'a, str>>,
    pub content_html: Option<Cow<'a, str>>,
    #[serde(borrow)]
    pub replies: Option<Vec<RespPostCommentInfo<'a>>>,
}

#[derive(Deserialize, Debug)]
pub struct RespPostInfo<'a> {
    #[serde(flatten, borrow)]
    pub base: RespPostListPost<'a>,
    pub score: i64,
    #[serde(borrow)]
    pub comments: Vec<RespPostCommentInfo<'a>>,
}

impl<'a> AsRef<RespPostListPost<'a>> for RespPostInfo<'a> {
    fn as_ref(&self) -> &RespPostListPost<'a> {
        &self.base
    }
}

#[derive(Deserialize, Debug)]
pub struct RespMinimalCommunityInfo<'a> {
    pub id: i64,
    pub name: Cow<'a, str>,
    pub local: bool,
    pub host: Cow<'a, str>,
}

#[derive(Deserialize, Debug)]
pub struct RespLoginInfoUser {
    pub id: i64,
}

#[derive(Deserialize, Debug)]
pub struct RespLoginInfo {
    pub user: RespLoginInfoUser,
}

M src/routes/communities.rs => src/routes/communities.rs +3 -2
@@ 1,6 1,7 @@
use crate::components::{CommunityLink, HTPage, PostItem};
use crate::resp_types::{RespMinimalCommunityInfo, RespPostListPost};
use crate::routes::{
    fetch_base_data, get_cookie_map, get_cookie_map_for_req, html_response, res_to_error,
    with_auth, CommunityLink, HTPage, PostItem, RespMinimalCommunityInfo, RespPostListPost,
    fetch_base_data, get_cookie_map, get_cookie_map_for_req, html_response, res_to_error, with_auth,
};
use std::collections::HashMap;
use std::sync::Arc;

M src/routes/mod.rs => src/routes/mod.rs +4 -326
@@ 2,6 2,10 @@ use serde_derive::Deserialize;
use std::borrow::Cow;
use std::sync::Arc;

use crate::components::{Content, HTPage, PostItem, UserLink};
use crate::resp_types::{RespPostCommentInfo, RespPostListPost};
use crate::PageBaseData;

mod communities;
mod posts;
mod r#static;


@@ 54,32 58,6 @@ fn with_auth(
    Ok(new_req)
}

fn author_is_me(author: &Option<RespMinimalAuthorInfo<'_>>, login: &Option<RespLoginInfo>) -> bool {
    if let Some(author) = author {
        if let Some(login) = login {
            if author.id == login.user.id {
                return true;
            }
        }
    }
    false
}

#[derive(Deserialize, Debug)]
struct RespLoginInfoUser {
    id: i64,
}

#[derive(Deserialize, Debug)]
struct RespLoginInfo {
    user: RespLoginInfoUser,
}

#[derive(Debug)]
struct PageBaseData {
    login: Option<RespLoginInfo>,
}

async fn fetch_base_data(
    backend_host: &str,
    http_client: &crate::HttpClient,


@@ 106,306 84,6 @@ async fn fetch_base_data(
    Ok(PageBaseData { login })
}

#[derive(Deserialize, Debug)]
struct RespMinimalAuthorInfo<'a> {
    id: i64,
    username: Cow<'a, str>,
    local: bool,
    host: Cow<'a, str>,
}

trait HavingContent {
    fn content_text(&self) -> Option<&str>;
    fn content_html(&self) -> Option<&str>;
}

#[derive(Deserialize, Debug)]
struct RespPostListPost<'a> {
    id: i64,
    title: Cow<'a, str>,
    href: Option<Cow<'a, str>>,
    content_text: Option<Cow<'a, str>>,
    content_html: Option<Cow<'a, str>>,
    #[serde(borrow)]
    author: Option<RespMinimalAuthorInfo<'a>>,
    created: Cow<'a, str>,
    #[serde(borrow)]
    community: RespMinimalCommunityInfo<'a>,
}

impl<'a> HavingContent for RespPostListPost<'a> {
    fn content_text(&self) -> Option<&str> {
        self.content_text.as_deref()
    }
    fn content_html(&self) -> Option<&str> {
        self.content_html.as_deref()
    }
}

#[derive(Deserialize, Debug)]
struct RespPostCommentInfo<'a> {
    id: i64,
    #[serde(borrow)]
    author: Option<RespMinimalAuthorInfo<'a>>,
    created: Cow<'a, str>,
    content_text: Option<Cow<'a, str>>,
    content_html: Option<Cow<'a, str>>,
    #[serde(borrow)]
    replies: Option<Vec<RespPostCommentInfo<'a>>>,
}

impl<'a> HavingContent for RespPostCommentInfo<'a> {
    fn content_text(&self) -> Option<&str> {
        self.content_text.as_deref()
    }
    fn content_html(&self) -> Option<&str> {
        self.content_html.as_deref()
    }
}

#[derive(Deserialize, Debug)]
struct RespPostInfo<'a> {
    #[serde(flatten, borrow)]
    pub base: RespPostListPost<'a>,
    pub score: i64,
    #[serde(borrow)]
    pub comments: Vec<RespPostCommentInfo<'a>>,
}

impl<'a> AsRef<RespPostListPost<'a>> for RespPostInfo<'a> {
    fn as_ref(&self) -> &RespPostListPost<'a> {
        &self.base
    }
}

#[render::component]
fn HTPage<'base_data, Children: render::Render>(
    base_data: &'base_data PageBaseData,
    children: Children,
) {
    render::rsx! {
        <>
            <render::html::HTML5Doctype />
            <html>
                <head>
                    <meta charset={"utf-8"} />
                    <link rel={"stylesheet"} href={"/static/main.css"} />
                </head>
                <body>
                    <header class={"mainHeader"}>
                        <div class={"left actionList"}>
                            <a href={"/"} class={"siteName"}>{"lotide"}</a>
                            <a href={"/communities"}>{"Communities"}</a>
                        </div>
                        <div class={"right actionList"}>
                            {
                                match base_data.login {
                                    Some(_) => None,
                                    None => {
                                        Some(render::rsx! {
                                            <a href={"/login"}>{"Login"}</a>
                                        })
                                    }
                                }
                            }
                        </div>
                    </header>
                    {children}
                </body>
            </html>
        </>
    }
}

fn abbreviate_link(href: &str) -> &str {
    // Attempt to find the hostname from the URL
    match href.find("://") {
        Some(idx1) => match href[(idx1 + 3)..].find('/') {
            Some(idx2) => Some(&href[(idx1 + 3)..(idx1 + 3 + idx2)]),
            None => None,
        },
        None => None,
    }
    .unwrap_or(href)
}

#[render::component]
fn PostItem<'post>(post: &'post RespPostListPost<'post>, in_community: bool) {
    render::rsx! {
        <li>
            <a href={format!("/posts/{}", post.id)}>
                {post.title.as_ref()}
            </a>
            {
                if let Some(href) = &post.href {
                    Some(render::rsx! {
                        <>
                            {" "}
                            <em><a href={href.as_ref()}>{abbreviate_link(&href)}{" ↗"}</a></em>
                        </>
                    })
                } else {
                    None
                }
            }
            <br />
            {"Submitted by "}<UserLink user={post.author.as_ref()} />
            {
                if !in_community {
                    Some(render::rsx! {
                        <>{" to "}<CommunityLink community={&post.community} /></>
                    })
                } else {
                    None
                }
            }
        </li>
    }
}

#[derive(Deserialize, Debug)]
struct RespMinimalCommunityInfo<'a> {
    id: i64,
    name: Cow<'a, str>,
    local: bool,
    host: Cow<'a, str>,
}

struct UserLink<'user> {
    user: Option<&'user RespMinimalAuthorInfo<'user>>,
}

impl<'user> render::Render for UserLink<'user> {
    fn render_into<W: std::fmt::Write>(self, writer: &mut W) -> std::fmt::Result {
        match self.user {
            None => "[unknown]".render_into(writer),
            Some(user) => {
                let href = format!("/users/{}", user.id);
                (render::rsx! {
                    <a href={&href}>
                        {
                            (if user.local {
                                user.username.as_ref().into()
                            } else {
                                Cow::Owned(format!("{}@{}", user.username, user.host))
                            }).as_ref()
                        }
                    </a>
                })
                .render_into(writer)
            }
        }
    }
}

struct CommunityLink<'community> {
    community: &'community RespMinimalCommunityInfo<'community>,
}
impl<'community> render::Render for CommunityLink<'community> {
    fn render_into<W: std::fmt::Write>(self, writer: &mut W) -> std::fmt::Result {
        let community = &self.community;

        let href = format!("/communities/{}", community.id);
        (render::rsx! {
            <a href={&href}>
            {
                (if community.local {
                    community.name.as_ref().into()
                } else {
                    Cow::Owned(format!("{}@{}", community.name, community.host))
                }).as_ref()
            }
            </a>
        })
        .render_into(writer)
    }
}

struct Content<'a, T: HavingContent + 'a> {
    src: &'a T,
}

impl<'a, T: HavingContent + 'a> render::Render for Content<'a, T> {
    fn render_into<W: std::fmt::Write>(self, writer: &mut W) -> std::fmt::Result {
        match self.src.content_html() {
            Some(html) => {
                let cleaned = ammonia::clean(&html);
                writer.write_str("<p>")?;
                render::raw!(cleaned.as_ref()).render_into(writer)?;
                writer.write_str("</p>")?;
            }
            None => match self.src.content_text() {
                Some(text) => {
                    writer.write_str("<p>")?;
                    text.render_into(writer)?;
                    writer.write_str("</p>")?;
                }
                None => {}
            },
        }

        Ok(())
    }
}

#[render::component]
fn Comment<'comment, 'base_data>(
    comment: &'comment RespPostCommentInfo<'comment>,
    base_data: &'base_data PageBaseData,
) {
    render::rsx! {
        <li>
            <small><cite><UserLink user={comment.author.as_ref()} /></cite>{":"}</small>
            <Content src={comment} />
            <div class={"actionList"}>
                {
                    if base_data.login.is_some() {
                        Some(render::rsx! {
                            <>
                                <form method={"POST"} action={format!("/comments/{}/like", comment.id)} style={"display: inline"}>
                                    <button r#type={"submit"}>{"Like"}</button>
                                </form>
                                <a href={format!("/comments/{}", comment.id)}>{"reply"}</a>
                            </>
                        })
                    } else {
                        None
                    }
                }
                {
                    if author_is_me(&comment.author, &base_data.login) {
                        Some(render::rsx! {
                            <a href={format!("/comments/{}/delete", comment.id)}>{"delete"}</a>
                        })
                    } else {
                        None
                    }
                }
            </div>

            {
                match &comment.replies {
                    Some(replies) => {
                        Some(render::rsx! {
                            <ul>
                                {
                                    replies.iter().map(|reply| {
                                        render::rsx! {
                                            <Comment comment={reply} base_data />
                                        }
                                    })
                                    .collect::<Vec<_>>()
                                }
                            </ul>
                        })
                    },
                    None => None,
                }
            }
        </li>
    }
}

fn html_response(html: String) -> hyper::Response<hyper::Body> {
    let mut res = hyper::Response::new(html.into());
    res.headers_mut().insert(

M src/routes/posts.rs => src/routes/posts.rs +5 -3
@@ 1,8 1,10 @@
use super::{
    author_is_me, fetch_base_data, get_cookie_map, get_cookie_map_for_req, get_cookies_string,
    html_response, res_to_error, with_auth, Comment, CommunityLink, Content, HTPage, RespPostInfo,
    UserLink,
    fetch_base_data, get_cookie_map, get_cookie_map_for_req, get_cookies_string, html_response,
    res_to_error, with_auth,
};
use crate::components::{Comment, CommunityLink, Content, HTPage, UserLink};
use crate::resp_types::RespPostInfo;
use crate::util::author_is_me;
use std::sync::Arc;

async fn page_post(

A src/util.rs => src/util.rs +27 -0
@@ 0,0 1,27 @@
use crate::resp_types::{RespLoginInfo, RespMinimalAuthorInfo};

pub fn abbreviate_link(href: &str) -> &str {
    // Attempt to find the hostname from the URL
    match href.find("://") {
        Some(idx1) => match href[(idx1 + 3)..].find('/') {
            Some(idx2) => Some(&href[(idx1 + 3)..(idx1 + 3 + idx2)]),
            None => None,
        },
        None => None,
    }
    .unwrap_or(href)
}

pub fn author_is_me(
    author: &Option<RespMinimalAuthorInfo<'_>>,
    login: &Option<RespLoginInfo>,
) -> bool {
    if let Some(author) = author {
        if let Some(login) = login {
            if author.id == login.user.id {
                return true;
            }
        }
    }
    false
}