use anyhow::{Context, Result};
use reqwest::Client;
use reqwest::multipart::{Form, Part};
use serde::Deserialize;
use tracing::debug;
use super::{parse_json, response_text};
use crate::TIMEOUT;
pub const API_URL: &str = "https://imgbox.com";
#[derive(Deserialize)]
struct TokenResponse {
token_id: u64,
token_secret: String,
gallery_id: Option<String>,
gallery_secret: Option<String>,
}
#[derive(Deserialize)]
struct UploadFile {
original_url: String,
}
#[derive(Deserialize)]
struct UploadResponse {
files: Vec<UploadFile>,
}
pub async fn upload(_client: &Client, data: Vec<u8>, url: &str) -> Result<String> {
let client = reqwest::Client::builder()
.cookie_store(true)
.timeout(TIMEOUT)
.build()
.context("failed to build imgbox client")?;
let csrf_token = fetch_csrf(&client, url).await?;
let token = fetch_token(&client, url, &csrf_token).await?;
let gallery_id = token.gallery_id.unwrap_or_else(|| "null".to_owned());
let gallery_secret = token.gallery_secret.unwrap_or_else(|| "null".to_owned());
let form = Form::new()
.text("token_id", token.token_id.to_string())
.text("token_secret", token.token_secret)
.text("gallery_id", gallery_id)
.text("gallery_secret", gallery_secret)
.text("content_type", "1")
.text("thumbnail_size", "100r")
.text("comments_enabled", "0")
.part("files[]", Part::bytes(data).file_name("image.png"));
let resp = client
.post(format!("{url}/upload/process"))
.header("X-CSRF-Token", &csrf_token)
.multipart(form)
.send()
.await
.context("failed to send upload request to imgbox")?;
let resp: UploadResponse = parse_json(resp, "imgbox").await?;
resp.files
.into_iter()
.next()
.context("imgbox returned no files")
.map(|f| f.original_url)
}
async fn fetch_csrf(client: &Client, base_url: &str) -> Result<String> {
let resp = client
.get(base_url)
.send()
.await
.context("failed to fetch imgbox main page")?;
let body = response_text(resp, "imgbox").await?;
let csrf_token = extract_csrf_token(&body)?;
debug!("csrf_token={csrf_token}");
Ok(csrf_token)
}
fn extract_csrf_token(html: &str) -> Result<String> {
for line in html.lines() {
if line.contains("csrf-token")
&& let Some(after) = line.split("content=\"").nth(1)
&& let Some(token) = after.split('"').next()
&& !token.is_empty()
{
return Ok(token.to_owned());
}
}
debug!("HTML:\n{html}");
anyhow::bail!("csrf token not found in imgbox page")
}
async fn fetch_token(client: &Client, base_url: &str, csrf_token: &str) -> Result<TokenResponse> {
let resp = client
.post(format!("{base_url}/ajax/token/generate"))
.header("X-CSRF-Token", csrf_token)
.form(&[
("gallery", "true"),
("gallery_title", ""),
("comments_enabled", "0"),
])
.send()
.await
.context("failed to fetch imgbox upload token")?;
parse_json(resp, "imgbox token").await
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::*;
#[tokio::test]
async fn test_upload_success() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(
ResponseTemplate::new(200)
.append_header("set-cookie", "_imgbox_session=sess; Path=/; HttpOnly")
.set_body_string(r#"<meta content="test_csrf" name="csrf-token" />"#),
)
.mount(&mock_server)
.await;
Mock::given(method("POST"))
.and(path("/ajax/token/generate"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"ok": true,
"token_id": 12345,
"token_secret": "tsec",
"gallery_id": "gid",
"gallery_secret": "gsec"
})))
.mount(&mock_server)
.await;
Mock::given(method("POST"))
.and(path("/upload/process"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"ok": true,
"files": [{"original_url": "https://images2.imgbox.com/test/img_o.png"}]
})))
.mount(&mock_server)
.await;
let client = Client::new();
let url = upload(&client, vec![1, 2, 3], &mock_server.uri())
.await
.unwrap();
assert_eq!(url, "https://images2.imgbox.com/test/img_o.png");
}
#[test]
fn test_extract_csrf_token() {
let html = r#"<meta content="abc123" name="csrf-token" />"#;
assert_eq!(extract_csrf_token(html).unwrap(), "abc123");
}
#[test]
fn test_extract_csrf_token_missing() {
assert!(extract_csrf_token("<html><head></head></html>").is_err());
}
}