1use std::path::PathBuf;
2
3use crate::{RhoResult, validate_relative_safe_path};
4use serde::{Deserialize, Serialize};
5
6pub mod github;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9pub struct IdentityLocator {
10 pub provider: String,
11 pub handle: String,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15pub struct RhoIdentity {
16 pub id: String,
17 pub provider: String,
18 pub handle: String,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub display_name: Option<String>,
21 #[serde(default, skip_serializing_if = "Vec::is_empty")]
22 pub keys: Vec<IdentityKey>,
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
24 pub proofs: Vec<IdentityProof>,
25 #[serde(default, skip_serializing_if = "Vec::is_empty")]
26 pub discovery: Vec<DiscoveryLocator>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct IdentityKey {
31 pub id: String,
32 pub kind: IdentityKeyKind,
33 pub algorithm: String,
34 pub public_key: String,
35 pub fingerprint: String,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "snake_case")]
40pub enum IdentityKeyKind {
41 Signing,
42 Encryption,
43 Transport,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47pub struct IdentityProof {
48 pub kind: String,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub provider_url: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub claim: Option<String>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub proof_url: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub verified_at: Option<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60pub struct DiscoveryLocator {
61 pub kind: String,
62 pub url: String,
63}
64
65pub trait IdentityProvider {
66 fn provider(&self) -> &'static str;
67 fn validate_handle(&self, handle: &str) -> RhoResult<()>;
68
69 fn identity_id(&self, handle: &str) -> RhoResult<String> {
70 self.validate_handle(handle)?;
71 Ok(format!("rho://id/{}/{}", self.provider(), handle))
72 }
73
74 fn handle_from_identity_id(&self, identity_id: &str) -> RhoResult<String> {
75 let locator = parse_identity_id(identity_id)?;
76 if locator.provider != self.provider() {
77 return Err(format!(
78 "unsupported identity provider for {}: {}",
79 self.provider(),
80 identity_id
81 )
82 .into());
83 }
84 self.validate_handle(&locator.handle)?;
85 Ok(locator.handle)
86 }
87
88 fn identity(&self, handle: &str) -> RhoResult<RhoIdentity> {
89 self.validate_handle(handle)?;
90 Ok(RhoIdentity {
91 id: self.identity_id(handle)?,
92 provider: self.provider().to_string(),
93 handle: handle.to_string(),
94 display_name: None,
95 keys: Vec::new(),
96 proofs: Vec::new(),
97 discovery: Vec::new(),
98 })
99 }
100}
101
102pub fn parse_identity_id(identity_id: &str) -> RhoResult<IdentityLocator> {
103 let Some(rest) = identity_id.strip_prefix("rho://id/") else {
104 return Err(format!("identity id must start with rho://id/: {identity_id}").into());
105 };
106 let Some((provider, handle)) = rest.split_once('/') else {
107 return Err(format!("identity id must include provider and handle: {identity_id}").into());
108 };
109 if provider.is_empty() || handle.is_empty() || handle.contains('/') {
110 return Err(format!("invalid identity id: {identity_id}").into());
111 }
112 validate_relative_safe_path(provider)?;
113 validate_relative_safe_path(handle)?;
114 Ok(IdentityLocator {
115 provider: provider.to_string(),
116 handle: handle.to_string(),
117 })
118}
119
120pub fn identity_inbox_relative_path(identity_id: &str) -> RhoResult<PathBuf> {
121 let locator = parse_identity_id(identity_id)?;
122 Ok(PathBuf::from("id")
123 .join(locator.provider)
124 .join(locator.handle))
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[derive(Debug, Clone, Copy)]
132 struct TestProvider;
133
134 impl IdentityProvider for TestProvider {
135 fn provider(&self) -> &'static str {
136 "test"
137 }
138
139 fn validate_handle(&self, handle: &str) -> RhoResult<()> {
140 if handle.is_empty() || handle.contains('/') {
141 return Err("invalid handle".into());
142 }
143 Ok(())
144 }
145 }
146
147 #[test]
148 fn builds_provider_neutral_identity() {
149 let identity = TestProvider.identity("alice").unwrap();
150 assert_eq!(identity.id, "rho://id/test/alice");
151 assert_eq!(identity.provider, "test");
152 assert!(identity.keys.is_empty());
153 }
154
155 #[test]
156 fn parses_identity_locator() {
157 let locator = parse_identity_id("rho://id/nostr/npub1abc").unwrap();
158 assert_eq!(locator.provider, "nostr");
159 assert_eq!(locator.handle, "npub1abc");
160 }
161}