use std::path::PathBuf;
use crate::{RhoResult, validate_relative_safe_path};
use serde::{Deserialize, Serialize};
pub mod github;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IdentityLocator {
pub provider: String,
pub handle: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RhoIdentity {
pub id: String,
pub provider: String,
pub handle: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub keys: Vec<IdentityKey>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub proofs: Vec<IdentityProof>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub discovery: Vec<DiscoveryLocator>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IdentityKey {
pub id: String,
pub kind: IdentityKeyKind,
pub algorithm: String,
pub public_key: String,
pub fingerprint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum IdentityKeyKind {
Signing,
Encryption,
Transport,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IdentityProof {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claim: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proof_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verified_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiscoveryLocator {
pub kind: String,
pub url: String,
}
pub trait IdentityProvider {
fn provider(&self) -> &'static str;
fn validate_handle(&self, handle: &str) -> RhoResult<()>;
fn identity_id(&self, handle: &str) -> RhoResult<String> {
self.validate_handle(handle)?;
Ok(format!("rho://id/{}/{}", self.provider(), handle))
}
fn handle_from_identity_id(&self, identity_id: &str) -> RhoResult<String> {
let locator = parse_identity_id(identity_id)?;
if locator.provider != self.provider() {
return Err(format!(
"unsupported identity provider for {}: {}",
self.provider(),
identity_id
)
.into());
}
self.validate_handle(&locator.handle)?;
Ok(locator.handle)
}
fn identity(&self, handle: &str) -> RhoResult<RhoIdentity> {
self.validate_handle(handle)?;
Ok(RhoIdentity {
id: self.identity_id(handle)?,
provider: self.provider().to_string(),
handle: handle.to_string(),
display_name: None,
keys: Vec::new(),
proofs: Vec::new(),
discovery: Vec::new(),
})
}
}
pub fn parse_identity_id(identity_id: &str) -> RhoResult<IdentityLocator> {
let Some(rest) = identity_id.strip_prefix("rho://id/") else {
return Err(format!("identity id must start with rho://id/: {identity_id}").into());
};
let Some((provider, handle)) = rest.split_once('/') else {
return Err(format!("identity id must include provider and handle: {identity_id}").into());
};
if provider.is_empty() || handle.is_empty() || handle.contains('/') {
return Err(format!("invalid identity id: {identity_id}").into());
}
validate_relative_safe_path(provider)?;
validate_relative_safe_path(handle)?;
Ok(IdentityLocator {
provider: provider.to_string(),
handle: handle.to_string(),
})
}
pub fn identity_inbox_relative_path(identity_id: &str) -> RhoResult<PathBuf> {
let locator = parse_identity_id(identity_id)?;
Ok(PathBuf::from("id")
.join(locator.provider)
.join(locator.handle))
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Copy)]
struct TestProvider;
impl IdentityProvider for TestProvider {
fn provider(&self) -> &'static str {
"test"
}
fn validate_handle(&self, handle: &str) -> RhoResult<()> {
if handle.is_empty() || handle.contains('/') {
return Err("invalid handle".into());
}
Ok(())
}
}
#[test]
fn builds_provider_neutral_identity() {
let identity = TestProvider.identity("alice").unwrap();
assert_eq!(identity.id, "rho://id/test/alice");
assert_eq!(identity.provider, "test");
assert!(identity.keys.is_empty());
}
#[test]
fn parses_identity_locator() {
let locator = parse_identity_id("rho://id/nostr/npub1abc").unwrap();
assert_eq!(locator.provider, "nostr");
assert_eq!(locator.handle, "npub1abc");
}
}