This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-atproto / src / pointer.rs
5.5 kB 166 lines
1use knot_types::{AccountDid, AtUri, Cid, Collection, Nsid, Rkey, ServiceDid}; 2use serde::{Deserialize, Serialize}; 3 4use crate::AtprotoError; 5use crate::resolve::PdsEndpoint; 6 7pub fn put_record_method() -> Nsid { 8 Nsid::new_static("com.atproto.repo.putRecord").expect("literal method nsid parses") 9} 10 11#[derive(Debug, Clone, PartialEq, Eq)] 12pub struct PointerReceipt { 13 pub uri: AtUri, 14 pub cid: Cid, 15} 16 17pub(crate) fn pds_service_did(pds: &PdsEndpoint) -> Result<ServiceDid, AtprotoError> { 18 let bad = || AtprotoError::BadPdsEndpoint { 19 pds: pds.url().as_str().to_string(), 20 }; 21 let host = pds 22 .url() 23 .host_str() 24 .filter(|host| !host.is_empty()) 25 .ok_or_else(bad)?; 26 let authority = pds 27 .url() 28 .port() 29 .map_or_else(|| host.to_string(), |port| format!("{host}%3A{port}")); 30 let msid = std::iter::once(authority) 31 .chain( 32 pds.url() 33 .path_segments() 34 .into_iter() 35 .flatten() 36 .filter(|segment| !segment.is_empty()) 37 .map(str::to_string), 38 ) 39 .collect::<Vec<_>>() 40 .join(":"); 41 ServiceDid::new(format!("did:web:{msid}")).map_err(|_| bad()) 42} 43 44#[derive(Serialize)] 45struct PutRecordInput<'a, R: Serialize> { 46 repo: &'a AccountDid, 47 collection: &'static str, 48 rkey: &'a Rkey, 49 record: &'a R, 50} 51 52pub(crate) fn put_record_body<R: Collection + Serialize>( 53 subject: &AccountDid, 54 rkey: &Rkey, 55 record: &R, 56) -> Result<Vec<u8>, AtprotoError> { 57 serde_json::to_vec(&PutRecordInput { 58 repo: subject, 59 collection: R::NSID, 60 rkey, 61 record, 62 }) 63 .map_err(|error| AtprotoError::PointerEncode(error.to_string())) 64} 65 66#[derive(Deserialize)] 67struct PutRecordOutput { 68 uri: String, 69 cid: String, 70} 71 72pub(crate) fn receipt_from_response(body: &[u8]) -> Result<PointerReceipt, AtprotoError> { 73 let output: PutRecordOutput = serde_json::from_slice(body) 74 .map_err(|error| AtprotoError::MalformedReceipt(error.to_string()))?; 75 let uri = AtUri::new_owned(&output.uri) 76 .map_err(|error| AtprotoError::MalformedReceipt(error.to_string()))?; 77 let cid = Cid::new_owned(output.cid.as_bytes()) 78 .map_err(|error| AtprotoError::MalformedReceipt(error.to_string())) 79 .and_then(|cid: Cid| { 80 cid.is_valid().then_some(cid).ok_or_else(|| { 81 AtprotoError::MalformedReceipt(format!("cid {:?} doesn't parse", output.cid)) 82 }) 83 })?; 84 Ok(PointerReceipt { uri, cid }) 85} 86 87#[cfg(test)] 88mod tests { 89 use super::*; 90 use url::Url; 91 92 fn service_did_of(value: &str) -> Result<ServiceDid, AtprotoError> { 93 PdsEndpoint::new(Url::parse(value).unwrap()) 94 .map_err(|_| AtprotoError::BadPdsEndpoint { 95 pds: value.to_string(), 96 }) 97 .and_then(|pds| pds_service_did(&pds)) 98 } 99 100 struct PdsCase { 101 url: &'static str, 102 expect: fn(&Result<ServiceDid, AtprotoError>) -> bool, 103 } 104 105 const PDS_CASES: &[PdsCase] = &[ 106 PdsCase { 107 url: "https://pds.oyster.cafe", 108 expect: |r| matches!(r, Ok(did) if did.as_str() == "did:web:pds.oyster.cafe"), 109 }, 110 PdsCase { 111 url: "https://pds.oyster.cafe:8443", 112 expect: |r| matches!(r, Ok(did) if did.as_str() == "did:web:pds.oyster.cafe%3A8443"), 113 }, 114 PdsCase { 115 url: "https://shared.host/account-pds", 116 expect: |r| matches!(r, Ok(did) if did.as_str() == "did:web:shared.host:account-pds"), 117 }, 118 PdsCase { 119 url: "unix:/run/pds.sock", 120 expect: |r| matches!(r, Err(AtprotoError::BadPdsEndpoint { .. })), 121 }, 122 ]; 123 124 #[test] 125 fn pds_service_did_maps_each_endpoint_shape_to_its_web_did() { 126 PDS_CASES.iter().for_each(|case| { 127 let result = service_did_of(case.url); 128 assert!((case.expect)(&result), "case {:?} got {result:?}", case.url); 129 }); 130 } 131 132 #[test] 133 fn a_receipt_round_trips_its_uri_and_cid() { 134 let body = serde_json::json!({ 135 "uri": "at://did:plc:squid/sh.tangled.knot.member/3jzfcijpj2z2a", 136 "cid": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" 137 }); 138 let receipt = receipt_from_response(&serde_json::to_vec(&body).unwrap()).unwrap(); 139 assert_eq!( 140 receipt.uri.as_str(), 141 "at://did:plc:squid/sh.tangled.knot.member/3jzfcijpj2z2a" 142 ); 143 assert_eq!( 144 receipt.cid.as_str(), 145 "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" 146 ); 147 } 148 149 #[test] 150 fn a_garbage_receipt_is_a_typed_error() { 151 assert!(matches!( 152 receipt_from_response(b"not json"), 153 Err(AtprotoError::MalformedReceipt(_)) 154 )); 155 let bad_uri = serde_json::json!({ "uri": "http://nope", "cid": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" }); 156 assert!(matches!( 157 receipt_from_response(&serde_json::to_vec(&bad_uri).unwrap()), 158 Err(AtprotoError::MalformedReceipt(_)) 159 )); 160 let bad_cid = serde_json::json!({ "uri": "at://did:plc:squid/sh.tangled.knot.member/3jzfcijpj2z2a", "cid": "not-a-cid" }); 161 assert!(matches!( 162 receipt_from_response(&serde_json::to_vec(&bad_cid).unwrap()), 163 Err(AtprotoError::MalformedReceipt(_)) 164 )); 165 } 166}