1pub mod proto;
  2pub mod token;
  3
  4use anyhow::Result;
  5use async_trait::async_trait;
  6use prost::Message;
  7use reqwest::header::CONTENT_TYPE;
  8use std::{future::Future, sync::Arc, time::Duration};
  9
 10#[async_trait]
 11pub trait Client: Send + Sync {
 12    fn url(&self) -> &str;
 13    async fn create_room(&self, name: String) -> Result<()>;
 14    async fn delete_room(&self, name: String) -> Result<()>;
 15    async fn remove_participant(&self, room: String, identity: String) -> Result<()>;
 16    async fn update_participant(
 17        &self,
 18        room: String,
 19        identity: String,
 20        permission: proto::ParticipantPermission,
 21    ) -> Result<()>;
 22    fn room_token(&self, room: &str, identity: &str) -> Result<String>;
 23    fn guest_token(&self, room: &str, identity: &str) -> Result<String>;
 24}
 25
 26pub struct LiveKitParticipantUpdate {}
 27
 28#[derive(Clone)]
 29pub struct LiveKitClient {
 30    http: reqwest::Client,
 31    url: Arc<str>,
 32    key: Arc<str>,
 33    secret: Arc<str>,
 34}
 35
 36impl LiveKitClient {
 37    pub fn new(mut url: String, key: String, secret: String) -> Self {
 38        if url.ends_with('/') {
 39            url.pop();
 40        }
 41
 42        Self {
 43            http: reqwest::ClientBuilder::new()
 44                .timeout(Duration::from_secs(5))
 45                .build()
 46                .unwrap(),
 47            url: url.into(),
 48            key: key.into(),
 49            secret: secret.into(),
 50        }
 51    }
 52
 53    fn request<Req, Res>(
 54        &self,
 55        path: &str,
 56        grant: token::VideoGrant,
 57        body: Req,
 58    ) -> impl Future<Output = Result<Res>>
 59    where
 60        Req: Message,
 61        Res: Default + Message,
 62    {
 63        let client = self.http.clone();
 64        let token = token::create(&self.key, &self.secret, None, grant);
 65        let url = format!("{}/{}", self.url, path);
 66        log::info!("Request {}: {:?}", url, body);
 67        async move {
 68            let token = token?;
 69            let response = client
 70                .post(&url)
 71                .header(CONTENT_TYPE, "application/protobuf")
 72                .bearer_auth(token)
 73                .body(body.encode_to_vec())
 74                .send()
 75                .await?;
 76
 77            if response.status().is_success() {
 78                log::info!("Response {}: {:?}", url, response.status());
 79                Ok(Res::decode(response.bytes().await?)?)
 80            } else {
 81                log::error!("Response {}: {:?}", url, response.status());
 82                anyhow::bail!(
 83                    "POST {} failed with status code {:?}, {:?}",
 84                    url,
 85                    response.status(),
 86                    response.text().await
 87                );
 88            }
 89        }
 90    }
 91}
 92
 93#[async_trait]
 94impl Client for LiveKitClient {
 95    fn url(&self) -> &str {
 96        &self.url
 97    }
 98
 99    async fn create_room(&self, name: String) -> Result<()> {
100        let _: proto::Room = self
101            .request(
102                "twirp/livekit.RoomService/CreateRoom",
103                token::VideoGrant {
104                    room_create: Some(true),
105                    ..Default::default()
106                },
107                proto::CreateRoomRequest {
108                    name,
109                    ..Default::default()
110                },
111            )
112            .await?;
113        Ok(())
114    }
115
116    async fn delete_room(&self, name: String) -> Result<()> {
117        let _: proto::DeleteRoomResponse = self
118            .request(
119                "twirp/livekit.RoomService/DeleteRoom",
120                token::VideoGrant {
121                    room_create: Some(true),
122                    ..Default::default()
123                },
124                proto::DeleteRoomRequest { room: name },
125            )
126            .await?;
127        Ok(())
128    }
129
130    async fn remove_participant(&self, room: String, identity: String) -> Result<()> {
131        let _: proto::RemoveParticipantResponse = self
132            .request(
133                "twirp/livekit.RoomService/RemoveParticipant",
134                token::VideoGrant::to_admin(&room),
135                proto::RoomParticipantIdentity {
136                    room: room.clone(),
137                    identity,
138                },
139            )
140            .await?;
141        Ok(())
142    }
143
144    async fn update_participant(
145        &self,
146        room: String,
147        identity: String,
148        permission: proto::ParticipantPermission,
149    ) -> Result<()> {
150        let _: proto::ParticipantInfo = self
151            .request(
152                "twirp/livekit.RoomService/UpdateParticipant",
153                token::VideoGrant::to_admin(&room),
154                proto::UpdateParticipantRequest {
155                    room: room.clone(),
156                    identity,
157                    metadata: "".to_string(),
158                    permission: Some(permission),
159                },
160            )
161            .await?;
162        Ok(())
163    }
164
165    fn room_token(&self, room: &str, identity: &str) -> Result<String> {
166        token::create(
167            &self.key,
168            &self.secret,
169            Some(identity),
170            token::VideoGrant::to_join(room),
171        )
172    }
173
174    fn guest_token(&self, room: &str, identity: &str) -> Result<String> {
175        token::create(
176            &self.key,
177            &self.secret,
178            Some(identity),
179            token::VideoGrant::for_guest(room),
180        )
181    }
182}