use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
use base64::Engine;
use serde::{Deserialize, Serialize};
use crate::error::{HuddleError, Result};
pub const INVITE_PREFIX: &str = "huddle://invite#";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InviteLink {
pub v: u32,
pub host_multiaddr: String,
pub fingerprint: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub room: Option<InviteRoom>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InviteRoom {
pub id: String,
pub name: String,
pub encrypted: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub salt_b64: Option<String>,
pub creator_fingerprint: String,
#[serde(default)]
pub owner_fingerprints: Vec<String>,
}
pub fn encode(invite: &InviteLink) -> Result<String> {
let json = serde_json::to_vec(invite)
.map_err(|e| HuddleError::Other(format!("invite encode: {e}")))?;
Ok(format!("{}{}", INVITE_PREFIX, B64.encode(&json)))
}
pub fn decode(url: &str) -> Result<InviteLink> {
let body = url
.strip_prefix(INVITE_PREFIX)
.ok_or_else(|| HuddleError::Other("not a huddle invite link".into()))?;
let json = B64
.decode(body.trim())
.map_err(|e| HuddleError::Other(format!("bad base64: {e}")))?;
let invite: InviteLink = serde_json::from_slice(&json)
.map_err(|e| HuddleError::Other(format!("bad invite json: {e}")))?;
if invite.v != 1 {
return Err(HuddleError::Other(format!(
"unsupported invite version: {}",
invite.v
)));
}
Ok(invite)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_peer_only() {
let inv = InviteLink {
v: 1,
host_multiaddr: "/ip4/1.2.3.4/tcp/9000/p2p/12D3KooW...".into(),
fingerprint: "abcd-1234-efef-5678-9090-1111".into(),
room: None,
};
let url = encode(&inv).unwrap();
assert!(url.starts_with(INVITE_PREFIX));
let back = decode(&url).unwrap();
assert_eq!(back, inv);
}
#[test]
fn round_trip_with_room() {
let inv = InviteLink {
v: 1,
host_multiaddr: "/ip4/1.2.3.4/tcp/9000/p2p/12D3KooW...".into(),
fingerprint: "abcd-1234-efef-5678-9090-1111".into(),
room: Some(InviteRoom {
id: "rid-x".into(),
name: "Project Alpha".into(),
encrypted: true,
salt_b64: Some("AAAAAA==".into()),
creator_fingerprint: "abcd-1234-efef-5678-9090-1111".into(),
owner_fingerprints: vec!["abcd-1234-efef-5678-9090-1111".into()],
}),
};
let url = encode(&inv).unwrap();
let back = decode(&url).unwrap();
assert_eq!(back, inv);
}
#[test]
fn decode_unknown_version_rejects() {
let bad = serde_json::json!({
"v": 99,
"host_multiaddr": "/ip4/1.1.1.1/tcp/1",
"fingerprint": "x"
});
let url = format!("{}{}", INVITE_PREFIX, B64.encode(bad.to_string()));
assert!(decode(&url).is_err());
}
#[test]
fn decode_not_huddle_url_rejects() {
assert!(decode("https://example.com/invite").is_err());
}
}