1use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
22use base64::Engine;
23use serde::{Deserialize, Serialize};
24
25use crate::error::{HuddleError, Result};
26
27pub const INVITE_PREFIX: &str = "huddle://invite#";
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct InviteLink {
31 pub v: u32,
34 pub host_multiaddr: String,
35 pub fingerprint: String,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub room: Option<InviteRoom>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41pub struct InviteRoom {
42 pub id: String,
43 pub name: String,
44 pub encrypted: bool,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub salt_b64: Option<String>,
50 pub creator_fingerprint: String,
51 #[serde(default)]
52 pub owner_fingerprints: Vec<String>,
53}
54
55pub fn encode(invite: &InviteLink) -> Result<String> {
57 let json = serde_json::to_vec(invite)
58 .map_err(|e| HuddleError::Other(format!("invite encode: {e}")))?;
59 Ok(format!("{}{}", INVITE_PREFIX, B64.encode(&json)))
60}
61
62pub fn decode(url: &str) -> Result<InviteLink> {
64 let body = url
65 .strip_prefix(INVITE_PREFIX)
66 .ok_or_else(|| HuddleError::Other("not a huddle invite link".into()))?;
67 let json = B64
68 .decode(body.trim())
69 .map_err(|e| HuddleError::Other(format!("bad base64: {e}")))?;
70 let invite: InviteLink = serde_json::from_slice(&json)
71 .map_err(|e| HuddleError::Other(format!("bad invite json: {e}")))?;
72 if invite.v != 1 {
73 return Err(HuddleError::Other(format!(
74 "unsupported invite version: {}",
75 invite.v
76 )));
77 }
78 Ok(invite)
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn round_trip_peer_only() {
87 let inv = InviteLink {
88 v: 1,
89 host_multiaddr: "/ip4/1.2.3.4/tcp/9000/p2p/12D3KooW...".into(),
90 fingerprint: "abcd-1234-efef-5678-9090-1111".into(),
91 room: None,
92 };
93 let url = encode(&inv).unwrap();
94 assert!(url.starts_with(INVITE_PREFIX));
95 let back = decode(&url).unwrap();
96 assert_eq!(back, inv);
97 }
98
99 #[test]
100 fn round_trip_with_room() {
101 let inv = InviteLink {
102 v: 1,
103 host_multiaddr: "/ip4/1.2.3.4/tcp/9000/p2p/12D3KooW...".into(),
104 fingerprint: "abcd-1234-efef-5678-9090-1111".into(),
105 room: Some(InviteRoom {
106 id: "rid-x".into(),
107 name: "Project Alpha".into(),
108 encrypted: true,
109 salt_b64: Some("AAAAAA==".into()),
110 creator_fingerprint: "abcd-1234-efef-5678-9090-1111".into(),
111 owner_fingerprints: vec!["abcd-1234-efef-5678-9090-1111".into()],
112 }),
113 };
114 let url = encode(&inv).unwrap();
115 let back = decode(&url).unwrap();
116 assert_eq!(back, inv);
117 }
118
119 #[test]
120 fn decode_unknown_version_rejects() {
121 let bad = serde_json::json!({
123 "v": 99,
124 "host_multiaddr": "/ip4/1.1.1.1/tcp/1",
125 "fingerprint": "x"
126 });
127 let url = format!("{}{}", INVITE_PREFIX, B64.encode(bad.to_string()));
128 assert!(decode(&url).is_err());
129 }
130
131 #[test]
132 fn decode_not_huddle_url_rejects() {
133 assert!(decode("https://example.com/invite").is_err());
134 }
135}