wowlab_centrifuge/
presence.rs1use std::time::Duration;
2
3use serde_json::json;
4use wowlab_common::NodePublicKey;
5use wowlab_types::{constants::HTTP_CONNECT_TIMEOUT_SECS, sensitive::Sensitive, sim::FastSet};
6
7use crate::Error;
8
9#[derive(Clone, Debug)]
11pub struct Presence {
13 http: reqwest::Client,
14 url: String,
15 api_key: Sensitive<String>,
16}
17
18impl Presence {
19 #[must_use]
21 pub fn new(url: impl Into<String>, api_key: impl Into<String>) -> Self {
22 Self {
23 http: reqwest::Client::builder()
24 .timeout(Duration::from_secs(HTTP_CONNECT_TIMEOUT_SECS))
25 .build()
26 .unwrap_or_else(|_| reqwest::Client::new()),
27 url: url.into().trim_end_matches('/').to_string(),
28 api_key: Sensitive::new(api_key.into()),
29 }
30 }
31
32 pub async fn get_online(&self, channel: &str) -> Result<FastSet<NodePublicKey>, Error> {
37 let resp = self
38 .http
39 .post(format!("{}/api/presence", self.url))
40 .header("X-API-Key", self.api_key.expose())
41 .json(&json!({ "channel": channel }))
42 .send()
43 .await?;
44 if !resp.status().is_success() {
47 return Err(Error::http_status(resp.status()));
48 }
49
50 let body: serde_json::Value = resp.json().await?;
51
52 if let Some(error) = body.get("error") {
53 let code = error
54 .get("code")
55 .and_then(serde_json::Value::as_u64)
56 .and_then(|code| u32::try_from(code).ok())
57 .unwrap_or(0);
58 let message = error
59 .get("message")
60 .and_then(|m| m.as_str())
61 .unwrap_or("unknown")
62 .to_string();
63
64 return Err(Error::server(code, message, false));
65 }
66
67 let presence = body
68 .get("result")
69 .and_then(|r| r.get("presence"))
70 .and_then(|p| p.as_object());
71 let keys = presence
72 .map(|obj| {
73 obj.values()
74 .filter_map(|v| v.get("user").and_then(|u| u.as_str()))
75 .filter_map(|s| s.parse::<NodePublicKey>().ok())
76 .collect()
77 })
78 .unwrap_or_default();
79
80 Ok(keys)
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use googletest::prelude::*;
87
88 use super::Presence;
89
90 #[gtest]
91 fn debug_redacts_api_key() -> Result<()> {
92 let presence = Presence::new("https://centrifugo.example.com", "presence-secret");
93 let debug = format!("{presence:?}");
94
95 verify_that!(debug.as_str(), contains_substring("[REDACTED]"))?;
96
97 verify_that!(debug.as_str(), not(contains_substring("presence-secret")))
98 }
99}