use crate::bluesky::did::DidResolver;
use crate::error::AppError;
#[derive(Debug, Clone)]
pub struct PostRef {
pub did: String,
pub rkey: String,
}
pub async fn parse_post_uri(uri: &str) -> Result<PostRef, AppError> {
let trimmed = uri.trim();
if trimmed.starts_with("at://") {
parse_at_uri(trimmed)
} else if trimmed.contains("bsky.app/profile/") {
parse_bsky_url(trimmed).await
} else if trimmed.starts_with('@') && trimmed.contains('/') {
parse_compact_format(trimmed).await
} else {
Err(AppError::InvalidInput(format!(
"Invalid post URI/URL format: {}. Expected at:// URI, https://bsky.app/... URL, or @handle/rkey",
uri
)))
}
}
fn parse_at_uri(uri: &str) -> Result<PostRef, AppError> {
let parts: Vec<&str> = uri.trim_start_matches("at://").split('/').collect();
if parts.len() < 3 {
return Err(AppError::InvalidInput(format!(
"Invalid at:// URI format: {}. Expected at://{{did}}/app.bsky.feed.post/{{rkey}}",
uri
)));
}
Ok(PostRef {
did: parts[0].to_string(),
rkey: parts[2].to_string(),
})
}
async fn parse_bsky_url(url: &str) -> Result<PostRef, AppError> {
let url_parts: Vec<&str> = url.split('/').collect();
if url_parts.len() < 7 {
return Err(AppError::InvalidInput(format!(
"Invalid bsky.app URL format: {}. Expected https://bsky.app/profile/{{handle}}/post/{{rkey}}",
url
)));
}
let handle = url_parts[4];
let rkey = url_parts[6];
let resolver = DidResolver::new();
let did = resolver
.resolve_handle(handle)
.await?
.ok_or_else(|| AppError::DidResolveFailed(format!("Could not resolve handle: {}", handle)))?;
Ok(PostRef {
did,
rkey: rkey.to_string(),
})
}
async fn parse_compact_format(input: &str) -> Result<PostRef, AppError> {
if !input.starts_with('@') {
return Err(AppError::InvalidInput(format!(
"Compact format must start with @: {}",
input
)));
}
let without_at = &input[1..]; let parts: Vec<&str> = without_at.split('/').collect();
if parts.len() < 2 {
return Err(AppError::InvalidInput(format!(
"Invalid compact format: {}. Expected @handle/rkey",
input
)));
}
let handle = parts[0];
let rkey = parts[1];
let resolver = DidResolver::new();
let did = resolver
.resolve_handle(handle)
.await?
.ok_or_else(|| AppError::DidResolveFailed(format!("Could not resolve handle: {}", handle)))?;
Ok(PostRef {
did,
rkey: rkey.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_at_uri_valid() {
let uri = "at://did:plc:abc123/app.bsky.feed.post/xyz789";
let result = parse_at_uri(uri).unwrap();
assert_eq!(result.did, "did:plc:abc123");
assert_eq!(result.rkey, "xyz789");
}
#[test]
fn test_parse_at_uri_invalid() {
let uri = "at://did:plc:abc123";
let result = parse_at_uri(uri);
assert!(result.is_err());
}
#[test]
fn test_parse_at_uri_with_extra_path() {
let uri = "at://did:plc:abc123/app.bsky.feed.post/xyz789/extra";
let result = parse_at_uri(uri).unwrap();
assert_eq!(result.did, "did:plc:abc123");
assert_eq!(result.rkey, "xyz789");
}
#[test]
fn test_parse_compact_format_invalid_no_at() {
let input = "alice.bsky.social/xyz789";
let result = parse_at_uri(input);
assert!(result.is_err());
}
#[test]
fn test_parse_compact_format_invalid_no_slash() {
let input = "@alice.bsky.social";
let result = parse_at_uri(input);
assert!(result.is_err());
}
}