medea_control_api_proto/control/
member.rs1use std::{
4 collections::HashMap,
5 fmt::Write as _,
6 hash::{Hash, Hasher},
7 time::Duration,
8};
9
10use derive_more::with_trait::{AsRef, Display, Error, From, FromStr, Into};
11use ref_cast::RefCast;
12use secrecy::{ExposeSecret as _, SecretString};
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize, Serializer};
15use url::Url;
16
17use super::{Pipeline, endpoint, room};
18
19#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct Member {
26 pub id: Id,
30
31 pub spec: Spec,
35}
36
37#[derive(Clone, Debug, Eq, PartialEq)]
41#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
42pub struct Spec {
43 pub pipeline: Pipeline<endpoint::Id, endpoint::Spec>,
47
48 pub credentials: Option<Credentials>,
54
55 pub on_join: Option<Url>,
60
61 pub on_leave: Option<Url>,
66
67 #[cfg_attr(feature = "serde", serde(default, with = "humantime_serde"))]
74 pub idle_timeout: Option<Duration>,
75
76 #[cfg_attr(feature = "serde", serde(default, with = "humantime_serde"))]
82 pub reconnect_timeout: Option<Duration>,
83
84 #[cfg_attr(feature = "serde", serde(default, with = "humantime_serde"))]
92 pub ping_interval: Option<Duration>,
93}
94
95#[derive(
99 AsRef,
100 Clone,
101 Debug,
102 Display,
103 Eq,
104 From,
105 Hash,
106 Into,
107 Ord,
108 PartialEq,
109 PartialOrd,
110 RefCast,
111)]
112#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
113#[cfg_attr(feature = "serde", serde(transparent))]
114#[from(&str, String)]
115#[into(String)]
116#[repr(transparent)]
117pub struct Id(Box<str>);
118
119#[cfg(feature = "client-api")]
120impl From<medea_client_api_proto::MemberId> for Id {
121 fn from(id: medea_client_api_proto::MemberId) -> Self {
122 id.0.into()
123 }
124}
125
126#[derive(Clone, Debug)]
131pub struct Sid {
132 pub public_url: PublicUrl,
137
138 pub room_id: room::Id,
142
143 pub member_id: Id,
147
148 pub creds: Option<PlainCredentials>,
150}
151
152impl Sid {
153 #[must_use]
159 pub fn to_uri_string(&self) -> String {
160 let mut sid =
161 format!("{}/{}/{}", self.public_url, self.room_id, self.member_id);
162 if let Some(plain) = &self.creds {
163 #[expect(clippy::expect_used, reason = "never fails")]
164 write!(sid, "?token={}", plain.expose_str())
165 .expect("writing to `String` never fails");
166 }
167 sid
168 }
169}
170
171impl FromStr for Sid {
172 type Err = ParseSidError;
173
174 fn from_str(s: &str) -> Result<Self, Self::Err> {
175 let mut url = Url::parse(s)
176 .map_err(|e| ParseSidError::InvalidUrl(s.into(), e))?;
177
178 let creds = url.query_pairs().find_map(|(k, v)| {
179 (k.as_ref() == "token").then(|| v.as_ref().into())
180 });
181
182 url.set_fragment(None);
183 url.set_query(None);
184
185 let err_missing = || ParseSidError::MissingPaths(s.into());
186 let mut segments = url.path_segments().ok_or_else(err_missing)?.rev();
187 let member_id = segments.next().ok_or_else(err_missing)?.into();
188 let room_id = segments.next().ok_or_else(err_missing)?.into();
189
190 if let Ok(mut path) = url.path_segments_mut() {
192 _ = path.pop().pop();
193 }
194
195 Ok(Self { public_url: url.into(), room_id, member_id, creds })
196 }
197}
198
199#[derive(Debug, Display, Error)]
201pub enum ParseSidError {
202 #[display("Missing paths in URI: {_0}")]
208 MissingPaths(#[error(not(source))] Box<str>),
209
210 #[display("Cannot parse provided URI `{_0}`: {_1}")]
214 InvalidUrl(Box<str>, #[error(source)] url::ParseError),
215}
216
217pub type Sids = HashMap<Id, Sid>;
222
223#[derive(
233 AsRef,
234 Clone,
235 Debug,
236 Display,
237 Eq,
238 From,
239 FromStr,
240 Hash,
241 Into,
242 Ord,
243 PartialEq,
244 PartialOrd,
245)]
246#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
247#[cfg_attr(feature = "serde", serde(transparent))]
248pub struct PublicUrl(Url);
249
250#[derive(Clone, Debug, Eq, From, PartialEq)]
256#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
257#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
258pub enum Credentials {
259 #[from(ignore)]
267 Hash(Box<str>),
268
269 Plain(PlainCredentials),
271}
272
273impl Credentials {
274 #[must_use]
276 pub fn random() -> Self {
277 use rand::{Rng as _, distr::Alphanumeric};
278
279 Self::Plain(
280 rand::rng()
281 .sample_iter(&Alphanumeric)
282 .take(32)
283 .map(char::from)
284 .collect::<String>()
285 .into(),
286 )
287 }
288}
289
290#[derive(AsRef, Clone, Debug)]
292#[cfg_attr(feature = "serde", derive(Deserialize))]
293#[cfg_attr(feature = "serde", serde(transparent))]
294pub struct PlainCredentials(SecretString);
295
296impl PlainCredentials {
297 #[must_use]
299 pub fn expose_str(&self) -> &str {
300 self.0.expose_secret()
301 }
302}
303
304impl<T> From<T> for PlainCredentials
305where
306 T: Into<String>,
307{
308 fn from(value: T) -> Self {
309 Self(value.into().into())
310 }
311}
312
313impl Hash for PlainCredentials {
314 fn hash<H: Hasher>(&self, state: &mut H) {
315 self.expose_str().hash(state);
316 }
317}
318
319impl Eq for PlainCredentials {}
320
321impl PartialEq for PlainCredentials {
322 fn eq(&self, other: &Self) -> bool {
323 use subtle::ConstantTimeEq as _;
324
325 self.expose_str().as_bytes().ct_eq(other.expose_str().as_bytes()).into()
326 }
327}
328
329#[cfg(feature = "serde")]
330impl Serialize for PlainCredentials {
331 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
332 where
333 S: Serializer,
334 {
335 self.0.expose_secret().serialize(serializer)
336 }
337}