summaryrefslogtreecommitdiffstats
path: root/adenosine-pds/src/bsky.rs
diff options
context:
space:
mode:
Diffstat (limited to 'adenosine-pds/src/bsky.rs')
-rw-r--r--adenosine-pds/src/bsky.rs773
1 files changed, 0 insertions, 773 deletions
diff --git a/adenosine-pds/src/bsky.rs b/adenosine-pds/src/bsky.rs
deleted file mode 100644
index caa16f6..0000000
--- a/adenosine-pds/src/bsky.rs
+++ /dev/null
@@ -1,773 +0,0 @@
-use crate::models::*;
-/// Helper functions for doing database and repo operations relating to bluesky endpoints and
-/// records
-use crate::{
- ipld_into_json_value, json_value_into_ipld, AtpDatabase, AtpService, Did, Result, Tid,
- XrpcError,
-};
-use adenosine::identifiers::{AtUri, DidOrHost, Nsid};
-use adenosine::repo::Mutation;
-use anyhow::anyhow;
-use libipld::Cid;
-use rusqlite::params;
-use serde_json::json;
-use std::str::FromStr;
-
-/// Handles updating the database with creation, update, deletion of arbitrary records
-pub fn bsky_mutate_db(db: &mut AtpDatabase, did: &Did, mutations: Vec<Mutation>) -> Result<()> {
- // TODO: this function could probably be refactored
- let bsky_post: Nsid = Nsid::from_str("app.bsky.feed.post").unwrap();
- let bsky_repost: Nsid = Nsid::from_str("app.bsky.feed.repost").unwrap();
- let bsky_like: Nsid = Nsid::from_str("app.bsky.feed.like").unwrap();
- let bsky_follow: Nsid = Nsid::from_str("app.bsky.graph.follow").unwrap();
- for m in mutations.into_iter() {
- match m {
- Mutation::Create(ref_type, tid, val) | Mutation::Update(ref_type, tid, val)
- if ref_type == bsky_post =>
- {
- db.bsky_upsert_post(did, &tid, Some(val))?
- }
- Mutation::Delete(ref_type, tid) if ref_type == bsky_post => {
- db.bsky_upsert_post(did, &tid, None)?
- }
- Mutation::Create(ref_type, tid, val) | Mutation::Update(ref_type, tid, val)
- if ref_type == bsky_repost =>
- {
- db.bsky_upsert_ref("repost", did, &tid, Some(val))?
- }
- Mutation::Delete(ref_type, tid) if ref_type == bsky_repost => {
- db.bsky_upsert_ref("repost", did, &tid, None)?
- }
- Mutation::Create(ref_type, tid, val) | Mutation::Update(ref_type, tid, val)
- if ref_type == bsky_like =>
- {
- db.bsky_upsert_ref("like", did, &tid, Some(val))?
- }
- Mutation::Delete(ref_type, tid) if ref_type == bsky_like => {
- db.bsky_upsert_ref("like", did, &tid, None)?
- }
- Mutation::Create(ref_type, tid, val) | Mutation::Update(ref_type, tid, val)
- if ref_type == bsky_follow =>
- {
- db.bsky_upsert_follow(did, &tid, Some(val))?
- }
- Mutation::Delete(ref_type, tid) if ref_type == bsky_follow => {
- db.bsky_upsert_follow(did, &tid, None)?
- }
- _ => (),
- }
- }
- Ok(())
-}
-
-// TODO: should probably return Result<Option<Profile>>?
-pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<Profile> {
- // first get the profile record
- let mut profile_cid: Option<Cid> = None;
- let commit_cid = match srv.repo.lookup_commit(did)? {
- Some(cid) => cid,
- None => Err(anyhow!("repository not found: {}", did))?,
- };
- let last_commit = srv.repo.get_commit(&commit_cid)?;
- let full_map = srv.repo.mst_to_map(&last_commit.mst_cid)?;
- let prefix = "/app.bsky.actor.profile/";
- for (mst_key, cid) in full_map.iter() {
- if mst_key.starts_with(prefix) {
- profile_cid = Some(*cid);
- }
- }
- let (display_name, description): (Option<String>, Option<String>) =
- if let Some(cid) = profile_cid {
- let record: ProfileRecord =
- serde_json::from_value(ipld_into_json_value(srv.repo.get_ipld(&cid)?))?;
- (Some(record.displayName), record.description)
- } else {
- (None, None)
- };
- let mut stmt = srv
- .atp_db
- .conn
- .prepare_cached("SELECT handle FROM account WHERE did = $1")?;
- let handle: String = stmt.query_row(params!(did.to_string()), |row| row.get(0))?;
- let mut stmt = srv
- .atp_db
- .conn
- .prepare_cached("SELECT COUNT(*) FROM bsky_post WHERE did = $1")?;
- let post_count: u64 = stmt.query_row(params!(did.to_string()), |row| row.get(0))?;
- let mut stmt = srv
- .atp_db
- .conn
- .prepare_cached("SELECT COUNT(*) FROM bsky_follow WHERE did = $1")?;
- let follows_count: u64 = stmt.query_row(params!(did.to_string()), |row| row.get(0))?;
- let mut stmt = srv
- .atp_db
- .conn
- .prepare_cached("SELECT COUNT(*) FROM bsky_follow WHERE subject_did = $1")?;
- let followers_count: u64 = stmt.query_row(params!(did.to_string()), |row| row.get(0))?;
- let decl = DeclRef {
- actorType: "app.bsky.system.actorUser".to_string(),
- cid: "bafyreid27zk7lbis4zw5fz4podbvbs4fc5ivwji3dmrwa6zggnj4bnd57u".to_string(),
- };
- Ok(Profile {
- did: did.to_string(),
- handle,
- creator: did.to_string(),
- displayName: display_name,
- description,
- declaration: decl,
- followersCount: followers_count,
- followsCount: follows_count,
- postsCount: post_count,
- membersCount: 0,
- myState: json!({}),
- })
-}
-
-pub fn bsky_update_profile(srv: &mut AtpService, did: &Did, profile: ProfileRecord) -> Result<()> {
- // get the profile record
- let mut profile_tid: Option<Tid> = None;
- let commit_cid = match srv.repo.lookup_commit(did)? {
- Some(cid) => cid,
- None => Err(anyhow!("repository not found: {}", did))?,
- };
- let last_commit = srv.repo.get_commit(&commit_cid)?;
- let full_map = srv.repo.mst_to_map(&last_commit.mst_cid)?;
- let prefix = "/app.bsky.actor.profile/";
- for (mst_key, _cid) in full_map.iter() {
- if mst_key.starts_with(prefix) {
- profile_tid = Some(Tid::from_str(mst_key.split('/').nth(2).unwrap())?);
- }
- }
- let profile_tid: Tid = profile_tid.unwrap_or(srv.tid_gen.next_tid());
- let mutations: Vec<Mutation> = vec![Mutation::Update(
- Nsid::from_str("app.bsky.actor.profile")?,
- profile_tid,
- json_value_into_ipld(serde_json::to_value(profile)?),
- )];
- let keypair = srv.pds_keypair.clone();
- srv.repo.mutate_repo(did, &mutations, &keypair)?;
- Ok(())
-}
-
-struct FeedRow {
- pub item_did: Did,
- pub item_handle: String,
- pub item_post_tid: Tid,
- pub item_post_cid: Cid,
- pub indexed_at: String,
-}
-
-fn feed_row(row: &rusqlite::Row) -> Result<FeedRow> {
- let item_did: String = row.get(0)?;
- let item_did = Did::from_str(&item_did)?;
- let item_handle = row.get(1)?;
- let item_post_tid: String = row.get(2)?;
- let item_post_tid = Tid::from_str(&item_post_tid)?;
- let cid_string: String = row.get(3)?;
- let item_post_cid = Cid::from_str(&cid_string)?;
- let indexed_at: String = row.get(4)?;
- Ok(FeedRow {
- item_did,
- item_handle,
- item_post_tid,
- item_post_cid,
- indexed_at,
- })
-}
-
-fn feed_row_to_item(srv: &mut AtpService, row: FeedRow) -> Result<FeedItem> {
- let record_ipld = srv.repo.get_ipld(&row.item_post_cid)?;
- let uri = format!(
- "at://{}/{}/{}",
- row.item_did, "app.bsky.feed.post", row.item_post_tid
- );
-
- let mut stmt = srv.atp_db.conn.prepare_cached(
- "SELECT COUNT(*) FROM bsky_ref WHERE ref_type = 'like' AND subject_uri = $1",
- )?;
- let like_count: u64 = stmt.query_row(params!(uri), |row| row.get(0))?;
-
- let mut stmt = srv.atp_db.conn.prepare_cached(
- "SELECT COUNT(*) FROM bsky_ref WHERE ref_type = 'repost' AND subject_uri = $1",
- )?;
- let repost_count: u64 = stmt.query_row(params!(uri), |row| row.get(0))?;
-
- let mut stmt = srv
- .atp_db
- .conn
- .prepare_cached("SELECT COUNT(*) FROM bsky_post WHERE reply_to_parent_uri = $1")?;
- let reply_count: u64 = stmt.query_row(params!(uri), |row| row.get(0))?;
-
- let feed_item = FeedItem {
- uri,
- cid: row.item_post_cid.to_string(),
- author: User {
- did: row.item_did.to_string(),
- handle: row.item_handle,
- displayName: None, // TODO: fetch from profile (or cache)
- },
- repostedBy: None,
- record: ipld_into_json_value(record_ipld),
- embed: None,
- replyCount: reply_count,
- repostCount: repost_count,
- upvoteCount: like_count,
- downvoteCount: 0,
- indexedAt: row.indexed_at,
- myState: None,
- };
- Ok(feed_item)
-}
-
-pub fn bsky_get_timeline(srv: &mut AtpService, did: &Did) -> Result<GenericFeed> {
- let mut feed: Vec<FeedItem> = vec![];
- // TODO: also handle reposts
- 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 LEFT JOIN bsky_follow ON bsky_post.did = bsky_follow.subject_did WHERE bsky_follow.did = ?1 AND account.did IS NOT NULL ORDER BY bsky_post.tid DESC LIMIT 20")?;
- let mut sql_rows = stmt.query(params!(did.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 {
- feed.push(feed_row_to_item(srv, row)?);
- }
- Ok(GenericFeed { feed })
-}
-
-pub fn bsky_get_author_feed(srv: &mut AtpService, did: &Did) -> Result<GenericFeed> {
- let mut feed: Vec<FeedItem> = vec![];
- // TODO: also handle reposts
- 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.did = ?1 ORDER BY bsky_post.tid DESC LIMIT 20")?;
- let mut sql_rows = stmt.query(params!(did.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 {
- feed.push(feed_row_to_item(srv, row)?);
- }
- 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,
- _depth: Option<u64>,
-) -> Result<PostThread> {
- // 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().next().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,
- upvoteCount: item.upvoteCount,
- downvoteCount: 0,
- 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,
- replyCount: post_item.replyCount,
- replies: Some(children),
- upvoteCount: post_item.upvoteCount,
- downvoteCount: 0,
- 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 libipld::ipld;
-
- let post_nsid = Nsid::from_str("app.bsky.feed.post").unwrap();
- let follow_nsid = Nsid::from_str("app.bsky.graph.follow").unwrap();
-
- let mut srv = AtpService::new_ephemeral().unwrap();
- let req = AccountRequest {
- email: "test@bogus.com".to_string(),
- handle: "handle.test".to_string(),
- password: "bogus".to_string(),
- inviteCode: None,
- recoveryKey: None,
- };
- let session = create_account(&mut srv, &req, true).unwrap();
- let did = Did::from_str(&session.did).unwrap();
- let profile = bsky_get_profile(&mut srv, &did).unwrap();
- assert_eq!(profile.did, session.did);
- assert_eq!(profile.handle, req.handle);
- assert_eq!(profile.displayName, None);
- assert_eq!(profile.description, None);
- assert_eq!(profile.followersCount, 0);
- assert_eq!(profile.followsCount, 0);
- assert_eq!(profile.postsCount, 0);
-
- let record = ProfileRecord {
- displayName: "Test Name".to_string(),
- description: Some("short description".to_string()),
- };
- bsky_update_profile(&mut srv, &did, record.clone()).unwrap();
- let profile = bsky_get_profile(&mut srv, &did).unwrap();
- assert_eq!(profile.displayName, Some(record.displayName));
- assert_eq!(profile.description, record.description);
-
- let record = ProfileRecord {
- displayName: "New Test Name".to_string(),
- description: Some("longer description".to_string()),
- };
- bsky_update_profile(&mut srv, &did, record.clone()).unwrap();
- let profile = bsky_get_profile(&mut srv, &did).unwrap();
- assert_eq!(profile.displayName, Some(record.displayName));
- assert_eq!(profile.description, record.description);
-
- let mutations = vec![
- Mutation::Create(
- follow_nsid.clone(),
- srv.tid_gen.next_tid(),
- ipld!({"subject": {"did": session.did}, "createdAt": created_at_now()}),
- ),
- Mutation::Create(
- follow_nsid.clone(),
- srv.tid_gen.next_tid(),
- ipld!({"subject": {"did": "did:web:external.domain"}, "createdAt": created_at_now()}),
- ),
- Mutation::Create(
- post_nsid.clone(),
- srv.tid_gen.next_tid(),
- ipld!({"text": "first post"}),
- ),
- Mutation::Create(
- post_nsid.clone(),
- srv.tid_gen.next_tid(),
- ipld!({"text": "second post"}),
- ),
- Mutation::Create(
- post_nsid.clone(),
- srv.tid_gen.next_tid(),
- ipld!({"text": "third post"}),
- ),
- ];
- srv.repo
- .mutate_repo(&did, &mutations, &srv.pds_keypair)
- .unwrap();
- bsky_mutate_db(&mut srv.atp_db, &did, mutations).unwrap();
-
- let profile = bsky_get_profile(&mut srv, &did).unwrap();
- assert_eq!(profile.followersCount, 1);
- assert_eq!(profile.followsCount, 2);
- assert_eq!(profile.postsCount, 3);
-}
-
-#[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;
-
- let post_nsid = Nsid::from_str("app.bsky.feed.post").unwrap();
- let like_nsid = Nsid::from_str("app.bsky.feed.like").unwrap();
- let repost_nsid = Nsid::from_str("app.bsky.feed.repost").unwrap();
- let follow_nsid = Nsid::from_str("app.bsky.graph.follow").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()
- };
- let carol_did = {
- let req = AccountRequest {
- email: "carol@bogus.com".to_string(),
- handle: "carol.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()
- };
-
- // all feeds and timelines should be empty
- let alice_feed = bsky_get_author_feed(&mut srv, &alice_did).unwrap();
- let alice_timeline = bsky_get_timeline(&mut srv, &alice_did).unwrap();
- assert!(alice_feed.feed.is_empty());
- assert!(alice_timeline.feed.is_empty());
- let bob_feed = bsky_get_author_feed(&mut srv, &bob_did).unwrap();
- let bob_timeline = bsky_get_timeline(&mut srv, &bob_did).unwrap();
- assert!(bob_feed.feed.is_empty());
- assert!(bob_timeline.feed.is_empty());
- let carol_feed = bsky_get_author_feed(&mut srv, &carol_did).unwrap();
- let carol_timeline = bsky_get_timeline(&mut srv, &carol_did).unwrap();
- assert!(carol_feed.feed.is_empty());
- assert!(carol_timeline.feed.is_empty());
-
- // alice does some posts
- let alice_post1_tid = srv.tid_gen.next_tid();
- let alice_post2_tid = srv.tid_gen.next_tid();
- let alice_post3_tid = srv.tid_gen.next_tid();
- assert!(alice_post1_tid < alice_post2_tid && alice_post2_tid < alice_post3_tid);
- let mutations = vec![
- Mutation::Create(
- post_nsid.clone(),
- alice_post1_tid.clone(),
- ipld!({"text": "alice first post"}),
- ),
- Mutation::Create(
- post_nsid.clone(),
- alice_post2_tid.clone(),
- ipld!({"text": "alice second post"}),
- ),
- Mutation::Create(
- post_nsid.clone(),
- alice_post3_tid.clone(),
- ipld!({"text": "alice third post"}),
- ),
- ];
- srv.repo
- .mutate_repo(&alice_did, &mutations, &srv.pds_keypair)
- .unwrap();
- bsky_mutate_db(&mut srv.atp_db, &alice_did, mutations).unwrap();
-
- // bob follows alice, likes first post, reposts second, replies third
- let alice_post3_uri = format!(
- "at://{}/{}/{}",
- alice_did.to_string(),
- post_nsid.to_string(),
- alice_post3_tid.to_string()
- );
- let mutations = vec![
- Mutation::Create(
- follow_nsid.clone(),
- srv.tid_gen.next_tid(),
- ipld!({"subject": {"did": alice_did.to_string()}, "createdAt": created_at_now()}),
- ),
- Mutation::Create(
- like_nsid.clone(),
- srv.tid_gen.next_tid(),
- ipld!({"subject": {"uri": format!("at://{}/{}/{}", alice_did.to_string(), post_nsid.to_string(), alice_post1_tid.to_string())}, "createdAt": created_at_now()}),
- ),
- Mutation::Create(
- repost_nsid.clone(),
- srv.tid_gen.next_tid(),
- ipld!({"subject": {"uri": format!("at://{}/{}/{}", alice_did.to_string(), post_nsid.to_string(), alice_post2_tid.to_string())}, "createdAt": created_at_now()}),
- ),
- Mutation::Create(
- post_nsid.clone(),
- srv.tid_gen.next_tid(),
- ipld!({"text": "bob comment on alice post3", "reply": {"parent": {"uri": alice_post3_uri.clone()}, "root": {"uri": alice_post3_uri.clone()}}}),
- ),
- ];
- srv.repo
- .mutate_repo(&bob_did, &mutations, &srv.pds_keypair)
- .unwrap();
- bsky_mutate_db(&mut srv.atp_db, &bob_did, mutations).unwrap();
-
- // carol follows bob
- let mutations = vec![Mutation::Create(
- follow_nsid.clone(),
- srv.tid_gen.next_tid(),
- ipld!({"subject": {"did": bob_did.to_string()}, "createdAt": created_at_now()}),
- )];
- srv.repo
- .mutate_repo(&bob_did, &mutations, &srv.pds_keypair)
- .unwrap();
- bsky_mutate_db(&mut srv.atp_db, &carol_did, mutations).unwrap();
-
- // test alice profile: counts should be updated
- let alice_profile = bsky_get_profile(&mut srv, &alice_did).unwrap();
- assert_eq!(alice_profile.followersCount, 1);
- assert_eq!(alice_profile.followsCount, 0);
- assert_eq!(alice_profile.postsCount, 3);
-
- // test alice timeline: still empty (?)
- let alice_timeline = bsky_get_timeline(&mut srv, &alice_did).unwrap();
- println!("{:?}", alice_timeline);
- assert!(alice_timeline.feed.is_empty());
-
- // test alice feed: should have 3 posts, with correct counts
- let alice_feed = bsky_get_author_feed(&mut srv, &alice_did).unwrap();
- assert_eq!(alice_feed.feed.len(), 3);
-
- assert_eq!(
- alice_feed.feed[2].uri,
- format!(
- "at://{}/{}/{}",
- alice_did.to_string(),
- post_nsid.to_string(),
- alice_post1_tid.to_string()
- )
- );
- // TODO: CID
- assert_eq!(alice_feed.feed[2].author.did, alice_did.to_string());
- assert_eq!(alice_feed.feed[2].author.handle, "alice.test");
- assert_eq!(alice_feed.feed[2].repostedBy, None);
- assert_eq!(
- alice_feed.feed[2].record["text"].as_str().unwrap(),
- "alice first post"
- );
- assert_eq!(alice_feed.feed[2].embed, None);
- assert_eq!(alice_feed.feed[2].replyCount, 0);
- assert_eq!(alice_feed.feed[2].repostCount, 0);
- assert_eq!(alice_feed.feed[2].upvoteCount, 1);
- assert_eq!(alice_feed.feed[2].downvoteCount, 0);
-
- assert_eq!(alice_feed.feed[1].author.did, alice_did.to_string());
- assert_eq!(alice_feed.feed[1].replyCount, 0);
- assert_eq!(alice_feed.feed[1].repostCount, 1);
- assert_eq!(alice_feed.feed[1].upvoteCount, 0);
-
- assert_eq!(alice_feed.feed[0].author.did, alice_did.to_string());
- assert_eq!(alice_feed.feed[0].replyCount, 1);
- assert_eq!(alice_feed.feed[0].repostCount, 0);
- assert_eq!(alice_feed.feed[0].upvoteCount, 0);
-
- // test bob timeline: should include alice posts
- let bob_timeline = bsky_get_timeline(&mut srv, &bob_did).unwrap();
- println!("BOB TIMELINE ======");
- for item in bob_timeline.feed.iter() {
- println!("{:?}", item);
- }
- assert_eq!(bob_timeline.feed.len(), 3);
- assert_eq!(
- bob_timeline.feed[2].uri,
- format!(
- "at://{}/{}/{}",
- alice_did.to_string(),
- post_nsid.to_string(),
- alice_post1_tid.to_string()
- )
- );
- // TODO: CID
- assert_eq!(bob_timeline.feed[2].author.did, alice_did.to_string());
- assert_eq!(bob_timeline.feed[2].author.handle, "alice.test");
-
- // test bob feed: should include repost and reply
- let bob_feed = bsky_get_author_feed(&mut srv, &bob_did).unwrap();
- assert_eq!(bob_feed.feed.len(), 1);
- // TODO: handle reposts
- /*
- assert_eq!(bob_feed.feed.len(), 2);
- assert_eq!(bob_feed.feed[1].uri, format!("at://{}/{}/{}", alice_did.to_string(), post_nsid.to_string(), alice_post1_tid.to_string()));
- // TODO: CID
- assert_eq!(bob_feed.feed[1].author.did, alice_did.to_string());
- assert_eq!(bob_feed.feed[1].author.handle, "alice.test");
- assert_eq!(bob_feed.feed[1].repostedBy.as_ref().unwrap().did, bob_did.to_string());
- assert_eq!(bob_feed.feed[1].repostedBy.as_ref().unwrap().handle, "bob.test");
- // TODO: "is a repost" (check record?)
- */
-
- assert_eq!(bob_feed.feed[0].author.did, bob_did.to_string());
- assert_eq!(bob_feed.feed[0].author.handle, "bob.test");
-
- // test carol timeline: should include bob's repost and reply
- let carol_timeline = bsky_get_timeline(&mut srv, &carol_did).unwrap();
- // TODO: handle re-posts (+1 here)
- assert_eq!(carol_timeline.feed.len(), 1);
- // TODO: details
-
- // test carol feed: still empty
- let carol_feed = bsky_get_author_feed(&mut srv, &carol_did).unwrap();
- assert!(carol_feed.feed.is_empty());
-}
-
-#[test]
-fn test_bsky_thread() {
- use crate::create_account;
- 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.upvoteCount, 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
-}