owo/
lib.rs

1use anyhow::{anyhow, Context, Result};
2use reqwest::blocking::multipart;
3use serde::{Deserialize, Serialize};
4const API_BASE: &str = "https://api.awau.moe";
5const USER_AGENT: &str = "WhatsThisClient (https://owo.codes/okashi/owo-rs, 0.4.0)";
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct UploadResponse {
9    pub success: bool,
10    pub files: Vec<File>,
11}
12
13#[derive(Debug, Serialize, Deserialize)]
14pub struct File {
15    pub success: bool,
16    pub hash: String,
17    pub name: String,
18    pub url: String,
19    pub size: Option<i64>,
20}
21
22#[derive(Debug, Serialize, Deserialize)]
23pub struct DeleteResponse {
24    pub success: bool,
25    pub data: FileListData,
26}
27
28#[derive(Debug, Serialize, Deserialize)]
29pub struct FileListResponse {
30    pub success: bool,
31    pub total_objects: i64,
32    pub data: Vec<FileListData>,
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36pub struct FileListData {
37    pub bucket: String,
38    pub key: String,
39    pub dir: String,
40    pub r#type: i64,
41    pub dest_url: Option<String>,
42    pub content_type: Option<String>,
43    pub content_length: Option<i64>,
44    pub created_at: String,
45    pub deleted_at: Option<String>,
46    pub delete_reason: Option<String>,
47    pub md5_hash: Option<String>,
48    pub sha256_hash: Option<String>,
49    pub associated_with_current_user: bool,
50}
51
52/// View files associated with your whats-th.is account.
53///
54/// Paginated, with `entries` amount of listings from `offset`.
55///
56/// # Arguments
57///
58/// * `key` - A valid whats-th.is API token.
59///
60/// * `entries` - The amount of entries to display at once.
61///
62/// * `offset` - The offset from which to display, with `0` being the most recently uploaded file.
63///
64pub fn list_files(key: &str, entries: &i64, offset: &i64) -> Result<FileListResponse> {
65    let url = format!("{API_BASE}/objects?limit={entries}&offset={offset}");
66
67    let client = reqwest::blocking::Client::new();
68
69    let resp = client
70        .get(url)
71        .header(reqwest::header::AUTHORIZATION, key)
72        .header(reqwest::header::USER_AGENT, USER_AGENT)
73        .send()?;
74
75    match resp.status() {
76        reqwest::StatusCode::OK => Ok(resp.json::<FileListResponse>()?),
77        i => Err(anyhow!(i)),
78    }
79}
80
81/// Delete a file associated with your whats-th.is account.
82///
83/// Must have been associated (i.e uploaded via /upload/pomf/associated) to be deletable
84///
85/// Works on both files and redirects (shortened URLs.)
86///
87/// # Arguments
88///
89/// * `key` - A valid whats-th.is API token.
90///
91/// * `object` - The object you are trying to delete, without the domain. (i.e  "/7IqAPwr")
92pub fn delete_file(key: &str, object: &str) -> Result<DeleteResponse> {
93    let url = format!("{API_BASE}/objects/{object}");
94
95    let client = reqwest::blocking::Client::new();
96
97    let resp = client
98        .delete(url)
99        .header(reqwest::header::AUTHORIZATION, key)
100        .header(reqwest::header::USER_AGENT, USER_AGENT)
101        .send()?;
102
103    match resp.status() {
104        reqwest::StatusCode::OK => Ok(resp.json::<DeleteResponse>()?),
105        i => Err(anyhow!(i)),
106    }
107}
108
109/// Use the whats-th.is API to shorten a link.
110///
111/// # Arguments
112///
113/// * `key` - A valid whats-th.is API token.
114///
115/// * `s_url` - The URL to be shortened.
116pub fn shorten(key: &str, s_url: &str) -> Result<String> {
117    let url = format!("{API_BASE}/shorten/polr");
118
119    let client = reqwest::blocking::Client::new();
120    let params = [("action", "shorten"), ("url", s_url)];
121    let url = reqwest::Url::parse_with_params(&url, params)?;
122
123    let resp = client
124        .get(url)
125        .header(reqwest::header::AUTHORIZATION, key)
126        .header(reqwest::header::USER_AGENT, USER_AGENT)
127        .send()?;
128
129    match resp.status() {
130        reqwest::StatusCode::OK => Ok(resp.text()?),
131        i => Err(anyhow!(i)),
132    }
133}
134
135/// Uploads a file with the whats-th.is API.
136///
137/// TODO: Enable concurrent uploads with a single upload
138///
139/// # Arguments
140/// * `key` - A valid whats-th.is API token.
141///
142/// * `in_file` - The file to be uploaded. Anything that implements std::io::Read should do.
143///
144/// * `mime_type` - A valid mime type, e.g "image/png".
145///
146/// * `file_name` - The desired upload file name.
147///
148/// * `result_url` - The vanity domain you wish to use.
149pub fn upload<
150    R: std::io::Read + std::marker::Send + 'static,
151    S: Into<String> + std::fmt::Display,
152>(
153    key: &str,
154    in_file: R,
155    mime_type: S,
156    file_name: S,
157    associated: &bool,
158) -> Result<UploadResponse> {
159    let mime_type = mime_type.into();
160    let file_name = file_name.into();
161
162    let url = match associated {
163        true => format!("{}/upload/pomf/associated", API_BASE),
164        false => format!("{}/upload/pomf", API_BASE),
165    };
166
167    let file_part = multipart::Part::reader(in_file)
168        .file_name(file_name)
169        .mime_str(&mime_type)
170        .expect("Error creating Multiform Part!");
171
172    let multi_form = multipart::Form::new()
173        .text("type", mime_type)
174        .part("files[]", file_part);
175
176    let client = reqwest::blocking::Client::new();
177    let resp = client
178        .post(url)
179        .header(reqwest::header::AUTHORIZATION, key)
180        .header(reqwest::header::USER_AGENT, USER_AGENT)
181        .multipart(multi_form)
182        .send()
183        .context("Failed to get request")?;
184
185    match resp.status() {
186        reqwest::StatusCode::OK => match resp.json::<UploadResponse>() {
187            Ok(i) => Ok(i),
188            Err(e) => Err(anyhow!(e)),
189        },
190        i => Err(anyhow!(i)),
191    }
192}