qualia_client_core/
social_peers.rs1use std::fs;
16use std::path::PathBuf;
17
18use crate::state::app_meta_dir;
19
20#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
26pub struct SocialPeer {
27 pub did: String,
29 pub display_name: String,
31 pub wireguard_pubkey_hex: String,
33 pub overlay_addr: String,
35 pub endpoint: Option<String>,
37 pub relation_type: String,
40 pub added_at: u64,
42 pub active: bool,
45 #[serde(default)]
49 pub envelope_pubkey_hex: Option<String>,
50}
51
52pub fn upsert(peers: &mut Vec<SocialPeer>, peer: SocialPeer) {
61 if let Some(slot) = peers.iter_mut().find(|p| p.did == peer.did) {
62 *slot = peer;
63 } else {
64 peers.push(peer);
65 }
66}
67
68pub fn find<'a>(peers: &'a [SocialPeer], did: &str) -> Option<&'a SocialPeer> {
70 peers.iter().find(|p| p.did == did)
71}
72
73fn peers_path() -> PathBuf {
78 app_meta_dir().join("social_peers.json")
79}
80
81fn save_peers(peers: &[SocialPeer]) -> Result<(), String> {
82 let path = peers_path();
83 if let Some(parent) = path.parent() {
84 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
85 }
86 let text = serde_json::to_string_pretty(peers).map_err(|e| e.to_string())?;
87 fs::write(path, text).map_err(|e| e.to_string())
88}
89
90pub fn list_peers() -> Vec<SocialPeer> {
93 fs::read_to_string(peers_path())
94 .ok()
95 .and_then(|t| serde_json::from_str(&t).ok())
96 .unwrap_or_default()
97}
98
99pub fn register_peer(peer: SocialPeer) -> Result<(), String> {
101 let mut peers = list_peers();
102 upsert(&mut peers, peer);
103 save_peers(&peers)
104}
105
106pub fn set_peer_active(did: &str, active: bool) -> Result<(), String> {
110 let mut peers = list_peers();
111 match peers.iter_mut().find(|p| p.did == did) {
112 Some(p) => p.active = active,
113 None => return Err(format!("no peer with did {did}")),
114 }
115 save_peers(&peers)
116}
117
118pub fn remove_peer(did: &str) -> Result<(), String> {
122 let mut peers = list_peers();
123 peers.retain(|p| p.did != did);
124 save_peers(&peers)
125}
126
127pub fn set_peer_envelope_key(did: &str, pubkey_hex: &str) -> Result<(), String> {
129 let mut peers = list_peers();
130 match peers.iter_mut().find(|p| p.did == did) {
131 Some(p) => p.envelope_pubkey_hex = Some(pubkey_hex.to_string()),
132 None => return Err(format!("no peer with did {did}")),
133 }
134 save_peers(&peers)
135}
136
137pub fn set_peer_endpoint(did: &str, endpoint: Option<&str>) -> Result<(), String> {
140 let mut peers = list_peers();
141 match peers.iter_mut().find(|p| p.did == did) {
142 Some(p) => {
143 p.endpoint = endpoint
144 .map(|e| e.trim().to_string())
145 .filter(|e| !e.is_empty());
146 }
147 None => return Err(format!("no peer with did {did}")),
148 }
149 save_peers(&peers)
150}
151
152pub fn resolve_envelope_keys(peers: &[SocialPeer], dids: &[String]) -> Vec<(String, String)> {
156 dids.iter()
157 .filter_map(|did| {
158 find(peers, did)
159 .and_then(|p| p.envelope_pubkey_hex.clone())
160 .map(|pk| (did.clone(), pk))
161 })
162 .collect()
163}
164
165#[cfg(test)]
171mod tests {
172 use super::*;
173
174 fn peer(did: &str, name: &str) -> SocialPeer {
175 SocialPeer {
176 did: did.to_string(),
177 display_name: name.to_string(),
178 wireguard_pubkey_hex: "aa".repeat(32),
179 overlay_addr: "10.44.0.2".to_string(),
180 endpoint: Some("203.0.113.5:51820".to_string()),
181 relation_type: "collaboration".to_string(),
182 added_at: 1_700_000_000,
183 active: true,
184 envelope_pubkey_hex: None,
185 }
186 }
187
188 #[test]
189 fn upsert_new_did_appends() {
190 let mut peers = vec![peer("did:key:alice", "Alice")];
191 upsert(&mut peers, peer("did:key:bob", "Bob"));
192
193 assert_eq!(peers.len(), 2);
194 assert_eq!(peers[0].did, "did:key:alice");
195 assert_eq!(peers[1].did, "did:key:bob");
196 }
197
198 #[test]
199 fn resolve_envelope_keys_returns_only_peers_with_a_published_key() {
200 let mut alice = peer("did:key:alice", "Alice");
201 alice.envelope_pubkey_hex = Some("ab".repeat(32));
202 let bob = peer("did:key:bob", "Bob"); let peers = vec![alice, bob];
204 let resolved = resolve_envelope_keys(
205 &peers,
206 &[
207 "did:key:alice".to_string(),
208 "did:key:bob".to_string(),
209 "did:key:carol".to_string(),
210 ],
211 );
212 assert_eq!(
214 resolved,
215 vec![("did:key:alice".to_string(), "ab".repeat(32))]
216 );
217 }
218
219 #[test]
220 fn upsert_existing_did_replaces_in_place() {
221 let mut peers = vec![peer("did:key:alice", "Alice"), peer("did:key:bob", "Bob")];
222
223 let mut updated = peer("did:key:alice", "Alice (renamed)");
224 updated.overlay_addr = "10.44.0.9".to_string();
225 updated.active = false;
226 upsert(&mut peers, updated);
227
228 assert_eq!(peers.len(), 2);
230 assert_eq!(peers[0].did, "did:key:alice");
232 assert_eq!(peers[1].did, "did:key:bob");
233 assert_eq!(peers[0].display_name, "Alice (renamed)");
235 assert_eq!(peers[0].overlay_addr, "10.44.0.9");
236 assert!(!peers[0].active);
237 }
238
239 #[test]
240 fn find_returns_peer_or_none() {
241 let peers = vec![peer("did:key:alice", "Alice"), peer("did:key:bob", "Bob")];
242
243 let found = find(&peers, "did:key:bob");
244 assert!(found.is_some());
245 assert_eq!(found.unwrap().display_name, "Bob");
246
247 assert!(find(&peers, "did:key:carol").is_none());
248 }
249}