use std::env;
use std::error;
use std::fmt;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use chrono::{Duration, Utc};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use reqwest::{get, header, Client, Method, Request, StatusCode, Url};
use serde::{Deserialize, Serialize};
use cio_api::{BuildingConfig, ResourceConfig};
const ENDPOINT: &str = "https://api.zoom.us/v2/";
pub struct Zoom {
key: String,
secret: String,
account_id: String,
token: String,
client: Arc<Client>,
}
impl Zoom {
pub fn new<K, S, A>(key: K, secret: S, account_id: A) -> Self
where
K: ToString,
S: ToString,
A: ToString,
{
let token = token(key.to_string(), secret.to_string());
let client = Client::builder().build();
match client {
Ok(c) => Self {
key: key.to_string(),
secret: secret.to_string(),
account_id: account_id.to_string(),
token,
client: Arc::new(c),
},
Err(e) => panic!("creating client failed: {:?}", e),
}
}
pub fn new_from_env() -> Self {
let key = env::var("ZOOM_API_KEY").unwrap();
let secret = env::var("ZOOM_API_SECRET").unwrap();
let account_id = env::var("ZOOM_ACCOUNT_ID").unwrap();
Zoom::new(key, secret, account_id)
}
pub fn get_key(&self) -> &str {
&self.key
}
pub fn get_secret(&self) -> &str {
&self.secret
}
pub fn get_token(&self) -> &str {
&self.token
}
fn request<B>(
&self,
method: Method,
path: String,
body: B,
query: Option<Vec<(&str, String)>>,
) -> Request
where
B: Serialize,
{
let url = if !path.starts_with("http") {
let base = Url::parse(ENDPOINT).unwrap();
base.join(&path).unwrap()
} else {
Url::parse(&path).unwrap()
};
let bt = format!("Bearer {}", self.token);
let bearer = header::HeaderValue::from_str(&bt).unwrap();
let mut headers = header::HeaderMap::new();
headers.append(header::AUTHORIZATION, bearer);
headers.append(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
let mut rb = self.client.request(method.clone(), url).headers(headers);
match query {
None => (),
Some(val) => {
rb = rb.query(&val);
}
}
if method != Method::GET && method != Method::DELETE {
rb = rb.json(&body);
}
rb.build().unwrap()
}
pub async fn list_users(&self) -> Result<Vec<User>, APIError> {
let request = self.request(
Method::GET,
"users".to_string(),
(),
Some(vec![
("page_size", "100".to_string()),
("page_number", "1".to_string()),
]),
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
let r: APIResponse = resp.json().await.unwrap();
Ok(r.users.unwrap())
}
async fn get_user_with_login(
&self,
email: String,
login_type: LoginType,
) -> Result<User, APIError> {
let request = self.request(
Method::GET,
format!("users/{}", email),
(),
Some(vec![("login_type", login_type.to_string())]),
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
let user: User = resp.json().await.unwrap();
Ok(user)
}
pub async fn get_user(&self, email: String) -> Result<User, APIError> {
match self
.get_user_with_login(email.to_string(), LoginType::Zoom)
.await
{
Ok(user) => Ok(user),
Err(_) => {
self.get_user_with_login(email.to_string(), LoginType::Google)
.await
}
}
}
pub async fn create_user(
&self,
first_name: String,
last_name: String,
email: String,
) -> Result<User, APIError> {
let request = self.request(
Method::POST,
"users".to_string(),
CreateUserOpts {
action: "create".to_string(),
user_info: UserInfo {
first_name,
last_name,
email,
typev: 2,
},
},
None,
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::CREATED => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
let user: User = resp.json().await.unwrap();
Ok(user)
}
pub async fn update_user(
&self,
first_name: String,
last_name: String,
email: String,
use_pmi: bool,
vanity_name: String,
) -> Result<(), APIError> {
let request = self.request(
Method::PATCH,
format!("users/{}", email),
UpdateUserOpts {
first_name,
last_name,
use_pmi,
vanity_name,
},
Some(vec![("login_type", "100".to_string())]),
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::NO_CONTENT => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
Ok(())
}
pub async fn list_rooms(&self) -> Result<Vec<Room>, APIError> {
let request = self.request(
Method::GET,
"rooms".to_string(),
(),
Some(vec![("page_size", "100".to_string())]),
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
let r: APIResponse = resp.json().await.unwrap();
Ok(r.rooms.unwrap())
}
pub async fn update_room(&self, room: Room) -> Result<(), APIError> {
let id = room.clone().id.unwrap();
let request = self.request(
Method::PATCH,
format!("rooms/{}", id),
UpdateRoomRequest { basic: room },
None,
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::NO_CONTENT => (),
s => {
let body = resp.text().await.unwrap();
if body.contains(
"This conference room already has a Zoom Room account",
) {
return Ok(());
}
return Err(APIError {
status_code: s,
body,
});
}
};
Ok(())
}
pub async fn create_room(&self, room: Room) -> Result<Room, APIError> {
let request =
self.request(Method::POST, "rooms".to_string(), room, None);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::CREATED => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
});
}
};
Ok(resp.json().await.unwrap())
}
pub async fn list_buildings(&self) -> Result<Vec<Building>, APIError> {
let request = self.request(
Method::GET,
"rooms/locations".to_string(),
(),
Some(vec![
("page_size", "100".to_string()),
("type", "building".to_string()),
]),
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
});
}
};
let r: APIResponse = resp.json().await.unwrap();
Ok(r.locations.unwrap())
}
pub async fn create_building(
&self,
mut building: Building,
) -> Result<Building, APIError> {
building.parent_location_id = Some(self.account_id.to_string());
let request = self.request(
Method::POST,
"rooms/locations".to_string(),
building,
None,
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::CREATED => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
});
}
};
Ok(resp.json().await.unwrap())
}
pub async fn update_building(
&self,
mut building: Building,
) -> Result<(), APIError> {
let id = building.clone().id.unwrap();
building.parent_location_id = Some(self.account_id.to_string());
let request = self.request(
Method::PATCH,
format!("rooms/locations/{}", id),
UpdateBuildingRequest { basic: building },
None,
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::NO_CONTENT => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
Ok(())
}
pub async fn list_recordings_as_admin(
&self,
) -> Result<Vec<Meeting>, APIError> {
let now = Utc::now();
let weeks = Duration::weeks(3);
let request = self.request(
Method::GET,
"accounts/me/recordings".to_string(),
(),
Some(vec![
("page_size", "100".to_string()),
("from", now.checked_sub_signed(weeks).unwrap().to_rfc3339()),
("to", now.to_rfc3339()),
]),
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
let r: APIResponse = resp.json().await.unwrap();
Ok(r.meetings.unwrap())
}
pub async fn download_recording_to_file(
&self,
download_url: String,
file: PathBuf,
) -> Result<(), APIError> {
let resp = get(&download_url).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
fs::create_dir_all(file.parent().unwrap()).unwrap();
let mut f = fs::File::create(file).unwrap();
f.write_all(resp.text().await.unwrap().as_bytes()).unwrap();
Ok(())
}
pub async fn delete_meeting_recordings(
&self,
meeting_id: i64,
) -> Result<(), APIError> {
let request = self.request(
Method::DELETE,
format!("meetings/{}/recordings", meeting_id),
(),
None,
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::NO_CONTENT => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
Ok(())
}
}
pub struct APIError {
pub status_code: StatusCode,
pub body: String,
}
impl fmt::Display for APIError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"APIError: status code -> {}, body -> {}",
self.status_code.to_string(),
self.body
)
}
}
impl fmt::Debug for APIError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"APIError: status code -> {}, body -> {}",
self.status_code.to_string(),
self.body
)
}
}
impl error::Error for APIError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
None
}
}
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
iss: String,
exp: usize,
}
fn token(key: String, secret: String) -> String {
let claims = Claims {
iss: key,
exp: 10_000_000_000,
};
let mut header = Header::default();
header.kid = Some("signing_key".to_owned());
header.alg = Algorithm::HS256;
match encode(&header, &claims, &EncodingKey::from_secret(secret.as_ref())) {
Ok(t) => t,
Err(e) => panic!("creating jwt failed: {}", e),
}
}
#[derive(Debug, Serialize, Deserialize)]
struct APIResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub page_count: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_number: Option<i64>,
pub page_size: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_records: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_page_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rooms: Option<Vec<Room>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub users: Option<Vec<User>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locations: Option<Vec<Building>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub meetings: Option<Vec<Meeting>>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct User {
pub id: Option<String>,
pub first_name: String,
pub last_name: String,
pub email: String,
#[serde(rename = "type")]
pub typev: i64,
pub status: Option<String>,
pub pmi: Option<i64>,
pub timezone: Option<String>,
pub dept: Option<String>,
pub created_at: Option<String>,
pub last_login_time: Option<String>,
pub last_client_version: Option<String>,
pub verified: Option<i64>,
pub role_name: Option<String>,
pub use_pmi: Option<bool>,
pub language: Option<String>,
pub vanity_url: Option<String>,
pub personal_meeting_url: Option<String>,
pub pic_url: Option<String>,
pub account_id: Option<String>,
pub host_key: Option<String>,
pub job_title: Option<String>,
pub company: Option<String>,
pub location: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LoginType {
Facebook = 0,
Google = 1,
API = 99,
Zoom = 100,
SSO = 101,
}
impl Default for LoginType {
fn default() -> Self {
LoginType::Zoom
}
}
impl fmt::Display for LoginType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug, Serialize, Deserialize)]
struct CreateUserOpts {
pub action: String,
pub user_info: UserInfo,
}
#[derive(Debug, Serialize, Deserialize)]
struct UserInfo {
pub first_name: String,
pub last_name: String,
pub email: String,
#[serde(rename = "type")]
pub typev: i64,
}
#[derive(Debug, Serialize, Deserialize)]
struct UpdateUserOpts {
pub first_name: String,
pub last_name: String,
pub use_pmi: bool,
pub vanity_name: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Room {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub activation_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
pub typev: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub support_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub support_phone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub room_passcode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required_code_to_ext: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hide_in_room_contacts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location_id: Option<String>,
}
impl Room {
pub fn update(
mut self,
resource: ResourceConfig,
passcode: String,
location_id: String,
) -> Room {
self.name = resource.name;
self.room_passcode = Some(passcode);
self.required_code_to_ext = Some(true);
self.typev = Some("ZoomRoom".to_string());
self.location_id = Some(location_id);
self.hide_in_room_contacts = Some(false);
self
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct UpdateRoomRequest {
pub basic: Room,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Building {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_location_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
pub typev: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub support_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub support_phone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub room_passcode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required_code_to_ext: Option<bool>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct UpdateBuildingRequest {
pub basic: Building,
}
impl Building {
pub fn update(
mut self,
building: BuildingConfig,
passcode: String,
) -> Building {
self.name = building.name;
self.description = Some(building.description);
self.address = Some(format!(
"{}
{}, {} {} {}",
building.address,
building.city,
building.state,
building.zipcode,
building.country
));
self.room_passcode = Some(passcode);
self.required_code_to_ext = Some(true);
self.typev = Some("building".to_string());
self
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Meeting {
pub uuid: String,
pub id: i64,
pub host_id: String,
pub topic: String,
pub start_time: String,
pub duration: i64,
pub total_size: i64,
pub recording_count: i32,
pub recording_files: Vec<Recording>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Recording {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub recording_start: String,
pub recording_end: String,
pub file_type: FileType,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_size: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub play_url: Option<String>,
pub download_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recording_type: Option<String>,
pub meeting_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum FileType {
MP4,
M4A,
Timeline,
Transcript,
Chat,
CC,
}
impl Default for FileType {
fn default() -> Self {
FileType::MP4
}
}
impl FileType {
pub fn to_extension(&self) -> String {
match self {
FileType::MP4 => "-video.mp4".to_string(),
FileType::M4A => "-audio.m4a".to_string(),
FileType::Timeline => "-timeline.txt".to_string(),
FileType::Transcript => "-transcription.txt".to_string(),
FileType::Chat => "-chat.txt".to_string(),
FileType::CC => "-closed-captions.txt".to_string(),
}
}
pub fn get_mime_type(&self) -> String {
match self {
FileType::MP4 => "video/mp4".to_string(),
FileType::M4A => "audio/m4a".to_string(),
FileType::Timeline => "text/plain".to_string(),
FileType::Transcript => "text/plain".to_string(),
FileType::Chat => "text/plain".to_string(),
FileType::CC => "text/plain".to_string(),
}
}
}