use crate::error::{MegaError, Result};
use crate::http::HttpClient;
use serde_json::Value;
use std::time::{Duration, Instant};
use tokio::time::timeout;
use tracing::{info_span, trace};
#[cfg(not(target_arch = "wasm32"))]
async fn sleep(duration: Duration) {
tokio::time::sleep(duration).await;
}
#[cfg(target_arch = "wasm32")]
async fn sleep(duration: Duration) {
use js_sys::Promise;
use wasm_bindgen_futures::JsFuture;
let millis = duration.as_millis() as i32;
let promise = Promise::new(&mut |resolve, _| {
let window = web_sys::window().expect("no window");
window
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, millis)
.expect("setTimeout failed");
});
JsFuture::from(promise).await.unwrap();
}
const API_URL: &str = "https://g.api.mega.co.nz/cs";
const WSC_URL: &str = "https://g.api.mega.co.nz/wsc";
const SC_ALERTS_URL: &str = "https://g.api.mega.co.nz/sc?c=50";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiErrorCode {
Internal = -1,
Args = -2,
Again = -3,
RateLimit = -4,
Failed = -5,
TooManyIps = -6,
AccessDenied = -7,
Exist = -8,
NotExist = -9,
Circular = -10,
AccessViolation = -11,
AppKey = -12,
Expired = -13,
NotConfirmed = -14,
Blocked = -15,
OverQuota = -16,
TempUnavail = -17,
TooManyConnections = -18,
Unknown = -9999,
}
impl From<i64> for ApiErrorCode {
fn from(code: i64) -> Self {
match code {
-1 => ApiErrorCode::Internal,
-2 => ApiErrorCode::Args,
-3 => ApiErrorCode::Again,
-4 => ApiErrorCode::RateLimit,
-5 => ApiErrorCode::Failed,
-6 => ApiErrorCode::TooManyIps,
-7 => ApiErrorCode::AccessDenied,
-8 => ApiErrorCode::Exist,
-9 => ApiErrorCode::NotExist,
-10 => ApiErrorCode::Circular,
-11 => ApiErrorCode::AccessViolation,
-12 => ApiErrorCode::AppKey,
-13 => ApiErrorCode::Expired,
-14 => ApiErrorCode::NotConfirmed,
-15 => ApiErrorCode::Blocked,
-16 => ApiErrorCode::OverQuota,
-17 => ApiErrorCode::TempUnavail,
-18 => ApiErrorCode::TooManyConnections,
_ => ApiErrorCode::Unknown,
}
}
}
impl ApiErrorCode {
pub fn description(&self) -> &'static str {
match self {
ApiErrorCode::Internal => "Internal error",
ApiErrorCode::Args => "Invalid arguments",
ApiErrorCode::Again => "Try again",
ApiErrorCode::RateLimit => "Rate limit exceeded",
ApiErrorCode::Failed => "Upload failed",
ApiErrorCode::TooManyIps => "Too many IPs",
ApiErrorCode::AccessDenied => "Access denied",
ApiErrorCode::Exist => "Resource already exists",
ApiErrorCode::NotExist => "Resource does not exist",
ApiErrorCode::Circular => "Circular linking",
ApiErrorCode::AccessViolation => "Access violation",
ApiErrorCode::AppKey => "Application key required",
ApiErrorCode::Expired => "Session expired",
ApiErrorCode::NotConfirmed => "Not confirmed",
ApiErrorCode::Blocked => "Resource blocked",
ApiErrorCode::OverQuota => "Over quota",
ApiErrorCode::TempUnavail => "Temporarily unavailable",
ApiErrorCode::TooManyConnections => "Too many connections",
ApiErrorCode::Unknown => "Unknown error",
}
}
}
#[derive(Debug)]
pub struct ApiClient {
http: HttpClient,
request_id: u32,
session_id: Option<String>,
}
impl ApiClient {
pub fn new() -> Self {
Self {
http: HttpClient::new(),
request_id: rand::random(),
session_id: None,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn with_proxy(proxy: &str) -> crate::error::Result<Self> {
Ok(Self {
http: HttpClient::with_proxy(proxy)?,
request_id: rand::random(),
session_id: None,
})
}
pub fn set_session_id(&mut self, sid: String) {
self.session_id = Some(sid);
}
pub fn clear_session_id(&mut self) {
self.session_id = None;
}
pub fn session_id(&self) -> Option<&str> {
self.session_id.as_deref()
}
pub async fn request(&mut self, request: Value) -> Result<Value> {
self.request_with_allowed(request, &[]).await
}
pub async fn request_with_allowed(
&mut self,
request: Value,
allowed_errors: &[i64],
) -> Result<Value> {
let action_name = request.get("a").and_then(|v| v.as_str()).unwrap_or("");
let span = info_span!(
"mega.api.request",
action = action_name,
sid_present = self.session_id.is_some(),
allowed_errors = ?allowed_errors
);
let _guard = span.enter();
let body = serde_json::to_string(&vec![request.clone()])?;
let mut delay_ms = 250u64;
let max_delay_ms = 256_000u64; let mut attempts = 0;
let max_attempts = if action_name == "s2" { 6 } else { 8 };
loop {
sleep(Duration::from_millis(20)).await;
self.request_id = self.request_id.wrapping_add(1);
let mut url = match &self.session_id {
Some(sid) => format!("{}?id={}&sid={}", API_URL, self.request_id, sid),
None => format!("{}?id={}", API_URL, self.request_id),
};
url.push_str("&v=3");
if action_name == "s2" {
url.push_str("&bc=1");
}
let attempt = attempts + 1;
let request_id = self.request_id;
let start = Instant::now();
let response_text =
match timeout(Duration::from_secs(20), self.http.post(&url, &body)).await {
Ok(Ok(text)) => text,
Ok(Err(err)) => {
let elapsed_ms = start.elapsed().as_millis() as u64;
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
elapsed_ms,
error = %err,
"api request"
);
return Err(err);
}
Err(_) => {
let elapsed_ms = start.elapsed().as_millis() as u64;
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
elapsed_ms,
error = "timeout",
"api request"
);
return Err(MegaError::Custom("HTTP request timed out".to_string()));
}
};
let elapsed_ms = start.elapsed().as_millis() as u64;
let response_bytes = response_text.len();
let response: Value = match serde_json::from_str(&response_text) {
Ok(value) => value,
Err(err) => {
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
error = %err,
"api request"
);
return Err(err.into());
}
};
attempts += 1;
if let Some(arr) = response.as_array() {
if arr.len() == 1 {
if let Some(code) = arr[0].as_i64() {
if code >= 0 {
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = "ok",
api_code = code,
"api request"
);
return Ok(Value::from(code));
}
let error_code = ApiErrorCode::from(code);
if allowed_errors.contains(&code) {
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = "allowed_error",
api_error = code,
"api request"
);
return Ok(Value::from(code));
}
if error_code == ApiErrorCode::Again {
sleep(Duration::from_millis(delay_ms)).await;
let next_delay = delay_ms.saturating_mul(2);
let retry_limit = attempts >= max_attempts || next_delay > max_delay_ms;
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = if retry_limit { "server_busy" } else { "retry" },
api_error = code,
delay_ms,
next_delay,
max_attempts,
"api request"
);
if retry_limit {
return Err(MegaError::ServerBusy);
}
delay_ms = next_delay;
continue;
}
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = "api_error",
api_error = code,
"api request"
);
return Err(MegaError::ApiError {
code: code as i32,
message: error_code.description().to_string(),
});
}
}
if let Some(first) = arr.first() {
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = "ok",
"api request"
);
return Ok(first.clone());
}
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = "invalid_response",
"api request"
);
return Err(MegaError::InvalidResponse);
}
if let Some(code) = response.as_i64() {
let error_code = ApiErrorCode::from(code);
if error_code == ApiErrorCode::Again {
sleep(Duration::from_millis(delay_ms)).await;
let next_delay = delay_ms.saturating_mul(2);
let retry_limit = attempts >= max_attempts || next_delay > max_delay_ms;
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = if retry_limit { "server_busy" } else { "retry" },
api_error = code,
delay_ms,
next_delay,
max_attempts,
"api request"
);
if retry_limit {
return Err(MegaError::ServerBusy);
}
delay_ms = next_delay;
continue;
}
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = "api_error",
api_error = code,
"api request"
);
return Err(MegaError::ApiError {
code: code as i32,
message: error_code.description().to_string(),
});
}
trace!(
request_id,
attempt,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = "invalid_response",
"api request"
);
return Err(MegaError::InvalidResponse);
}
}
pub async fn poll_sc(
&mut self,
sn: Option<&str>,
wsc_base: Option<&str>,
) -> Result<(Vec<Value>, String, Option<String>)> {
let sn = sn.ok_or_else(|| MegaError::Custom("Missing SC sequence number".to_string()))?;
let sid = self
.session_id
.as_deref()
.ok_or_else(|| MegaError::Custom("Session ID not set".to_string()))?;
let base = wsc_base.unwrap_or(WSC_URL);
let mut url = base.to_string();
let sep = if url.contains('?') { "&" } else { "?" };
url.push_str(sep);
url.push_str("sn=");
url.push_str(sn);
url.push_str("&sid=");
url.push_str(sid);
let response_text = self.http.post(&url, "").await?;
let resp: Value = serde_json::from_str(&response_text)
.map_err(|_| MegaError::InvalidResponse)?;
if let Some(code) = resp.as_i64() {
if code == 0 {
return Ok((Vec::new(), sn.to_string(), None));
}
let error_code = ApiErrorCode::from(code);
return Err(MegaError::ApiError {
code: code as i32,
message: error_code.description().to_string(),
});
}
let obj = resp.as_object().ok_or(MegaError::InvalidResponse)?;
let next_sn = obj
.get("sn")
.and_then(|v| v.as_str())
.ok_or(MegaError::InvalidResponse)?
.to_string();
let wsc = obj.get("w").and_then(|v| v.as_str()).map(|s| s.to_string());
let events = obj
.get("a")
.and_then(|v| v.as_array())
.map(|arr| arr.clone())
.unwrap_or_default();
Ok((events, next_sn, wsc))
}
pub async fn poll_user_alerts(&mut self) -> Result<(Vec<Value>, Option<String>)> {
let sid = self
.session_id
.as_deref()
.ok_or_else(|| MegaError::Custom("Session ID not set".to_string()))?;
let url = format!("{}&sid={}", SC_ALERTS_URL, sid);
let response_text = self.http.post(&url, "").await?;
let resp: Value = serde_json::from_str(&response_text)
.map_err(|_| MegaError::InvalidResponse)?;
if let Some(code) = resp.as_i64() {
let error_code = ApiErrorCode::from(code);
return Err(MegaError::ApiError {
code: code as i32,
message: error_code.description().to_string(),
});
}
let obj = resp.as_object().ok_or(MegaError::InvalidResponse)?;
let alerts = obj
.get("c")
.and_then(|v| v.as_array())
.map(|arr| arr.clone())
.unwrap_or_default();
let lsn = obj.get("lsn").and_then(|v| v.as_str()).map(|s| s.to_string());
Ok((alerts, lsn))
}
pub async fn request_batch(&mut self, requests: Vec<Value>) -> Result<Value> {
if requests.is_empty() {
return Ok(Value::Array(vec![]));
}
let span = info_span!(
"mega.api.request_batch",
sid_present = self.session_id.is_some(),
batch_len = requests.len()
);
let _guard = span.enter();
self.request_id = self.request_id.wrapping_add(1);
let request_id = self.request_id;
let url = match &self.session_id {
Some(sid) => format!("{}?id={}&sid={}", API_URL, self.request_id, sid),
None => format!("{}?id={}", API_URL, self.request_id),
};
let url = format!("{}&v=3", url);
let body = serde_json::to_string(&requests)?;
let mut delay_ms = 250u64;
let max_delay_ms = 256_000u64;
loop {
sleep(Duration::from_millis(20)).await;
let start = Instant::now();
let response_text = match self.http.post(&url, &body).await {
Ok(text) => text,
Err(err) => {
let elapsed_ms = start.elapsed().as_millis() as u64;
trace!(
request_id,
url = %url,
body_bytes = body.len(),
body = %body,
elapsed_ms,
error = %err,
"api batch request"
);
return Err(err);
}
};
let elapsed_ms = start.elapsed().as_millis() as u64;
let response_bytes = response_text.len();
let response: Value = match serde_json::from_str(&response_text) {
Ok(value) => value,
Err(err) => {
trace!(
request_id,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
error = %err,
"api batch request"
);
return Err(err.into());
}
};
if let Some(code) = response.as_i64() {
let error_code = ApiErrorCode::from(code);
if error_code == ApiErrorCode::Again {
sleep(Duration::from_millis(delay_ms)).await;
let next_delay = delay_ms.saturating_mul(2);
let retry_limit = next_delay > max_delay_ms;
trace!(
request_id,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = if retry_limit { "server_busy" } else { "retry" },
api_error = code,
delay_ms,
next_delay,
"api batch request"
);
if retry_limit {
return Err(MegaError::ServerBusy);
}
delay_ms = next_delay;
continue;
}
trace!(
request_id,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = "api_error",
api_error = code,
"api batch request"
);
return Err(MegaError::ApiError {
code: code as i32,
message: error_code.description().to_string(),
});
}
trace!(
request_id,
url = %url,
body_bytes = body.len(),
body = %body,
response_bytes,
response_body = %response_text,
elapsed_ms,
result = "ok",
"api batch request"
);
return Ok(response);
}
}
pub async fn get_user_attribute(&mut self, attr: &str) -> Result<Value> {
self.request(serde_json::json!({
"a": "uga",
"ua": attr
}))
.await
}
pub async fn set_private_attribute(
&mut self,
attr: &str,
value: &str,
version: Option<i64>,
) -> Result<Value> {
let ver = version.unwrap_or(0);
let mut obj = serde_json::Map::new();
obj.insert("a".into(), serde_json::Value::from("upv"));
obj.insert(attr.into(), serde_json::json!([value, ver]));
self.request(serde_json::Value::Object(obj)).await
}
}
impl Default for ApiClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_code_conversion() {
assert_eq!(ApiErrorCode::from(-1), ApiErrorCode::Internal);
assert_eq!(ApiErrorCode::from(-2), ApiErrorCode::Args);
assert_eq!(ApiErrorCode::from(-3), ApiErrorCode::Again);
assert_eq!(ApiErrorCode::from(-4), ApiErrorCode::RateLimit);
assert_eq!(ApiErrorCode::from(-5), ApiErrorCode::Failed);
assert_eq!(ApiErrorCode::from(-6), ApiErrorCode::TooManyIps);
assert_eq!(ApiErrorCode::from(-7), ApiErrorCode::AccessDenied);
assert_eq!(ApiErrorCode::from(-8), ApiErrorCode::Exist);
assert_eq!(ApiErrorCode::from(-9), ApiErrorCode::NotExist);
assert_eq!(ApiErrorCode::from(-10), ApiErrorCode::Circular);
assert_eq!(ApiErrorCode::from(-11), ApiErrorCode::AccessViolation);
assert_eq!(ApiErrorCode::from(-12), ApiErrorCode::AppKey);
assert_eq!(ApiErrorCode::from(-13), ApiErrorCode::Expired);
assert_eq!(ApiErrorCode::from(-14), ApiErrorCode::NotConfirmed);
assert_eq!(ApiErrorCode::from(-15), ApiErrorCode::Blocked);
assert_eq!(ApiErrorCode::from(-16), ApiErrorCode::OverQuota);
assert_eq!(ApiErrorCode::from(-17), ApiErrorCode::TempUnavail);
assert_eq!(ApiErrorCode::from(-18), ApiErrorCode::TooManyConnections);
assert_eq!(ApiErrorCode::from(-999), ApiErrorCode::Unknown);
}
#[test]
fn test_error_code_descriptions() {
assert_eq!(ApiErrorCode::Internal.description(), "Internal error");
assert_eq!(ApiErrorCode::Args.description(), "Invalid arguments");
assert_eq!(ApiErrorCode::Again.description(), "Try again");
assert_eq!(ApiErrorCode::RateLimit.description(), "Rate limit exceeded");
assert_eq!(ApiErrorCode::Failed.description(), "Upload failed");
assert_eq!(ApiErrorCode::TooManyIps.description(), "Too many IPs");
assert_eq!(ApiErrorCode::AccessDenied.description(), "Access denied");
assert_eq!(ApiErrorCode::Exist.description(), "Resource already exists");
assert_eq!(
ApiErrorCode::NotExist.description(),
"Resource does not exist"
);
assert_eq!(ApiErrorCode::Circular.description(), "Circular linking");
assert_eq!(
ApiErrorCode::AccessViolation.description(),
"Access violation"
);
assert_eq!(
ApiErrorCode::AppKey.description(),
"Application key required"
);
assert_eq!(ApiErrorCode::Expired.description(), "Session expired");
assert_eq!(ApiErrorCode::NotConfirmed.description(), "Not confirmed");
assert_eq!(ApiErrorCode::Blocked.description(), "Resource blocked");
assert_eq!(ApiErrorCode::OverQuota.description(), "Over quota");
assert_eq!(
ApiErrorCode::TempUnavail.description(),
"Temporarily unavailable"
);
assert_eq!(
ApiErrorCode::TooManyConnections.description(),
"Too many connections"
);
assert_eq!(ApiErrorCode::Unknown.description(), "Unknown error");
}
#[test]
fn test_client_creation() {
let client = ApiClient::new();
assert!(client.session_id.is_none());
}
#[test]
fn test_proxy_creation() {
let client = ApiClient::with_proxy("http://127.0.0.1:8080");
assert!(client.is_ok());
}
#[test]
fn test_session_management() {
let mut client = ApiClient::new();
assert!(client.session_id().is_none());
client.set_session_id("test_session_id".to_string());
assert_eq!(client.session_id(), Some("test_session_id"));
client.clear_session_id();
assert!(client.session_id().is_none());
}
}