Now let's take a silly one
1

Configure Feed

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

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