Skip to main content

hanzo_client/apis/
meet_api.rs

1/*
2 * Hanzo Cloud API
3 *
4 * The Hanzo Cloud API as a customer calls it: every operation under /v1/ except the operator's admin product, relay routes, legacy spellings and capabilities still reached by flag. Tagged by product: the first path segment after /v1/.
5 *
6 * The version of the OpenAPI document: v1
7 * 
8 * Generated by: https://openapi-generator.tech
9 */
10
11
12use reqwest;
13use serde::{Deserialize, Serialize, de::Error as _};
14use crate::{apis::ResponseContent, models};
15use super::{Error, configuration, ContentType};
16
17
18/// struct for typed errors of method [`get_meet_health`]
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum GetMeetHealthError {
22    Status503(models::MeetHealth),
23    UnknownValue(serde_json::Value),
24}
25
26/// struct for typed errors of method [`get_meet_session`]
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(untagged)]
29pub enum GetMeetSessionError {
30    UnknownValue(serde_json::Value),
31}
32
33/// struct for typed errors of method [`meet_call`]
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(untagged)]
36pub enum MeetCallError {
37    UnknownValue(serde_json::Value),
38}
39
40/// struct for typed errors of method [`meet_record_read`]
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(untagged)]
43pub enum MeetRecordReadError {
44    UnknownValue(serde_json::Value),
45}
46
47/// struct for typed errors of method [`meet_record_start`]
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(untagged)]
50pub enum MeetRecordStartError {
51    UnknownValue(serde_json::Value),
52}
53
54/// struct for typed errors of method [`meet_record_stop`]
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(untagged)]
57pub enum MeetRecordStopError {
58    UnknownValue(serde_json::Value),
59}
60
61/// struct for typed errors of method [`post_meet_gettoken`]
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(untagged)]
64pub enum PostMeetGettokenError {
65    UnknownValue(serde_json::Value),
66}
67
68
69/// Health reports whether the office can mint join tokens.  It reports whether this deployment holds the LiveKit key pair it needs: ready:true with 200 when tokens can be minted, the SAME body with ready:false, status \"degraded\" and 503 when they cannot — so a probe and a dashboard both read the degraded state instead of someone grepping a boot log.  It takes no credential and is reachable on every public host, so it withholds both the reason and the signing key's name on purpose: ready is the whole dashboard fact, and the reason — which names the key file and the Secret — is written to the boot log where an operator already is.
70pub async fn get_meet_health(configuration: &configuration::Configuration, ) -> Result<models::MeetHealth, Error<GetMeetHealthError>> {
71
72    let uri_str = format!("{}/v1/meet/health", configuration.base_path);
73    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
74
75    if let Some(ref user_agent) = configuration.user_agent {
76        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
77    }
78    if let Some(ref token) = configuration.bearer_access_token {
79        req_builder = req_builder.bearer_auth(token.to_owned());
80    };
81
82    let req = req_builder.build()?;
83    let resp = configuration.client.execute(req).await?;
84
85    let status = resp.status();
86    let content_type = resp
87        .headers()
88        .get("content-type")
89        .and_then(|v| v.to_str().ok())
90        .unwrap_or("application/octet-stream");
91    let content_type = super::ContentType::from(content_type);
92
93    if !status.is_client_error() && !status.is_server_error() {
94        let content = resp.text().await?;
95        match content_type {
96            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
97            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::MeetHealth`"))),
98            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::MeetHealth`")))),
99        }
100    } else {
101        let content = resp.text().await?;
102        let entity: Option<GetMeetHealthError> = serde_json::from_str(&content).ok();
103        Err(Error::ResponseError(ResponseContent { status, content, entity }))
104    }
105}
106
107/// Answers the three facts the native lobby cannot know on its own: the identity a seat would be taken under, the LiveKit address the browser dials, and the workspaces this caller may open a room in.  It is the SAME decision getToken makes, asked before the room exists rather than after it is named. A room is bound to its tenant by its name's leading workspace segment, and only a workspace this answer lists will be admitted — so the lobby offers exactly what the mint would grant, and a person is never shown a room they would then be refused. Workspaces the caller holds only a guest role in are omitted for that reason.  An empty list is a real answer, not a fault: an IAM identity with no workspace has no room to open, and the lobby says so instead of failing.  `ws` is empty when this deployment has not been told where its media plane is (LIVEKIT_WS). Token minting is unaffected — the published office client supplies its own address — so this is a degraded native UI, not a degraded service, and the lobby refuses to dial rather than guessing a host.
108pub async fn get_meet_session(configuration: &configuration::Configuration, ) -> Result<(), Error<GetMeetSessionError>> {
109
110    let uri_str = format!("{}/v1/meet/session", configuration.base_path);
111    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
112
113    if let Some(ref user_agent) = configuration.user_agent {
114        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
115    }
116    if let Some(ref token) = configuration.bearer_access_token {
117        req_builder = req_builder.bearer_auth(token.to_owned());
118    };
119
120    let req = req_builder.build()?;
121    let resp = configuration.client.execute(req).await?;
122
123    let status = resp.status();
124
125    if !status.is_client_error() && !status.is_server_error() {
126        Ok(())
127    } else {
128        let content = resp.text().await?;
129        let entity: Option<GetMeetSessionError> = serde_json::from_str(&content).ok();
130        Err(Error::ResponseError(ResponseContent { status, content, entity }))
131    }
132}
133
134/// Answers where a room's call happens, for a caller who may join it.  It is the \"resolved at render\" half of HIP-0523 §12: a surface showing a channel asks for the room's call at the moment it draws one, rather than reading a media room name someone stored on the room. Nothing here is persisted and nothing is created — a media room begins existing when the first participant connects and stops when the last leaves, so there is no call to create and none to clean up.  AUTHORIZATION IS THE JOIN DECISION, unchanged and shared. It delegates to state.admits, the same function POST /v1/meet/getToken and all three recording operations admit on, so a caller who is told where a call is, is a caller who could have joined it. Answering the address to someone who cannot join would make this a workspace-membership oracle for anyone who can guess a room id.  It deliberately does NOT report whether a call is in progress. That is a fact the media server holds and this binary would have to ask for it over the network, which is a different decision with a different failure mode — and reporting \"nobody is in this call\" when the question could not be asked would be exactly the unknown-rendered-as-zero this surface refuses elsewhere.
135pub async fn meet_call(configuration: &configuration::Configuration, workspace: &str, room: &str) -> Result<models::Venue, Error<MeetCallError>> {
136    // add a prefix to parameters to efficiently prevent name collisions
137    let p_workspace = workspace;
138    let p_room = room;
139
140    let uri_str = format!("{}/v1/meet/call", configuration.base_path);
141    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
142
143    req_builder = req_builder.query(&[("workspace", &p_workspace.to_string())]);
144    req_builder = req_builder.query(&[("room", &p_room.to_string())]);
145    if let Some(ref user_agent) = configuration.user_agent {
146        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
147    }
148    if let Some(ref token) = configuration.bearer_access_token {
149        req_builder = req_builder.bearer_auth(token.to_owned());
150    };
151
152    let req = req_builder.build()?;
153    let resp = configuration.client.execute(req).await?;
154
155    let status = resp.status();
156    let content_type = resp
157        .headers()
158        .get("content-type")
159        .and_then(|v| v.to_str().ok())
160        .unwrap_or("application/octet-stream");
161    let content_type = super::ContentType::from(content_type);
162
163    if !status.is_client_error() && !status.is_server_error() {
164        let content = resp.text().await?;
165        match content_type {
166            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
167            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::Venue`"))),
168            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::Venue`")))),
169        }
170    } else {
171        let content = resp.text().await?;
172        let entity: Option<MeetCallError> = serde_json::from_str(&content).ok();
173        Err(Error::ResponseError(ResponseContent { status, content, entity }))
174    }
175}
176
177/// Answers what is being recorded in a room, and where the file went.  It reports the recording that is RUNNING, and once none is, the most recent one the media server still holds — with its final status and its object. That second case is the one that matters for finding a file: the answer to a start is the only other place the location appears, and a client that lost it, or a colleague who was not the one to press record, has nowhere else to look.  It is behind the same check as starting one: where a recording of a private conversation is kept is a fact about that conversation, so it is told to the people the room admits and to nobody else.
178pub async fn meet_record_read(configuration: &configuration::Configuration, room: &str) -> Result<models::Recording, Error<MeetRecordReadError>> {
179    // add a prefix to parameters to efficiently prevent name collisions
180    let p_room = room;
181
182    let uri_str = format!("{}/v1/meet/record", configuration.base_path);
183    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
184
185    req_builder = req_builder.query(&[("room", &p_room.to_string())]);
186    if let Some(ref user_agent) = configuration.user_agent {
187        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
188    }
189    if let Some(ref token) = configuration.bearer_access_token {
190        req_builder = req_builder.bearer_auth(token.to_owned());
191    };
192
193    let req = req_builder.build()?;
194    let resp = configuration.client.execute(req).await?;
195
196    let status = resp.status();
197    let content_type = resp
198        .headers()
199        .get("content-type")
200        .and_then(|v| v.to_str().ok())
201        .unwrap_or("application/octet-stream");
202    let content_type = super::ContentType::from(content_type);
203
204    if !status.is_client_error() && !status.is_server_error() {
205        let content = resp.text().await?;
206        match content_type {
207            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
208            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::Recording`"))),
209            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::Recording`")))),
210        }
211    } else {
212        let content = resp.text().await?;
213        let entity: Option<MeetRecordReadError> = serde_json::from_str(&content).ok();
214        Err(Error::ResponseError(ResponseContent { status, content, entity }))
215    }
216}
217
218/// Begins recording a room, or hands back the recording already running.  A recording is a durable artifact of a conversation, so only someone this room would admit may make one: the caller is authorized by the SAME decision /v1/meet/getToken makes about the same room, and refused with the same 401.  A SECOND START RETURNS THE FIRST rather than refusing it. There is at most one recording per room and this operation's job is to establish that there is one — which is already true when a colleague, or the caller's own double-click, started it a moment ago. The answer is the same shape either way, naming the recording that is actually running, so a client never has to tell the two cases apart to find the id.  A deployment with no media server address or no object store answers 503 naming which, because a recording that silently does not happen is worse than one that is refused. The reason reaches only a caller this room already admits.
219pub async fn meet_record_start(configuration: &configuration::Configuration, record_in: models::RecordIn) -> Result<models::Recording, Error<MeetRecordStartError>> {
220    // add a prefix to parameters to efficiently prevent name collisions
221    let p_record_in = record_in;
222
223    let uri_str = format!("{}/v1/meet/record", configuration.base_path);
224    let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
225
226    if let Some(ref user_agent) = configuration.user_agent {
227        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
228    }
229    if let Some(ref token) = configuration.bearer_access_token {
230        req_builder = req_builder.bearer_auth(token.to_owned());
231    };
232    req_builder = req_builder.json(&p_record_in);
233
234    let req = req_builder.build()?;
235    let resp = configuration.client.execute(req).await?;
236
237    let status = resp.status();
238    let content_type = resp
239        .headers()
240        .get("content-type")
241        .and_then(|v| v.to_str().ok())
242        .unwrap_or("application/octet-stream");
243    let content_type = super::ContentType::from(content_type);
244
245    if !status.is_client_error() && !status.is_server_error() {
246        let content = resp.text().await?;
247        match content_type {
248            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
249            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::Recording`"))),
250            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::Recording`")))),
251        }
252    } else {
253        let content = resp.text().await?;
254        let entity: Option<MeetRecordStartError> = serde_json::from_str(&content).ok();
255        Err(Error::ResponseError(ResponseContent { status, content, entity }))
256    }
257}
258
259/// Ends a room's recording — EVERY one of them.  Whoever the room admits may stop it, including someone who did not start it: a person being recorded has to be able to end it, and a rule that only the starter may stop would deny exactly that. Stopping is free — a caller made to pay to stop being recorded would be paying for the wrong thing.  200 MEANS THE ROOM IS NOT BEING RECORDED, and that is why this ends all of them rather than the first. \"At most one per room\" is an invariant this surface wants and cannot impose: reading the list and starting are two calls, and two replicas racing through that window both start. When the list comes back holding two, two is the truth — and ending one while answering 200 tells the person withdrawing consent that it stopped while a second worker keeps writing. A stop that cannot finish the job says so instead.  Stopping a room that is not being recorded is not an error. The answer names the room with no recording on it, which is the state the caller asked for.
260pub async fn meet_record_stop(configuration: &configuration::Configuration, room: &str) -> Result<models::Recording, Error<MeetRecordStopError>> {
261    // add a prefix to parameters to efficiently prevent name collisions
262    let p_room = room;
263
264    let uri_str = format!("{}/v1/meet/record", configuration.base_path);
265    let mut req_builder = configuration.client.request(reqwest::Method::DELETE, &uri_str);
266
267    req_builder = req_builder.query(&[("room", &p_room.to_string())]);
268    if let Some(ref user_agent) = configuration.user_agent {
269        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
270    }
271    if let Some(ref token) = configuration.bearer_access_token {
272        req_builder = req_builder.bearer_auth(token.to_owned());
273    };
274
275    let req = req_builder.build()?;
276    let resp = configuration.client.execute(req).await?;
277
278    let status = resp.status();
279    let content_type = resp
280        .headers()
281        .get("content-type")
282        .and_then(|v| v.to_str().ok())
283        .unwrap_or("application/octet-stream");
284    let content_type = super::ContentType::from(content_type);
285
286    if !status.is_client_error() && !status.is_server_error() {
287        let content = resp.text().await?;
288        match content_type {
289            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
290            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::Recording`"))),
291            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::Recording`")))),
292        }
293    } else {
294        let content = resp.text().await?;
295        let entity: Option<MeetRecordStopError> = serde_json::from_str(&content).ok();
296        Err(Error::ResponseError(ResponseContent { status, content, entity }))
297    }
298}
299
300/// Answers with a LiveKit join token for exactly the room named in the body. The body is the RAW token as text/plain — one opaque string, not JSON and not wrapped in an envelope, which is what the office client reads.  The caller presents its workspace session as a Bearer. Every clause is a refusal: the session must verify, its SIGNED workspace claim must equal the room's leading name segment — rooms are named `<workspace>_<room>_<id>`, and that prefix is the only thing binding a room to a tenant — and the session must carry a privileged workspace role, so a guest is refused rather than seated.  The participant identity is the SESSION'S, never the body's. `_id` is accepted for compatibility with the published client bundle and deliberately ignored: LiveKit treats the identity as unique and ejects a duplicate, so honouring a caller-chosen one would let anyone in a workspace kick out a colleague and impersonate them. `participantName` is a display name only.  An unconfigured deployment answers 503 under its own name rather than 404, and the refusal states only that the office is unconfigured — the reason names key material and stays in the boot log.
301pub async fn post_meet_gettoken(configuration: &configuration::Configuration, ) -> Result<(), Error<PostMeetGettokenError>> {
302
303    let uri_str = format!("{}/v1/meet/getToken", configuration.base_path);
304    let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
305
306    if let Some(ref user_agent) = configuration.user_agent {
307        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
308    }
309    if let Some(ref token) = configuration.bearer_access_token {
310        req_builder = req_builder.bearer_auth(token.to_owned());
311    };
312
313    let req = req_builder.build()?;
314    let resp = configuration.client.execute(req).await?;
315
316    let status = resp.status();
317
318    if !status.is_client_error() && !status.is_server_error() {
319        Ok(())
320    } else {
321        let content = resp.text().await?;
322        let entity: Option<PostMeetGettokenError> = serde_json::from_str(&content).ok();
323        Err(Error::ResponseError(ResponseContent { status, content, entity }))
324    }
325}
326