Skip to main content

rift_core/
invite.rs

1//! Invite encoding/decoding and helper utilities.
2//!
3//! Invites encapsulate enough metadata to bootstrap a session:
4//! - channel identity (name + optional password)
5//! - per-channel encryption key
6//! - known peers and ICE-style candidates
7
8use std::net::SocketAddr;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11use rand::rngs::OsRng;
12use rand::RngCore;
13use serde::{Deserialize, Serialize};
14
15use crate::CoreError;
16use base64::Engine;
17
18/// URI prefix for invites.
19const INVITE_PREFIX: &str = "rift://join/";
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Invite {
23    /// Human-readable channel name.
24    pub channel_name: String,
25    /// Optional channel password.
26    pub password: Option<String>,
27    /// Symmetric channel key used for message encryption bootstrap.
28    pub channel_key: [u8; 32],
29    /// Known peer endpoints supplied at invite creation time.
30    pub known_peers: Vec<SocketAddr>,
31    /// Candidate endpoints discovered by ICE-lite routines.
32    #[serde(default)]
33    pub candidates: Vec<SocketAddr>,
34    /// Optional relay endpoints (TURN or peer relay).
35    #[serde(default)]
36    pub relay_candidates: Vec<SocketAddr>,
37    /// Protocol version encoded into the invite.
38    pub version: u8,
39    /// Creation timestamp in seconds since epoch.
40    pub created_at: u64,
41}
42
43/// Create a new invite with a fresh channel key.
44pub fn generate_invite(
45    channel_name: &str,
46    password: Option<&str>,
47    known_peers: Vec<SocketAddr>,
48    candidates: Vec<SocketAddr>,
49) -> Invite {
50    let mut channel_key = [0u8; 32];
51    OsRng.fill_bytes(&mut channel_key);
52    Invite {
53        channel_name: channel_name.to_string(),
54        password: password.map(|s| s.to_string()),
55        channel_key,
56        known_peers,
57        candidates,
58        relay_candidates: Vec::new(),
59        version: 2,
60        created_at: now_timestamp(),
61    }
62}
63
64/// Encode the invite as a URL-safe string.
65pub fn encode_invite(invite: &Invite) -> String {
66    let bytes = bincode::serialize(invite).expect("serialize invite");
67    let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
68    format!("{}{}", INVITE_PREFIX, encoded)
69}
70
71/// Decode an invite string back to the structured form.
72pub fn decode_invite(url: &str) -> Result<Invite, CoreError> {
73    if !url.starts_with(INVITE_PREFIX) {
74        return Err(CoreError::InvalidInvite);
75    }
76    let payload = &url[INVITE_PREFIX.len()..];
77    let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
78        .decode(payload)
79        .map_err(|_| CoreError::InvalidInvite)?;
80    let invite: Invite = bincode::deserialize(&bytes)?;
81    Ok(invite)
82}
83
84/// Timestamp helper for invite creation.
85fn now_timestamp() -> u64 {
86    SystemTime::now()
87        .duration_since(UNIX_EPOCH)
88        .map(|d| d.as_secs())
89        .unwrap_or(0)
90}