diff options
author | Bryan Newbold <bnewbold@robocracy.org> | 2022-11-10 18:38:05 -0800 |
---|---|---|
committer | Bryan Newbold <bnewbold@robocracy.org> | 2022-11-10 18:38:05 -0800 |
commit | 64c9d94476e29724243c6a6488ba4ce41ce24114 (patch) | |
tree | e60929fd529cf81fd32318a98acfe795025878c1 /adenosine-pds/src/bsky.rs | |
parent | db603c1fa18b19ed220e3ce18383ce1d15117bcc (diff) | |
download | adenosine-64c9d94476e29724243c6a6488ba4ce41ce24114.tar.gz adenosine-64c9d94476e29724243c6a6488ba4ce41ce24114.zip |
pds: basic/partial post thread implementation
Diffstat (limited to 'adenosine-pds/src/bsky.rs')
-rw-r--r-- | adenosine-pds/src/bsky.rs | 204 |
1 files changed, 196 insertions, 8 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 +} |