pub mod context; use apb::{target::Addressed, Activity, ActivityMut, Actor, Base, Collection, Object, ObjectMut}; use context::CTX; use leptos::{leptos_dom::logging::console_log, *}; use leptos_router::*; pub const URL_BASE: &str = "https://feditest.alemi.dev"; pub const URL_PREFIX: &str = "/web"; #[derive(Debug, serde::Serialize)] struct LoginForm { email: String, password: String, } fn web_uri(kind: &str, url: &str) -> String { if url.starts_with(URL_BASE) { format!("/web/{kind}/{}", url.split('/').last().unwrap_or_default()) } else { format!("/web/{kind}/+{}", url.replace("https://", "").replace('/', "@")) } } fn api_uri(kind: &str, url: &str) -> String { if url.starts_with(URL_BASE) { url.to_string() } else { format!("{URL_BASE}/{kind}/+{}", url.replace("https://", "").replace('/', "@")) } } #[derive(Debug, serde::Deserialize)] struct AuthSuccess { token: String, user: String, expires: chrono::DateTime, } #[component] pub fn LoginBox( rx: Signal>, tx: WriteSignal>, ) -> impl IntoView { let (username, username_set) = create_signal("".to_string()); let username_ref: NodeRef = create_node_ref(); let password_ref: NodeRef = create_node_ref(); view! {
() .await.unwrap(); tx.set(Some(auth.token)); username_set.set(auth.user); console_log(&format!("logged in until {}", auth.expires)); }); } />
} } #[component] pub fn PostBox(token: Signal>) -> impl IntoView { let summary_ref: NodeRef = create_node_ref(); let content_ref: NodeRef = create_node_ref(); view! {
} } #[component] pub fn TimelinePicker( tx: WriteSignal, rx: ReadSignal, ) -> impl IntoView { let targets = ( "https://feditest.alemi.dev/users/test/inbox/page".to_string(), "https://feditest.alemi.dev/users/test/outbox/page".to_string(), "https://feditest.alemi.dev/inbox/page".to_string(), "https://feditest.alemi.dev/outbox/page".to_string(), ); let (my_in, my_out, our_in, our_out) = targets.clone(); let (my_in_, my_out_, our_in_, our_out_) = targets; view! { } } #[component] pub fn ActorBanner(object: serde_json::Value) -> impl IntoView { match object { serde_json::Value::String(id) => view! {
{id}
}, serde_json::Value::Object(_) => { let uid = object.id().unwrap_or_default().to_string(); let uri = web_uri("users", &uid); let avatar_url = object.icon().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default(); let display_name = object.name().unwrap_or_default().to_string(); let username = object.preferred_username().unwrap_or_default().to_string(); let domain = object.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string(); view! {
{display_name}
{username}@{domain}
} }, _ => view! {
invalid actor
} } } #[component] pub fn UserPage() -> impl IntoView { let params = use_params_map(); let actor = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), |id| { async move { let uri = web_uri("users", &id); match CTX.cache.actors.get(&uri) { Some(x) => Some(x.clone()), None => { let user = reqwest::get(&uri) .await .ok()? .json::() .await .ok()?; CTX.cache.actors.insert(uri, user.clone()); Some(user) }, } } }); view! { {move || match actor.get() { None => view! {

loading...

}.into_view(), Some(None) => view! {

error loading

}.into_view(), Some(Some(x)) => view! {

{x.summary().unwrap_or("").to_string()}

  • type" "{x.actor_type().unwrap_or(apb::ActorType::Person).as_ref().to_string()}
  • following" "{x.following().get().map(|x| x.total_items().unwrap_or(0))}
  • followers" "{x.followers().get().map(|x| x.total_items().unwrap_or(0))}
  • created" "{x.published().map(|x| x.to_rfc3339())}
}.into_view(), }} } } #[component] pub fn ObjectPage() -> impl IntoView { let params = use_params_map(); let object = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), |oid| { async move { let uid = format!("{URL_BASE}/objects/{oid}"); match CTX.cache.actors.get(&uid) { Some(x) => Some(x.clone()), None => reqwest::get(uid) .await .ok()? .json::() .await .ok() } } }); view! { {move || match object.get() { Some(Some(o)) => view!{ }.into_view(), Some(None) => view! {

loading failed

}.into_view(), None => view! {

loading ...

}.into_view(), }} } } #[component] pub fn Object(object: serde_json::Value) -> impl IntoView { let summary = object.summary().unwrap_or_default().to_string(); let content = object.content().unwrap_or_default().to_string(); let date = object.published().map(|x| x.to_rfc3339()).unwrap_or_default(); let author_id = object.attributed_to().id().unwrap_or_default(); let author = CTX.cache.actors.get(&author_id).map(|x| view! { }); view! { {author}
{summary}
{content}
{date}
} } #[component] pub fn InlineActivity(activity: serde_json::Value) -> impl IntoView { let object = activity.clone().object().extract().unwrap_or_else(|| serde_json::Value::String(activity.object().id().unwrap_or_default()) ); let object_id = object.id().unwrap_or_default().to_string(); let object_uri = web_uri("objects", &object_id); let content = dissolve::strip_html_tags(object.content().unwrap_or_default()); let addressed = activity.addressed(); let audience = format!("[ {} ]", addressed.join(", ")); let privacy = if addressed.iter().any(|x| x == apb::target::PUBLIC) { "[public]" } else if addressed.iter().any(|x| x.ends_with("/followers")) { "[followers]" } else { "[private]" }; let title = object.summary().unwrap_or_default().to_string(); let date = object.published().map(|x| x.to_rfc3339()).unwrap_or_else(|| activity.published().map(|x| x.to_rfc3339()).unwrap_or_default() ); let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity); view! { {match kind { // post apb::ActivityType::Create => view! {

{title}

{ content .into_iter() .map(|x| view! {

{x}

}.into_view()) .collect::>() }
}, kind => view! {
{kind.as_ref().to_string()}" >> "{object_id}
}, }} {privacy}" "{date} } } #[derive(Debug, thiserror::Error)] #[error("{0}")] struct OmgReqwestErrorIsNotClonable(String); #[component] pub fn Timeline( token: Signal>, ) -> impl IntoView { let (timeline, set_timeline) = create_signal(format!("{URL_BASE}/inbox/page")); let items = create_local_resource(move || timeline.get(), move |feed_url| async move { fetch_activities_with_users(&feed_url, token).await }); view! {
{format!("{:?}", err.get())}

} > {move || items.with(|x| match x { None => Ok(view! {

loading...

}.into_view()), Some(data) => match data { Err(e) => Err(OmgReqwestErrorIsNotClonable(e.to_string())), Ok(values) => Ok( values .iter() .map(|object| { let actor = object.actor().extract().unwrap_or_else(|| serde_json::Value::String(object.actor().id().unwrap_or_default()) ); view! {

} }) .collect::>() .into_view() ), } })}
} } async fn fetch_activities_with_users( feed_url: &str, token: Signal>, ) -> reqwest::Result> { let mut req = reqwest::Client::new().get(feed_url); if let Some(token) = token.get() { req = req.header("Authorization", format!("Bearer {token}")); } let activities : Vec = req .send() .await? .json::() .await? .ordered_items() .collect(); // i could make this fancier with iterators and futures::join_all but they would run // concurrently and make a ton of parallel request, we actually want these sequential because // first one may fetch same user as second one // some fancier logic may make a set of all actors and fetch uniques concurrently... let mut out = Vec::new(); for x in activities { if let Some(uid) = x.actor().id() { if let Some(actor) = CTX.cache.actors.get(&uid) { out.push(x.set_actor(apb::Node::object(actor.clone()))) } else { let mut req = reqwest::Client::new() .get(api_uri("users", &uid)); if let Some(token) = token.get() { req = req.header("Authorization", format!("Bearer {token}")); } // TODO don't fail whole timeline fetch when one user fails fetching... let actor = req.send().await?.json::().await?; CTX.cache.actors.insert(web_uri("users", &uid), actor.clone()); out.push(x.set_actor(apb::Node::object(actor))) } } else { out.push(x) } } Ok(out) }