diff options
Diffstat (limited to 'adenosine-cli')
| -rw-r--r-- | adenosine-cli/Cargo.toml | 1 | ||||
| -rw-r--r-- | adenosine-cli/src/bin/adenosine.rs | 3 | ||||
| -rw-r--r-- | adenosine-cli/src/identifiers.rs | 418 | ||||
| -rw-r--r-- | adenosine-cli/src/lib.rs | 243 | 
4 files changed, 3 insertions, 662 deletions
| diff --git a/adenosine-cli/Cargo.toml b/adenosine-cli/Cargo.toml index 72e3a0b..9c8c62c 100644 --- a/adenosine-cli/Cargo.toml +++ b/adenosine-cli/Cargo.toml @@ -15,6 +15,7 @@ readme = "README.md"  repository = "https://gitlab.com/bnewbold/adenosine"  [dependencies] +adenosine = { version = "0.2.0", path = "../adenosine" }  structopt = "0.3"  # NOTE: could try 'rustls-tls' feature instead of default native TLS?  reqwest = { version = "0.11", features = ["blocking", "json"] } diff --git a/adenosine-cli/src/bin/adenosine.rs b/adenosine-cli/src/bin/adenosine.rs index 21fdf31..4164181 100644 --- a/adenosine-cli/src/bin/adenosine.rs +++ b/adenosine-cli/src/bin/adenosine.rs @@ -1,4 +1,5 @@ -use adenosine_cli::identifiers::*; +use adenosine::identifiers::*; +use adenosine::*;  use adenosine_cli::*;  use anyhow::anyhow;  use serde_json::{json, Value}; diff --git a/adenosine-cli/src/identifiers.rs b/adenosine-cli/src/identifiers.rs deleted file mode 100644 index 7e93d71..0000000 --- a/adenosine-cli/src/identifiers.rs +++ /dev/null @@ -1,418 +0,0 @@ -use anyhow::{anyhow, Result}; -use lazy_static::lazy_static; -use regex::Regex; -use std::fmt; -use std::ops::Deref; -use std::str::FromStr; -use std::time::SystemTime; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum DidOrHost { -    Did(String, String), -    Host(String), -} - -impl FromStr for DidOrHost { -    type Err = anyhow::Error; - -    /// DID syntax is specified in: <https://w3c.github.io/did-core/#did-syntax> -    /// -    /// Lazy partial hostname regex, isn't very correct. -    fn from_str(s: &str) -> Result<Self, Self::Err> { -        lazy_static! { -            static ref DID_RE: Regex = -                Regex::new(r"^did:([a-z]{1,64}):([a-zA-Z0-9\-.]{1,1024})$").unwrap(); -        } -        lazy_static! { -            static ref HOSTNAME_RE: Regex = -                Regex::new(r"^[A-Za-z][A-Za-z0-9-]*(\.[A-Za-z][A-Za-z0-9-]*)+$").unwrap(); -        } -        if let Some(caps) = DID_RE.captures(s) { -            Ok(Self::Did(caps[1].to_string(), caps[2].to_string())) -        } else if HOSTNAME_RE.is_match(s) { -            Ok(Self::Host(s.to_string())) -        } else { -            Err(anyhow!("does not match as a DID or hostname: {}", s)) -        } -    } -} - -impl fmt::Display for DidOrHost { -    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -        match self { -            Self::Host(v) => write!(f, "{}", v), -            Self::Did(m, v) => write!(f, "did:{}:{}", m, v), -        } -    } -} - -#[test] -fn test_didorhost() { -    assert_eq!( -        DidOrHost::from_str("hyde.test").unwrap(), -        DidOrHost::Host("hyde.test".to_string()) -    ); -    assert_eq!( -        DidOrHost::from_str("did:method:blah").unwrap(), -        DidOrHost::Did("method".to_string(), "blah".to_string()) -    ); - -    assert!(DidOrHost::from_str("barestring").is_err()); -    assert!(DidOrHost::from_str("did:partial:").is_err()); -    assert!(DidOrHost::from_str("").is_err()); -    assert!(DidOrHost::from_str(" ").is_err()); -    assert!(DidOrHost::from_str("1234").is_err()); - -    assert!(DidOrHost::from_str("multi.part.domain").is_ok()); -    assert!(DidOrHost::from_str("did:is:weird").is_ok()); -    assert!(DidOrHost::from_str("did:plc:bv6ggog3tya2z3vxsub7hnal").is_ok()); -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct AtUri { -    pub repository: DidOrHost, -    pub collection: Option<String>, -    pub record: Option<String>, -    pub fragment: Option<String>, -} - -impl FromStr for AtUri { -    type Err = anyhow::Error; - -    fn from_str(s: &str) -> Result<Self, Self::Err> { -        lazy_static! { -            static ref ATURI_RE: Regex = Regex::new(r"^at://([a-zA-Z0-9:_\.-]+)(/(([a-zA-Z0-9\.]+))?)?(/(([a-zA-Z0-9\.-]+))?)?(#([a-zA-Z0-9/-]+))?$").unwrap(); -        } -        if let Some(caps) = ATURI_RE.captures(s) { -            let uri = AtUri { -                repository: DidOrHost::from_str(&caps[1])?, -                collection: caps.get(4).map(|v| v.as_str().to_string()), -                record: caps.get(7).map(|v| v.as_str().to_string()), -                fragment: caps.get(9).map(|v| v.as_str().to_string()), -            }; -            Ok(uri) -        } else { -            Err(anyhow!("couldn't parse as an at:// URI: {}", s)) -        } -    } -} - -impl fmt::Display for AtUri { -    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -        write!(f, "at://{}", self.repository)?; -        if let Some(ref c) = self.collection { -            write!(f, "/{}", c)?; -        }; -        if let Some(ref r) = self.record { -            write!(f, "/{}", r)?; -        }; -        if let Some(ref v) = self.fragment { -            write!(f, "#{}", v)?; -        }; -        Ok(()) -    } -} - -#[test] -fn test_aturi() { -    assert!(AtUri::from_str("at://bob.com").is_ok()); -    assert!(AtUri::from_str("at://bob.com/").is_ok()); -    assert!(AtUri::from_str("at://did:plc:bv6ggog3tya2z3vxsub7hnal").is_ok()); -    assert!(AtUri::from_str("at://bob.com/io.example.song").is_ok()); -    assert!(AtUri::from_str("at://bob.com/io.example.song/").is_ok()); -    assert!(AtUri::from_str("at://bob.com/io.example.song/3yI5-c1z-cc2p-1a").is_ok()); -    assert!(AtUri::from_str("at://bob.com/io.example.song/3yI5-c1z-cc2p-1a#/title").is_ok()); -    assert!( -        AtUri::from_str("at://did:plc:ltk4reuh7rkoy2frnueetpb5/app.bsky.follow/3jg23pbmlhc2a") -            .is_ok() -    ); - -    let uri = AtUri { -        repository: DidOrHost::Did("some".to_string(), "thing".to_string()), -        collection: Some("com.atproto.record".to_string()), -        record: Some("asdf-123".to_string()), -        fragment: Some("/path".to_string()), -    }; -    assert_eq!( -        "at://did:some:thing/com.atproto.record/asdf-123#/path", -        uri.to_string() -    ); -    println!("{:?}", AtUri::from_str(&uri.to_string())); -    assert!(AtUri::from_str(&uri.to_string()).is_ok()); - -    let uri = AtUri::from_str("at://bob.com/io.example.song/3yI5-c1z-cc2p-1a#/title").unwrap(); -    assert_eq!(uri.repository, DidOrHost::Host("bob.com".to_string())); -    assert_eq!(uri.collection, Some("io.example.song".to_string())); -    assert_eq!(uri.record, Some("3yI5-c1z-cc2p-1a".to_string())); -    assert_eq!(uri.fragment, Some("/title".to_string())); - -    let uri = AtUri::from_str("at://bob.com/io.example.song/").unwrap(); -    assert_eq!(uri.repository, DidOrHost::Host("bob.com".to_string())); -    assert_eq!(uri.collection, Some("io.example.song".to_string())); -} - -/// A String (newtype) representing an NSID -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] -pub struct Nsid(String); - -impl FromStr for Nsid { -    type Err = anyhow::Error; - -    fn from_str(s: &str) -> Result<Self, Self::Err> { -        lazy_static! { -            static ref NSID_RE: Regex = Regex::new(r"^([a-z][a-z0-9-]+\.)+[a-zA-Z0-9-]+$").unwrap(); -        } -        if NSID_RE.is_match(s) { -            Ok(Self(s.to_string())) -        } else { -            Err(anyhow!("does not match as an NSID: {}", s)) -        } -    } -} - -impl Deref for Nsid { -    type Target = str; - -    fn deref(&self) -> &Self::Target { -        &self.0 -    } -} - -impl Nsid { -    pub fn domain(&self) -> String { -        self.rsplit('.').skip(1).collect::<Vec<&str>>().join(".") -    } - -    pub fn name(&self) -> String { -        self.split('.') -            .last() -            .expect("multiple segments in NSID") -            .to_string() -    } -} - -impl fmt::Display for Nsid { -    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -        write!(f, "{}", self.0) -    } -} - -#[test] -fn test_nsid() { -    assert!(Nsid::from_str("com.atproto.recordType").is_ok()); - -    let nsid = Nsid::from_str("com.atproto.recordType").unwrap(); -    assert_eq!(nsid.domain(), "atproto.com".to_string()); -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] -pub struct Did(String); - -impl FromStr for Did { -    type Err = anyhow::Error; - -    /// DID syntax is specified in: <https://w3c.github.io/did-core/#did-syntax> -    /// -    /// This regex does not follow that definition exactly. -    fn from_str(s: &str) -> Result<Self, Self::Err> { -        lazy_static! { -            static ref DID_RE: Regex = -                Regex::new(r"^did:([a-z]{1,32}):([a-zA-Z0-9\-.]{1,256})$").unwrap(); -        } -        if DID_RE.is_match(s) { -            Ok(Self(s.to_string())) -        } else { -            Err(anyhow!("does not match as a DID: {}", s)) -        } -    } -} - -impl Deref for Did { -    type Target = str; - -    fn deref(&self) -> &Self::Target { -        &self.0 -    } -} - -impl Did { -    pub fn did_type(&self) -> String { -        self.split(':').nth(1).unwrap().to_string() -    } -} - -impl fmt::Display for Did { -    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -        write!(f, "{}", self.0) -    } -} - -#[test] -fn test_did() { -    assert!(Did::from_str("did:web:asdf.org").is_ok()); -    assert!(Did::from_str("did:plc:asdf").is_ok()); - -    assert!(Did::from_str("bob.com").is_err()); -    assert!(Did::from_str("").is_err()); -    assert!(Did::from_str("did:").is_err()); -    assert!(Did::from_str("did:plc:").is_err()); -    assert!(Did::from_str("plc:asdf").is_err()); -    assert!(Did::from_str("DID:thing:thang").is_err()); - -    assert_eq!( -        Did::from_str("did:web:asdf.org").unwrap().did_type(), -        "web".to_string() -    ); -} - -lazy_static! { -    /// Sortable base32 encoding, as bluesky implements/defines -    static ref BASE32SORT: data_encoding::Encoding = { -        let mut spec = data_encoding::Specification::new(); -        spec.symbols.push_str("234567abcdefghijklmnopqrstuvwxyz"); -        spec.padding = None; -        spec.encoding().unwrap() -    }; -} - -/// A string identifier for individual records, based on UNIX timestamp in microseconds. -/// -/// See also: https://github.com/bluesky-social/atproto/issues/334 -/// -/// Pretty permissive about what can be parsed/accepted, because there were some old TIDs floating -/// around with weird format. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Tid(String); - -impl FromStr for Tid { -    type Err = anyhow::Error; - -    fn from_str(s: &str) -> Result<Self, Self::Err> { -        lazy_static! { -            static ref TID_RE: Regex = Regex::new(r"^[0-9a-zA-Z-]{13,20}$").unwrap(); -        } -        if TID_RE.is_match(s) { -            Ok(Self(s.to_string())) -        } else { -            Err(anyhow!("does not match as a TID: {}", s)) -        } -    } -} - -impl Deref for Tid { -    type Target = str; - -    fn deref(&self) -> &Self::Target { -        &self.0 -    } -} - -impl fmt::Display for Tid { -    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -        write!(f, "{}", self.0) -    } -} - -impl Tid { -    pub fn new(micros: u64, clock_id: u16) -> Self { -        // 53 bits of millis -        let micros = micros & 0x001FFFFFFFFFFFFF; -        // 10 bits of clock ID -        let clock_id = clock_id & 0x03FF; -        let val: u64 = (micros << 10) | (clock_id as u64); -        // big-endian encoding -        let enc = BASE32SORT.encode(&val.to_be_bytes()); -        Tid(format!( -            "{}-{}-{}-{}", -            &enc[0..4], -            &enc[4..7], -            &enc[7..11], -            &enc[11..13] -        )) -    } -} - -#[test] -fn test_tid() { -    Tid::from_str("3yI5-c1z-cc2p-1a").unwrap(); -    assert!(Tid::from_str("3jg6anbimrc2a").is_ok()); -    assert!(Tid::from_str("3yI5-c1z-cc2p-1a").is_ok()); - -    Tid::from_str("asdf234as4asdf234").unwrap(); -    assert!(Tid::from_str("asdf234as4asdf234").is_ok()); - -    assert!(Tid::from_str("").is_err()); -    assert!(Tid::from_str("com").is_err()); -    assert!(Tid::from_str("com.blah.Thing").is_err()); -    assert!(Tid::from_str("did:stuff:blah").is_err()); - -    let t1 = Tid::new(0, 0); -    assert_eq!(t1.to_string(), "2222-222-2222-22".to_string()); -} - -/// TID Generator -/// -/// This version uses 53-bit microsecond counter (since UNIX epoch), and a random 10-bit clock id. -/// -/// If the current timestamp is not greater than the last timestamp (either because clock did not -/// advance monotonically, or multiple TIDs were generated in the same microsecond (very unlikely), -/// the timestamp is simply incremented. -pub struct Ticker { -    last_timestamp: u64, -    clock_id: u16, -} - -impl Ticker { -    pub fn new() -> Self { -        let mut ticker = Self { -            last_timestamp: 0, -            // mask to 10 bits -            clock_id: rand::random::<u16>() & 0x03FF, -        }; -        // prime the pump -        ticker.next_tid(); -        ticker -    } - -    pub fn next_tid(&mut self) -> Tid { -        let now = SystemTime::now() -            .duration_since(SystemTime::UNIX_EPOCH) -            .expect("timestamp in micros since UNIX epoch") -            .as_micros() as u64; -        // mask to 53 bits -        let now = now & 0x001FFFFFFFFFFFFF; -        if now > self.last_timestamp { -            self.last_timestamp = now; -        } else { -            self.last_timestamp += 1; -        } -        Tid::new(self.last_timestamp, self.clock_id) -    } -} - -impl Default for Ticker { -    fn default() -> Self { -        Self::new() -    } -} - -#[test] -fn test_ticker() { -    let mut ticker = Ticker::new(); -    let mut prev = ticker.next_tid(); -    let mut next = ticker.next_tid(); -    for _ in [0..100] { -        println!("{} >? {}", next, prev); -        assert!(next > prev); -        prev = next; -        next = ticker.next_tid(); -    } -    println!("{}", prev); -    assert_eq!(prev, Tid::from_str(&prev.to_string()).unwrap()); -    assert_eq!(next[13..16], prev[13..16]); - -    let mut other_ticker = Ticker::new(); -    let other = other_ticker.next_tid(); -    assert!(other > next); -    assert!(next[13..16] != other[13..16]); -} diff --git a/adenosine-cli/src/lib.rs b/adenosine-cli/src/lib.rs index 0db2cb4..3eb56cc 100644 --- a/adenosine-cli/src/lib.rs +++ b/adenosine-cli/src/lib.rs @@ -2,226 +2,9 @@ use anyhow::anyhow;  pub use anyhow::Result;  use lazy_static::lazy_static;  use regex::Regex; -use reqwest::header;  use serde_json::Value;  use std::collections::HashMap;  use std::str::FromStr; -use std::time::Duration; - -pub mod identifiers; -use identifiers::Nsid; - -static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum XrpcMethod { -    Get, -    Post, -} - -impl FromStr for XrpcMethod { -    type Err = anyhow::Error; - -    fn from_str(s: &str) -> Result<Self, Self::Err> { -        match s { -            "get" => Ok(XrpcMethod::Get), -            "post" => Ok(XrpcMethod::Post), -            _ => Err(anyhow!("unknown method: {}", s)), -        } -    } -} - -pub struct XrpcClient { -    http_client: reqwest::blocking::Client, -    host: String, -} - -impl XrpcClient { -    pub fn new(host: String, auth_token: Option<String>) -> Result<Self> { -        let mut headers = header::HeaderMap::new(); -        if let Some(token) = &auth_token { -            let mut auth_value = header::HeaderValue::from_str(&format!("Bearer {}", token))?; -            auth_value.set_sensitive(true); -            headers.insert(header::AUTHORIZATION, auth_value); -        }; - -        let http_client = reqwest::blocking::Client::builder() -            .default_headers(headers) -            .user_agent(APP_USER_AGENT) -            .timeout(Duration::from_secs(30)) -            //.danger_accept_invalid_certs(true) -            .build() -            .expect("ERROR :: Could not build reqwest client"); - -        Ok(XrpcClient { http_client, host }) -    } - -    pub fn get( -        &self, -        nsid: &Nsid, -        params: Option<HashMap<String, String>>, -    ) -> Result<Option<Value>> { -        log::debug!("XRPC GET endpoint={} params={:?}", nsid, params); -        let params: HashMap<String, String> = params.unwrap_or_default(); -        let res = self -            .http_client -            .get(format!("{}/xrpc/{}", self.host, nsid)) -            .query(¶ms) -            .send()?; -        // TODO: refactor this error handling stuff into single method -        if res.status() == 400 { -            let val: Value = res.json()?; -            return Err(anyhow!( -                "XRPC Bad Request (400): {}", -                val["message"].as_str().unwrap_or("unknown") -            )); -        } else if res.status() == 500 { -            let val: Value = res.json()?; -            return Err(anyhow!( -                "XRPC Internal Error (500): {}", -                val["message"].as_str().unwrap_or("unknown") -            )); -        } -        let res = res.error_for_status()?; -        Ok(res.json()?) -    } - -    pub fn get_to_writer<W: std::io::Write>( -        &self, -        nsid: &Nsid, -        params: Option<HashMap<String, String>>, -        output: &mut W, -    ) -> Result<u64> { -        let params: HashMap<String, String> = params.unwrap_or_default(); -        let res = self -            .http_client -            .get(format!("{}/xrpc/{}", self.host, nsid)) -            .query(¶ms) -            .send()?; -        if res.status() == 400 { -            let val: Value = res.json()?; -            return Err(anyhow!( -                "XRPC Bad Request (400): {}", -                val["message"].as_str().unwrap_or("unknown") -            )); -        } else if res.status() == 500 { -            let val: Value = res.json()?; -            return Err(anyhow!( -                "XRPC Internal Error (500): {}", -                val["message"].as_str().unwrap_or("unknown") -            )); -        } -        let mut res = res.error_for_status()?; -        Ok(res.copy_to(output)?) -    } - -    pub fn post( -        &self, -        nsid: &Nsid, -        params: Option<HashMap<String, String>>, -        body: Option<Value>, -    ) -> Result<Option<Value>> { -        let params: HashMap<String, String> = params.unwrap_or_default(); -        log::debug!( -            "XRPC POST endpoint={} params={:?} body={:?}", -            nsid, -            params, -            body -        ); -        let mut req = self -            .http_client -            .post(format!("{}/xrpc/{}", self.host, nsid)) -            .query(¶ms); -        req = if let Some(b) = body { -            req.json(&b) -        } else { -            req -        }; -        let res = req.send()?; -        if res.status() == 400 { -            let val: Value = res.json()?; -            return Err(anyhow!( -                "XRPC Bad Request (400): {}", -                val["message"].as_str().unwrap_or("unknown") -            )); -        } else if res.status() == 500 { -            let val: Value = res.json()?; -            return Err(anyhow!( -                "XRPC Internal Error (500): {}", -                val["message"].as_str().unwrap_or("unknown") -            )); -        } -        let res = res.error_for_status()?; -        if res.content_length() == Some(0) { -            Ok(None) -        } else { -            Ok(res.json()?) -        } -    } - -    pub fn post_cbor_from_reader<R: std::io::Read>( -        &self, -        nsid: &Nsid, -        params: Option<HashMap<String, String>>, -        input: &mut R, -    ) -> Result<Option<Value>> { -        let params: HashMap<String, String> = params.unwrap_or_default(); -        let mut buf: Vec<u8> = Vec::new(); -        input.read_to_end(&mut buf)?; -        let res = self -            .http_client -            .post(format!("{}/xrpc/{}", self.host, nsid)) -            .query(¶ms) -            .header(reqwest::header::CONTENT_TYPE, "application/cbor") -            .body(buf) -            .send()?; -        if res.status() == 400 { -            let val: Value = res.json()?; -            return Err(anyhow!( -                "XRPC Bad Request: {}", -                val["message"].as_str().unwrap_or("unknown") -            )); -        } -        let res = res.error_for_status()?; -        Ok(res.json()?) -    } - -    //  reqwest::blocking::Body -} - -/// Tries to parse a DID internal identifier from a JWT (as base64-encoded token) -pub fn parse_did_from_jwt(jwt: &str) -> Result<String> { -    let second_b64 = jwt.split('.').nth(1).ok_or(anyhow!("couldn't parse JWT"))?; -    let second_json: Vec<u8> = base64::decode_config(second_b64, base64::URL_SAFE)?; -    let obj: Value = serde_json::from_slice(&second_json)?; -    // trying to also support pulling "aud" as DID; not sure this is actually correct use of -    // UCAN/JWT semantics? -    let did = obj["sub"] -        .as_str() -        .or(obj["aud"].as_str()) -        .ok_or(anyhow!("couldn't find DID subject in JWT"))? -        .to_string(); -    if !did.starts_with("did:") { -        return Err(anyhow!("couldn't find DID subject in JWT")); -    } -    Ok(did) -} - -#[test] -fn test_parse_jwt() { -    assert!(parse_did_from_jwt(".").is_err()); -    // JWT from atproto ("sub") -    assert_eq!( -        parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6cGxjOmV4M3NpNTI3Y2QyYW9nYnZpZGtvb296YyIsImlhdCI6MTY2NjgyOTM5M30.UvZgTqvaJICONa1wIUT1bny7u3hqVAqWhWy3qeuyZrE").unwrap(), -        "did:plc:ex3si527cd2aogbvidkooozc", -    ); -    // UCAN from adenosine-pds ("aud") -    assert_eq!( -        parse_did_from_jwt("eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6cGxjOnM3b25ieWphN2MzeXJzZ3Zob2xrbHM1YiIsImV4cCI6MTY3NTM4Mzg2NywiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6RG5hZWRHVGJkb0Frb1NlOG96a3k1WHAzMjZTVFpUSm50aDlHY2dxaTZQYjNzYjczIiwibm5jIjoiTnZURDhENWZjNXFpalIyMWJ1V2Z1ZE02dzlBM2drSy1ac3RtUW03b21pdyIsInByZiI6W119.QwZkb9R17tNhXnY_roqFYgdiIgUnSC18FYWQb3PcH6BU1R5l4W_T4XdACyczPGfM-jAnF2r2loBXDntYVS6N5A").unwrap(), -        "did:plc:s7onbyja7c3yrsgvholkls5b", -    ); -    assert!(parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").is_err()); -}  /// Represents fields/content specified on the command line.  /// @@ -322,29 +105,3 @@ pub fn value_from_fields(fields: Vec<ArgField>) -> Value {      }      Value::Object(serde_json::map::Map::from_iter(map.into_iter()))  } - -/// Helper to generate the current timestamp as right now, UTC, formatted as an RFC 3339 string. -/// -/// Currently, bluesky PDS expects millisecond precision, so we use that. -/// -/// Returns something like "2022-11-22T09:21:15.640Z" -pub fn created_at_now() -> String { -    let now = time::OffsetDateTime::now_utc(); -    // remove microsecond precision, but retain millisecond precision -    let ms = now.millisecond(); -    let now = now.replace_microsecond(0).unwrap(); -    let now = now.replace_millisecond(ms).unwrap(); -    now.format(&time::format_description::well_known::Rfc3339) -        .unwrap() -} - -#[test] -fn test_created_at_now() { -    // eg: 2022-11-22T09:20:44.123Z -    let ts = created_at_now(); -    println!("{}", ts); -    assert_eq!(&ts[4..5], "-"); -    assert_eq!(&ts[7..8], "-"); -    assert_eq!(&ts[10..11], "T"); -    assert_eq!(&ts[23..24], "Z"); -} | 
