diff options
-rw-r--r-- | adenosine-pds/src/bsky.rs | 204 | ||||
-rw-r--r-- | adenosine-pds/src/lib.rs | 2 | ||||
-rw-r--r-- | adenosine-pds/src/models.rs | 4 | ||||
-rw-r--r-- | adenosine-pds/src/web.rs | 2 | ||||
-rw-r--r-- | adenosine-pds/templates/macro.html | 3 | ||||
-rw-r--r-- | adenosine-pds/templates/thread.html | 16 |
6 files changed, 217 insertions, 14 deletions
diff --git a/adenosine-pds/src/bsky.rs b/adenosine-pds/src/bsky.rs index a4d025b..3b0fbe3 100644 --- a/adenosine-pds/src/bsky.rs +++ b/adenosine-pds/src/bsky.rs @@ -4,8 +4,10 @@ use crate::repo::Mutation; /// records use crate::{ ipld_into_json_value, json_value_into_ipld, AtpDatabase, AtpService, Did, Result, Tid, + XrpcError, }; -use adenosine_cli::identifiers::{AtUri, Nsid}; +use adenosine_cli::identifiers::{AtUri, DidOrHost, Nsid}; +use anyhow::anyhow; use libipld::Cid; use rusqlite::params; use serde_json::json; @@ -244,19 +246,103 @@ pub fn bsky_get_author_feed(srv: &mut AtpService, did: &Did) -> Result<GenericFe Ok(GenericFeed { feed }) } +// TODO: this is a partial implementation +// TODO: should maybe have this take a did and tid instead of a aturi? pub fn bsky_get_thread( - _srv: &mut AtpService, - _uri: &AtUri, + srv: &mut AtpService, + uri: &AtUri, _depth: Option<u64>, ) -> Result<PostThread> { - // TODO: what is the best way to implement this? recurisvely? just first-level children to - // start? - unimplemented!() + // parse the URI + let did = match uri.repository { + DidOrHost::Did(ref did_type, ref did_body) => { + Did::from_str(&format!("did:{}:{}", did_type, did_body))? + } + _ => Err(anyhow!("expected a DID, not handle, in uri: {}", uri))?, + }; + if uri.collection != Some("app.bsky.feed.post".to_string()) { + Err(anyhow!("expected a post collection in uri: {}", uri))?; + }; + let tid = match uri.record { + Some(ref tid) => Tid::from_str(&tid)?, + _ => Err(anyhow!("expected a record in uri: {}", uri))?, + }; + + // post itself, as a FeedItem + let post_items = { + let mut stmt = srv.atp_db + .conn + .prepare_cached("SELECT account.did, account.handle, bsky_post.tid, bsky_post.cid, bsky_post.indexed_at FROM bsky_post LEFT JOIN account ON bsky_post.did = account.did WHERE bsky_post.did = ?1 AND bsky_post.tid = ?2")?; + let mut sql_rows = stmt.query(params!(did.to_string(), tid.to_string()))?; + let mut rows: Vec<FeedRow> = vec![]; + while let Some(sql_row) = sql_rows.next()? { + let row = feed_row(sql_row)?; + rows.push(row); + } + rows + }; + if post_items.is_empty() { + Err(XrpcError::NotFound("post not found".to_string()))?; + }; + let post_item = feed_row_to_item(srv, post_items.into_iter().nth(0).unwrap())?; + + // TODO: any parent + let parent = None; + + // any children + let mut children = vec![]; + let rows = { + let mut stmt = srv.atp_db + .conn + .prepare_cached("SELECT account.did, account.handle, bsky_post.tid, bsky_post.cid, bsky_post.indexed_at FROM bsky_post LEFT JOIN account ON bsky_post.did = account.did WHERE bsky_post.reply_to_parent_uri = ?1 ORDER BY bsky_post.tid DESC LIMIT 20")?; + let mut sql_rows = stmt.query(params!(uri.to_string()))?; + let mut rows: Vec<FeedRow> = vec![]; + while let Some(sql_row) = sql_rows.next()? { + let row = feed_row(sql_row)?; + rows.push(row); + } + rows + }; + for row in rows { + let item = feed_row_to_item(srv, row)?; + children.push(ThreadItem { + uri: item.uri, + cid: item.cid, + author: item.author, + record: item.record, + embed: item.embed, + // don't want a loop here + parent: None, + replyCount: item.replyCount, + // only going to depth of one here + replies: None, + likeCount: item.likeCount, + repostCount: item.repostCount, + indexedAt: item.indexedAt, + myState: None, + }); + } + + let post = ThreadItem { + uri: post_item.uri, + cid: post_item.cid, + author: post_item.author, + record: post_item.record, + embed: post_item.embed, + parent: parent, + replyCount: post_item.replyCount, + replies: Some(children), + likeCount: post_item.likeCount, + repostCount: post_item.repostCount, + indexedAt: post_item.indexedAt, + myState: None, + }; + Ok(PostThread { thread: post }) } #[test] fn test_bsky_profile() { - use crate::{create_account, created_at_now}; + use crate::create_account; use libipld::ipld; let post_nsid = Nsid::from_str("app.bsky.feed.post").unwrap(); @@ -339,6 +425,7 @@ fn test_bsky_profile() { #[test] fn test_bsky_feeds() { + // TODO: test that displayName comes through in feeds and timelines (it does not currently) use crate::{create_account, created_at_now}; use libipld::ipld; @@ -564,4 +651,105 @@ fn test_bsky_feeds() { assert!(carol_feed.feed.is_empty()); } -// TODO: post threads (include in the above test?) +#[test] +fn test_bsky_thread() { + use crate::{create_account, created_at_now}; + use libipld::ipld; + + let post_nsid = Nsid::from_str("app.bsky.feed.post").unwrap(); + + let mut srv = AtpService::new_ephemeral().unwrap(); + let alice_did = { + let req = AccountRequest { + email: "alice@bogus.com".to_string(), + handle: "alice.test".to_string(), + password: "bogus".to_string(), + inviteCode: None, + recoveryKey: None, + }; + let session = create_account(&mut srv, &req, true).unwrap(); + Did::from_str(&session.did).unwrap() + }; + let bob_did = { + let req = AccountRequest { + email: "bob@bogus.com".to_string(), + handle: "bob.test".to_string(), + password: "bogus".to_string(), + inviteCode: None, + recoveryKey: None, + }; + let session = create_account(&mut srv, &req, true).unwrap(); + Did::from_str(&session.did).unwrap() + }; + + // alice does a post + let alice_post1_tid = srv.tid_gen.next_tid(); + let mutations = vec![Mutation::Create( + post_nsid.clone(), + alice_post1_tid.clone(), + ipld!({"text": "alice first post"}), + )]; + srv.repo + .mutate_repo(&alice_did, &mutations, &srv.pds_keypair) + .unwrap(); + bsky_mutate_db(&mut srv.atp_db, &alice_did, mutations).unwrap(); + let alice_post1_uri = format!( + "at://{}/{}/{}", + alice_did.to_string(), + post_nsid.to_string(), + alice_post1_tid.to_string() + ); + + // bob likes and replies first post + let bob_post1_tid = srv.tid_gen.next_tid(); + let mutations = vec![Mutation::Create( + post_nsid.clone(), + bob_post1_tid.clone(), + ipld!({"text": "bob comment on alice post1", "reply": {"parent": {"uri": alice_post1_uri.clone()}, "root": {"uri": alice_post1_uri.clone()}}}), + )]; + srv.repo + .mutate_repo(&bob_did, &mutations, &srv.pds_keypair) + .unwrap(); + bsky_mutate_db(&mut srv.atp_db, &bob_did, mutations).unwrap(); + let bob_post1_uri = format!( + "at://{}/{}/{}", + bob_did.to_string(), + post_nsid.to_string(), + bob_post1_tid.to_string() + ); + + // alice replies to bob reply + let alice_post2_tid = srv.tid_gen.next_tid(); + let mutations = vec![Mutation::Create( + post_nsid.clone(), + alice_post2_tid.clone(), + ipld!({"text": "alice second post, replying to bob comment", "reply": {"parent": {"uri": bob_post1_uri.clone()}, "root": {"uri": alice_post1_uri.clone()}}}), + )]; + srv.repo + .mutate_repo(&alice_did, &mutations, &srv.pds_keypair) + .unwrap(); + bsky_mutate_db(&mut srv.atp_db, &alice_did, mutations).unwrap(); + let _alice_post2_uri = format!( + "at://{}/{}/{}", + alice_did.to_string(), + post_nsid.to_string(), + alice_post2_tid.to_string() + ); + + // get thread from bob's post + // TODO: should have both parent and children + let post = bsky_get_thread(&mut srv, &AtUri::from_str(&bob_post1_uri).unwrap(), None) + .unwrap() + .thread; + assert_eq!(post.author.did, bob_did.to_string()); + assert_eq!(post.author.handle, "bob.test".to_string()); + assert_eq!(post.embed, None); + assert_eq!(post.replyCount, 1); + assert_eq!(post.repostCount, 0); + assert_eq!(post.likeCount, 0); + assert_eq!(post.replies.as_ref().unwrap().len(), 1); + + let post_replies = post.replies.unwrap(); + assert_eq!(post_replies[0].author.did, alice_did.to_string()); + // TODO: root URI, etc +} diff --git a/adenosine-pds/src/lib.rs b/adenosine-pds/src/lib.rs index c70cb88..cc233b1 100644 --- a/adenosine-pds/src/lib.rs +++ b/adenosine-pds/src/lib.rs @@ -720,7 +720,7 @@ fn thread_view_handler( did, collection, tid: tid.clone(), - thread: bsky_get_thread(&mut srv, &uri, None)?.thread, + post: bsky_get_thread(&mut srv, &uri, None)?.thread, } .render()?) } diff --git a/adenosine-pds/src/models.rs b/adenosine-pds/src/models.rs index 3af780e..6dbe725 100644 --- a/adenosine-pds/src/models.rs +++ b/adenosine-pds/src/models.rs @@ -155,7 +155,7 @@ pub struct PostReply { #[allow(non_snake_case)] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] pub struct PostThread { - pub thread: Vec<ThreadItem>, + pub thread: ThreadItem, } #[allow(non_snake_case)] @@ -168,7 +168,7 @@ pub struct ThreadItem { pub record: Value, //pub embed?: RecordEmbed | ExternalEmbed | UnknownEmbed, pub embed: Option<Value>, - pub parent: Box<ThreadItem>, + pub parent: Option<Box<ThreadItem>>, pub replyCount: u64, pub replies: Option<Vec<ThreadItem>>, pub likeCount: u64, diff --git a/adenosine-pds/src/web.rs b/adenosine-pds/src/web.rs index 4432566..98af87e 100644 --- a/adenosine-pds/src/web.rs +++ b/adenosine-pds/src/web.rs @@ -40,7 +40,7 @@ pub struct ThreadView { pub did: Did, pub collection: Nsid, pub tid: Tid, - pub thread: Vec<ThreadItem>, + pub post: ThreadItem, } #[derive(Template)] diff --git a/adenosine-pds/templates/macro.html b/adenosine-pds/templates/macro.html index 1d38482..96fdfe3 100644 --- a/adenosine-pds/templates/macro.html +++ b/adenosine-pds/templates/macro.html @@ -2,10 +2,12 @@ {% macro feed_item(item) %} <div class="feed_item"> +{# TODO: handle both thread items and feed items {% if item.repostedBy.is_some() %} {% if item.author.displayName.is_some() %}{{ item.author.displayName.as_ref().unwrap() }}{% endif %} <b>@{{ item.author.handle }}</b> {% endif %} +#} <div style="float: right;"> <a class="item_timestamp" href="/u/{{ item.author.handle }}/post/{{ item.uri|aturi_to_tid }}"> @@ -27,6 +29,7 @@ <br> <span class="counts"> [<a href="#">{{ item.likeCount }} like</a> / <a href="#">{{ item.repostCount }} repost</a> / <a href="#">{{ item.replyCount }} reply</a>] + [<a href="{{ item.uri|aturi_to_path }}">inspect</a>] </span> {% if item.record.get("reply").is_some() %} diff --git a/adenosine-pds/templates/thread.html b/adenosine-pds/templates/thread.html index e2e2e96..608c971 100644 --- a/adenosine-pds/templates/thread.html +++ b/adenosine-pds/templates/thread.html @@ -1,8 +1,20 @@ {% extends "base.html" %} {% import "macro.html" as macro %} -{% block post %} +{% block main %} -Post stuff will go here +{% if post.parent.is_some() %} + {% call macro::feed_item(post.parent.as_ref().unwrap()) %} + <center><i>---</i></center> +{% endif %} + +{% call macro::feed_item(post) %} + +{% if post.replies.is_some() && post.replies.as_ref().unwrap().len() > 0 %} + <center><i>--- replies ---</i></center> + {% for item in post.replies.as_ref().unwrap() %} + {% call macro::feed_item(item) %} + {% endfor %} +{% endif %} {% endblock %} |