neoercities/
lib.rs

1//! Another simple Neocities API wrapper.
2//! 
3//! # Usage:
4//! 
5//! Create a [`NeocitiesClient`] either with or without authentication
6//! (no-auth clients have very limited functionality).
7//! 
8//! ```no_run
9//! let client1 = NeocitiesClient::new("randomuser", "notmypassword");
10//! let client2 = NeocitiesClient::new_with_key(&key);
11//! let client3 = NeocitiesClient::new_no_auth();
12//! ```
13//! 
14//! From there, you can talk to the Neocities API at your leisure.
15//! 
16//! ```no_run
17//! let info = client1.info();
18//! client2.upload("site/file.txt", "file.txt");
19//! let someone_elses_info = client3.info_no_auth("ambyshframber");
20//! // this is the only method that no-auth clients can call
21//! ```
22//! 
23//! The crate also includes an optional utility module for dealing with site file lists. Enable the `site_info` feature to use it.
24
25use std::path::Path;
26use std::fs::read;
27
28use reqwest::{blocking::{Client, RequestBuilder, multipart::{Part, Form}}};
29use thiserror::Error;
30
31#[cfg(any(feature = "site_info", doc))]
32pub mod site_info;
33
34/// The API client.
35/// 
36/// Can be created with authentication (username/password or API key) or without authentication.
37/// Clients without authentication can only use `info_no_auth()`,
38/// and all other methods will return an error.
39/// 
40/// All methods should return valid JSON.
41/// Check beforehand though, because something might go wrong at the remote end.
42#[derive(Debug)]
43pub struct NeocitiesClient {
44    client: Client,
45    has_auth: bool,
46    username: String,
47    password: String,
48    api_key: Option<String>
49}
50
51impl NeocitiesClient {
52    /// Creates a client with a username and password.
53    /// API methods called on the client will relate to the website belonging to the auth user.
54    /// 
55    /// Using a username and password is not recommended for automated tasks,
56    /// as that involves leaving plaintext passwords in source code or configuration files.
57    pub fn new(username: &str, password: &str) -> NeocitiesClient {
58        NeocitiesClient {
59            client: Client::new(),
60            has_auth: true,
61            username: String::from(username),
62            password: String::from(password),
63            api_key: None
64        }
65    }
66    /// Creates a client with an API key.
67    /// API methods called on the client will relate to the website belonging to the auth user.
68    /// 
69    /// ```no_run
70    /// # use rs_neocities::client::NeocitiesClient;
71    /// # use std::fs;
72    /// let key = fs::read_to_string("auth_key.txt")?;
73    /// let c = NeocitiesClient::new_with_key(&key);
74    /// assert!(c.info().is_ok());
75    /// ```
76    /// 
77    /// A key can be obtained by creating a client with a username and password,
78    /// and calling `get_key()`. Keep it somewhere secure!
79    pub fn new_with_key(key: &str) -> NeocitiesClient {
80        NeocitiesClient {
81            client: Client::new(),
82            has_auth: true,
83            username: String::new(),
84            password: String::new(),
85            api_key: Some(String::from(key))
86        }
87    }
88    /// Creates a client with no authentication.
89    /// 
90    /// Calls to methods other than `info_no_auth()` will return an error.
91    /// 
92    /// ```no_run
93    /// # use rs_neocities::client::NeocitiesClient;
94    /// let c = NeocitiesClient::new_no_auth();
95    /// assert!(c.info_no_auth("ambyshframber").is_ok());
96    /// assert!(c.list_all().is_err())
97    /// ```
98    pub fn new_no_auth() -> NeocitiesClient {
99        NeocitiesClient {
100            client: Client::new(),
101            has_auth: false,
102            username: String::new(),
103            password: String::new(),
104            api_key: None
105        }
106    }
107
108    fn get_auth(&self, req: RequestBuilder) -> Result<RequestBuilder, NeocitiesError> {
109        if !self.has_auth {
110            return Err(NeocitiesError::AuthError)
111        }
112        Ok(match &self.api_key {
113            Some(k) => req.bearer_auth(k),
114            None => req.basic_auth(&self.username, Some(&self.password))
115        })
116    }
117    fn get(&self, endpoint: &str) -> Result<String, NeocitiesError> {
118        let url = format!("https://neocities.org/api/{}", endpoint);
119        Ok(self.get_auth(self.client.get(url))?.send()?.text()?)
120    }
121
122    /// Gets info about the auth user's site.
123    pub fn info(&self) -> Result<String, NeocitiesError> {
124        self.get("info")
125    }
126    /// Gets info about the given site.
127    /// 
128    /// Does not error if the site doesn't exist, but the returned value will be an error message from Neocities.
129    pub fn info_no_auth(&self, site_name: &str) -> Result<String, NeocitiesError> {
130        let url = format!("https://neocities.org/api/info?sitename={}", site_name); // doesn't need auth, just send it raw
131        Ok(self.client.get(&url).send()?.text()?)
132    }
133
134    /// Lists all files and directories on the auth user's site.
135    pub fn list_all(&self) -> Result<String, NeocitiesError> {
136        self.get("list")
137    }
138    /// Lists files and directories starting from the specified path.
139    pub fn list(&self, path: &str) -> Result<String, NeocitiesError> {
140        self.get(&format!("list?path={}", path))
141    }
142
143    /// Uploads a local file to the site, placing it at `remote_path` relative to the site root.
144    /// 
145    /// Returns an error if the file can't be opened.
146    /// 
147    /// ## Example
148    /// 
149    /// ```no_run
150    /// # use rs_neocities::client::NeocitiesClient;
151    /// # use std::fs;
152    /// # let key = String::new();
153    /// let c = NeocitiesClient::new_with_key(&key);
154    /// c.upload("site/index.html", "index.html");
155    /// ```
156    pub fn upload(&self, local_path: impl AsRef<Path>, remote_path: &str) -> Result<String, NeocitiesError> {
157        let v = vec![(local_path, remote_path)];
158        self.upload_multiple(&v) // reduce code reuse
159    }
160    /// Uploads multiple local files to the site. Path tuples should take the form `(local, remote)`,
161    /// where `local` is the local path, and `remote` is the desired remote path relative to the root.
162    /// 
163    /// Returns an error if any of the files can't be opened.
164    /// 
165    /// ## Example
166    /// 
167    /// ```no_run
168    /// # use rs_neocities::client::NeocitiesClient;
169    /// # use std::fs;
170    /// # let key = String::new();
171    /// let c = NeocitiesClient::new_with_key(&key);
172    /// 
173    /// let mut files = Vec::new();
174    /// files.push(("site/index.html", "index.html"));
175    /// files.push(("images/favicon.ico", "favicon.ico"));
176    /// 
177    /// c.upload_multiple(files);
178    /// ```
179    pub fn upload_multiple(&self, paths: &[(impl AsRef<Path>, &str)]) -> Result<String, NeocitiesError> {
180        let mut files = Vec::new();
181        for (local, remote) in paths {
182            files.push((read(local)?, remote))
183        }
184
185        self.upload_bytes_multiple(files)
186    }
187    /// Uploads a vector of bytes to the site as a file, placing it at `remote_path` relative to the site root.
188    /// This is useful if you're generating data directly from an application,
189    /// and want to upload it without having to save it to a file first.
190    /// 
191    /// ## Example
192    /// 
193    /// ```no_run
194    /// # use rs_neocities::client::NeocitiesClient;
195    /// # use std::fs;
196    /// # let key = String::new();
197    /// let c = NeocitiesClient::new_with_key(&key);
198    /// let bytes = String::from("hello world!").bytes().collect();
199    /// c.upload_bytes(bytes, "hello.txt");
200    /// ```
201    pub fn upload_bytes(&self, bytes: Vec<u8>, remote_path: &str) -> Result<String, NeocitiesError> {
202        let v = vec![(bytes, remote_path)];
203        self.upload_bytes_multiple(v)
204    }
205    /// Uploads multiple vectors  of bytes to the site as files.
206    /// Tuples should take the form `(data, remote)`, where `data` is the data,
207    /// and `remote` is the desired remote path relative to the root.
208    /// 
209    /// ## Example
210    /// 
211    /// ```no_run
212    /// # use rs_neocities::client::NeocitiesClient;
213    /// # use std::fs;
214    /// # let key = String::new();
215    /// let c = NeocitiesClient::new_with_key(&key);
216    /// 
217    /// let data = Vec::new();
218    /// data.push((String::from("hello world!").bytes().collect(), "hello.txt"));
219    /// let generated_data = get_data();
220    /// data.push((generated_data, "data.bin"))
221    /// 
222    /// c.upload_bytes_multiple(data);
223    /// ```
224    pub fn upload_bytes_multiple(&self, bytes: Vec<(Vec<u8>, impl AsRef<str>)>) -> Result<String, NeocitiesError> {
225        let mut form = Form::new();
226
227        for (data, path) in bytes {
228            let part = Part::bytes(data).file_name(String::from(path.as_ref()));
229            form = form.part("", part)
230        }
231
232        Ok(self.get_auth(self.client.post("https://neocities.org/api/upload").multipart(form))?.send()?.text()?)
233    }
234
235    /// Delete a file on the site.
236    /// `path` is from the site root.
237    pub fn delete(&self, path: &str) -> Result<String, NeocitiesError> {
238        let v = vec![path];
239        self.delete_multiple(v)
240    }
241
242    /// Delete multiple files.
243    pub fn delete_multiple(&self, files: Vec<&str>) -> Result<String, NeocitiesError> {
244        let mut req = self.get_auth(self.client.post("https://neocities.org/api/delete"))?;
245
246        for f in files {
247            req = req.query(&[("filenames[]", f)]);
248        }
249
250        Ok(req.send()?.text()?)
251    }
252    
253    /// Gets the API key for the auth user. You generally only need to get this once,
254    /// so I would recommend just doing it with curl:
255    /// 
256    /// ```sh
257    /// curl "https://USER:PASS@neocities.org/api/key"
258    /// ```
259    pub fn get_key(&self) -> Result<String, NeocitiesError> {
260        self.get("key")
261    }
262}
263
264#[derive(Error, Debug)]
265pub enum NeocitiesError {
266    #[error("http request error")]
267    RequestError(#[from] reqwest::Error),
268    #[error("local file read error")]
269    FileError(#[from] std::io::Error),
270    #[error("authentication error")]
271    AuthError,
272    #[error("site item list parse error")]
273    ListParseError
274}