From a3677bcf3cd79284813e903bf1c8052370263b1f Mon Sep 17 00:00:00 2001 From: LordMZTE Date: Thu, 1 Apr 2021 19:12:23 +0200 Subject: [PATCH] move client core to seperate crate --- Cargo.toml | 1 + cli/Cargo.toml | 2 + cli/src/api.rs | 65 +----------- cli/src/commands/cats.rs | 3 +- cli/src/commands/list.rs | 5 +- cli/src/commands/search.rs | 5 +- cli/src/commands/up.rs | 7 +- cli/src/commands/users.rs | 5 +- cli/src/main.rs | 2 +- cli/src/util.rs | 200 ------------------------------------ jm_client_core/Cargo.toml | 20 ++++ jm_client_core/src/api.rs | 64 ++++++++++++ jm_client_core/src/lib.rs | 2 + jm_client_core/src/util.rs | 201 +++++++++++++++++++++++++++++++++++++ tokencracker/src/api.rs | 13 ++- tokencracker/src/main.rs | 15 ++- 16 files changed, 326 insertions(+), 284 deletions(-) create mode 100644 jm_client_core/Cargo.toml create mode 100644 jm_client_core/src/api.rs create mode 100644 jm_client_core/src/lib.rs create mode 100644 jm_client_core/src/util.rs diff --git a/Cargo.toml b/Cargo.toml index 71cb74f..c5417f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,5 @@ members = [ "tokencracker", "cli", + "jm_client_core", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2aea4da..6b530e7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -16,6 +16,8 @@ name = "jm" path = "src/main.rs" [dependencies] +jm_client_core = { path = "../jm_client_core" } + anyhow = "1.0.34" clap = "2.33.3" env_logger = "0.8.2" diff --git a/cli/src/api.rs b/cli/src/api.rs index 024fdc9..88ac617 100644 --- a/cli/src/api.rs +++ b/cli/src/api.rs @@ -1,58 +1,13 @@ use crate::util::IntoTableRow; -use anyhow::{anyhow, Result}; -use serde::Deserialize; +use jm_client_core::api::{Category, Meme, User}; use term_table::{row::Row, table_cell::TableCell}; -#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct UpResp { - pub files: Vec, -} - -#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct CatsResp { - pub categories: Vec, -} - -#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct UsersResp { - pub users: Vec, -} - -#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct MemesResp { - pub memes: Vec, -} - -#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct Category { - pub id: String, - pub name: String, -} - impl IntoTableRow for Category { fn into_table_row(&self) -> Row<'_> { Row::new(vec![TableCell::new(&self.id), TableCell::new(&self.name)]) } } -#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct Meme { - pub id: String, - pub link: String, - pub path: String, - pub category: String, - pub user: String, -} - -impl Meme { - pub fn file_name(&self) -> Result<&str> { - self.path - .split('/') - .last() - .ok_or_else(|| anyhow!("failed to get file name. server response invalid")) - } -} - impl IntoTableRow for Meme { fn into_table_row(&self) -> Row<'_> { Row::new(vec![ @@ -63,24 +18,6 @@ impl IntoTableRow for Meme { } } -#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct User { - pub name: String, - pub id: Option, - pub tokenhash: Option, - pub userdir: Option, - pub dayuploads: String, -} - -impl User { - pub fn get_id(&self) -> Option<&String> { - self.id - .as_ref() - .or(self.tokenhash.as_ref()) - .or(self.userdir.as_ref()) - } -} - impl IntoTableRow for User { fn into_table_row(&self) -> Row<'_> { Row::new(vec![ diff --git a/cli/src/commands/cats.rs b/cli/src/commands/cats.rs index be95acc..b4dbb8f 100644 --- a/cli/src/commands/cats.rs +++ b/cli/src/commands/cats.rs @@ -1,4 +1,5 @@ -use crate::util::{self, api, IntoTableRow}; +use crate::util::{self, IntoTableRow}; +use jm_client_core::util::api; use reqwest::Client; pub async fn run(http: &Client) -> anyhow::Result<()> { diff --git a/cli/src/commands/list.rs b/cli/src/commands/list.rs index 76013f3..e27e4c5 100644 --- a/cli/src/commands/list.rs +++ b/cli/src/commands/list.rs @@ -1,7 +1,8 @@ use anyhow::Result; use reqwest::Client; -use crate::util::{self, api, IntoTableRow, MemeSorting}; +use crate::util::IntoTableRow; +use jm_client_core::util::{self, api, MemeSorting}; pub async fn run( http: &Client, @@ -39,7 +40,7 @@ pub async fn run( s.sort_with(&mut memes); } - let mut table = util::list_table(); + let mut table = crate::util::list_table(); for m in memes { table.add_row(m.into_table_row()); diff --git a/cli/src/commands/search.rs b/cli/src/commands/search.rs index 172e77c..2ee47d2 100644 --- a/cli/src/commands/search.rs +++ b/cli/src/commands/search.rs @@ -2,7 +2,8 @@ use anyhow::Result; use log::info; use reqwest::Client; -use crate::util::{self, api, IntoTableRow}; +use crate::util::IntoTableRow; +use jm_client_core::util::{self, api}; pub async fn run( http: &Client, @@ -46,7 +47,7 @@ pub async fn run( matches.sort_by(|a, b| b.1.cmp(&a.1)); - let mut table = util::list_table(); + let mut table = crate::util::list_table(); for m in matches { table.add_row(m.0.into_table_row()); diff --git a/cli/src/commands/up.rs b/cli/src/commands/up.rs index f83425e..86e0dd3 100644 --- a/cli/src/commands/up.rs +++ b/cli/src/commands/up.rs @@ -1,4 +1,6 @@ +use crate::util::open_link; use anyhow::Result; +use jm_client_core::{api::UpResp, util}; use log::info; use reqwest::{ multipart::{Form, Part}, @@ -7,11 +9,6 @@ use reqwest::{ }; use tokio::{fs::File, io::reader_stream}; -use crate::{ - api::UpResp, - util::{self, open_link}, -}; - pub async fn run( http: &Client, token: String, diff --git a/cli/src/commands/users.rs b/cli/src/commands/users.rs index ba66bda..4bfb8d5 100644 --- a/cli/src/commands/users.rs +++ b/cli/src/commands/users.rs @@ -1,10 +1,11 @@ -use crate::util::{self, api, IntoTableRow}; +use crate::util::IntoTableRow; use anyhow::Result; +use jm_client_core::util::api; use reqwest::Client; pub async fn run(http: &Client) -> Result<()> { let users = api::users(http).await?; - let mut table = util::list_table(); + let mut table = crate::util::list_table(); for u in users { table.add_row(u.into_table_row()) diff --git a/cli/src/main.rs b/cli/src/main.rs index abd6a97..6ed9fdc 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,5 +1,5 @@ -use crate::util::MemeSorting; use anyhow::Result; +use jm_client_core::util::MemeSorting; use reqwest::Client; use structopt::StructOpt; diff --git a/cli/src/util.rs b/cli/src/util.rs index e8ef0e3..2587e45 100644 --- a/cli/src/util.rs +++ b/cli/src/util.rs @@ -1,152 +1,6 @@ -use crate::api::Meme; -use anyhow::bail; -use reqwest::Client; -use std::str::FromStr; use term_table::{Table, TableBuilder, TableStyle}; use tokio::process::Command; -#[macro_export] -macro_rules! init_once_cell { - ($cell:ident, $init_fn:expr) => { - match $cell.get() { - Some(x) => x, - None => { - let x = $init_fn; - $cell.get_or_init(|| x) - }, - } - }; -} - -pub mod consts { - pub const NO_SUCH_CATEGORY_ERROR: &str = "The given Category does not exist!"; - pub const NO_SUCH_USER_ERROR: &str = "The given User does not exist!"; -} - -/// ways to communicyte with the JM API -pub mod api { - use crate::api::{Category, CatsResp, Meme, MemesResp, User, UsersResp}; - use anyhow::Result; - use lazy_static::lazy_static; - use log::info; - use once_cell::sync::OnceCell; - use reqwest::Client; - use std::{ - collections::HashMap, - sync::{Arc, Mutex}, - }; - use thiserror::Error; - use url::Url; - - // cached api responses - static USERS: OnceCell> = OnceCell::new(); - static CATS: OnceCell> = OnceCell::new(); - lazy_static! { - // is this type long enough yet? - static ref MEMES: Mutex, Option), Arc>>> = - Mutex::new(HashMap::new()); - } - - #[derive(Error, Debug)] - #[error("Error Deserializing JSON api response: \n{json}\nError: {error}")] - pub struct ApiDeserializeError { - pub json: String, - #[source] - pub error: serde_json::Error, - } - - pub async fn cats(http: &Client) -> Result<&Vec> { - Ok(init_once_cell!(CATS, { - info!("Requesting categories from server"); - let res = http - .get("https://data.tilera.xyz/api/jensmemes/categories") - .send() - .await?; - let cats = try_deserialize_api_reponse::(&res.bytes().await?)?; - cats.categories.into_iter().collect() - })) - } - - pub async fn memes<'a>( - http: &Client, - cat_filter: Option, - usr_filter: Option, - ) -> Result>> { - let filters = (cat_filter, usr_filter); - if let Some(m) = MEMES.lock().unwrap().get(&filters) { - return Ok(m.clone()); - } - - let mut url = Url::options().parse("https://data.tilera.xyz/api/jensmemes/memes")?; - let mut pairs = url.query_pairs_mut(); - - if let Some(cat) = filters.0.as_ref() { - pairs.append_pair("category", cat); - } - - if let Some(usr) = filters.1.as_ref() { - pairs.append_pair("user", usr); - } - - // drop required in order to move the URL into the request - drop(pairs); - - info!("Requesting memes from server"); - let res = http.get(url).send().await?; - let memes = try_deserialize_api_reponse::(&res.bytes().await?)?; - - Ok(MEMES - .lock() - .unwrap() - .entry(filters) - .or_insert(Arc::new(memes.memes)) - .clone()) - } - - pub async fn users(http: &Client) -> Result<&Vec> { - Ok(init_once_cell!(USERS, { - info!("Requesting users from server"); - let res = http - .get("https://data.tilera.xyz/api/jensmemes/users") - .send() - .await?; - let users = try_deserialize_api_reponse::(&res.bytes().await?)?; - users.users - })) - } - - /// tries to deserialize `json` as `T`, and if it fails converts the error - /// to a `ApiDeserializeError` - pub fn try_deserialize_api_reponse<'a, T: serde::Deserialize<'a>>(json: &'a [u8]) -> Result { - let result = serde_json::from_slice::(&json); - result.map_err(|error| match std::str::from_utf8(json) { - Err(e) => e.into(), - Ok(json) => ApiDeserializeError { - json: json.into(), - error, - } - .into(), - }) - } - - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn api_deserialize_error() { - let incorrect_json = r#" - { - "foo": "bar" - } - "#; - - let result = try_deserialize_api_reponse::(incorrect_json.as_bytes()); - println!("{}", result.unwrap_err()); - } - } -} - pub async fn open_link(url: &str) -> anyhow::Result<()> { match std::env::var_os("BROWSER") { Some(browser) => { @@ -168,60 +22,6 @@ pub fn list_table<'a>() -> Table<'a> { .build() } -pub enum MemeSorting { - Id, - Link, - Category, - User, -} - -impl MemeSorting { - pub fn sort_with(&self, memes: &mut [&Meme]) { - macro_rules! sort { - ($list:ident, $field:ident) => { - $list.sort_by(|a, b| { - a.$field - .to_ascii_lowercase() - .cmp(&b.$field.to_ascii_lowercase()) - }); - }; - } - - match self { - Self::Id => sort!(memes, id), - Self::Link => sort!(memes, link), - Self::Category => sort!(memes, category), - Self::User => sort!(memes, user), - } - } -} - -impl FromStr for MemeSorting { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_ref() { - "id" => Ok(Self::Id), - "link" => Ok(Self::Link), - "category" => Ok(Self::Category), - "user" => Ok(Self::User), - _ => bail!("Invalid Meme sorting! options are id, link, category and user!"), - } - } -} - -pub async fn assert_user_exists(http: &Client, user: &str) -> anyhow::Result<()> { - if !api::users(http).await?.iter().any(|u| u.name == user) { - bail!(consts::NO_SUCH_USER_ERROR); - } - Ok(()) -} -pub async fn assert_category_exists(http: &Client, cat: &str) -> anyhow::Result<()> { - if !api::cats(http).await?.iter().any(|c| c.id == cat) { - bail!(consts::NO_SUCH_CATEGORY_ERROR); - } - Ok(()) -} pub trait IntoTableRow { fn into_table_row(&self) -> term_table::row::Row; } diff --git a/jm_client_core/Cargo.toml b/jm_client_core/Cargo.toml new file mode 100644 index 0000000..3072552 --- /dev/null +++ b/jm_client_core/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "jm_client_core" +version = "0.1.0" +authors = ["LordMZTE "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.34" +env_logger = "0.8.2" +lazy_static = "1.4.0" +log = "0.4.11" +once_cell = "1.5.2" +reqwest = "0.10.9" +serde = { version = "1.0.117", features = ["derive"] } +serde_json = "1.0.60" +thiserror = "1.0.23" +tokio = { version = "0.2.23", features = ["macros", "fs", "process"] } +url = "2.2.0" diff --git a/jm_client_core/src/api.rs b/jm_client_core/src/api.rs new file mode 100644 index 0000000..1663ef1 --- /dev/null +++ b/jm_client_core/src/api.rs @@ -0,0 +1,64 @@ +use anyhow::{anyhow, Result}; +use serde::Deserialize; + +#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct UpResp { + pub files: Vec, +} + +#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct CatsResp { + pub categories: Vec, +} + +#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct UsersResp { + pub users: Vec, +} + +#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct MemesResp { + pub memes: Vec, +} + +#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Category { + pub id: String, + pub name: String, +} + +#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Meme { + pub id: String, + pub link: String, + pub path: String, + pub category: String, + pub user: String, +} + +impl Meme { + pub fn file_name(&self) -> Result<&str> { + self.path + .split('/') + .last() + .ok_or_else(|| anyhow!("failed to get file name. server response invalid")) + } +} + +#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct User { + pub name: String, + pub id: Option, + pub tokenhash: Option, + pub userdir: Option, + pub dayuploads: String, +} + +impl User { + pub fn get_id(&self) -> Option<&String> { + self.id + .as_ref() + .or(self.tokenhash.as_ref()) + .or(self.userdir.as_ref()) + } +} diff --git a/jm_client_core/src/lib.rs b/jm_client_core/src/lib.rs new file mode 100644 index 0000000..c4755a6 --- /dev/null +++ b/jm_client_core/src/lib.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod util; diff --git a/jm_client_core/src/util.rs b/jm_client_core/src/util.rs new file mode 100644 index 0000000..b689047 --- /dev/null +++ b/jm_client_core/src/util.rs @@ -0,0 +1,201 @@ +use crate::api::Meme; +use anyhow::bail; +use reqwest::Client; +use std::str::FromStr; + +#[macro_export] +macro_rules! init_once_cell { + ($cell:ident, $init_fn:expr) => { + match $cell.get() { + Some(x) => x, + None => { + let x = $init_fn; + $cell.get_or_init(|| x) + }, + } + }; +} + +pub mod consts { + pub const NO_SUCH_CATEGORY_ERROR: &str = "The given Category does not exist!"; + pub const NO_SUCH_USER_ERROR: &str = "The given User does not exist!"; +} + +/// ways to communicyte with the JM API +pub mod api { + use crate::api::{Category, CatsResp, Meme, MemesResp, User, UsersResp}; + use anyhow::Result; + use lazy_static::lazy_static; + use log::info; + use once_cell::sync::OnceCell; + use reqwest::Client; + use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + }; + use thiserror::Error; + use url::Url; + + // cached api responses + static USERS: OnceCell> = OnceCell::new(); + static CATS: OnceCell> = OnceCell::new(); + lazy_static! { + // is this type long enough yet? + static ref MEMES: Mutex, Option), Arc>>> = + Mutex::new(HashMap::new()); + } + + #[derive(Error, Debug)] + #[error("Error Deserializing JSON api response: \n{json}\nError: {error}")] + pub struct ApiDeserializeError { + pub json: String, + #[source] + pub error: serde_json::Error, + } + + pub async fn cats(http: &Client) -> Result<&Vec> { + Ok(init_once_cell!(CATS, { + info!("Requesting categories from server"); + let res = http + .get("https://data.tilera.xyz/api/jensmemes/categories") + .send() + .await?; + let cats = try_deserialize_api_reponse::(&res.bytes().await?)?; + cats.categories.into_iter().collect() + })) + } + + pub async fn memes<'a>( + http: &Client, + cat_filter: Option, + usr_filter: Option, + ) -> Result>> { + let filters = (cat_filter, usr_filter); + if let Some(m) = MEMES.lock().unwrap().get(&filters) { + return Ok(m.clone()); + } + + let mut url = Url::options().parse("https://data.tilera.xyz/api/jensmemes/memes")?; + let mut pairs = url.query_pairs_mut(); + + if let Some(cat) = filters.0.as_ref() { + pairs.append_pair("category", cat); + } + + if let Some(usr) = filters.1.as_ref() { + pairs.append_pair("user", usr); + } + + // drop required in order to move the URL into the request + drop(pairs); + + info!("Requesting memes from server"); + let res = http.get(url).send().await?; + let memes = try_deserialize_api_reponse::(&res.bytes().await?)?; + + Ok(MEMES + .lock() + .unwrap() + .entry(filters) + .or_insert(Arc::new(memes.memes)) + .clone()) + } + + pub async fn users(http: &Client) -> Result<&Vec> { + Ok(init_once_cell!(USERS, { + info!("Requesting users from server"); + let res = http + .get("https://data.tilera.xyz/api/jensmemes/users") + .send() + .await?; + let users = try_deserialize_api_reponse::(&res.bytes().await?)?; + users.users + })) + } + + /// tries to deserialize `json` as `T`, and if it fails converts the error + /// to a `ApiDeserializeError` + pub fn try_deserialize_api_reponse<'a, T: serde::Deserialize<'a>>(json: &'a [u8]) -> Result { + let result = serde_json::from_slice::(&json); + result.map_err(|error| match std::str::from_utf8(json) { + Err(e) => e.into(), + Ok(json) => ApiDeserializeError { + json: json.into(), + error, + } + .into(), + }) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn api_deserialize_error() { + let incorrect_json = r#" + { + "foo": "bar" + } + "#; + + let result = try_deserialize_api_reponse::(incorrect_json.as_bytes()); + println!("{}", result.unwrap_err()); + } + } +} + +pub enum MemeSorting { + Id, + Link, + Category, + User, +} + +impl MemeSorting { + pub fn sort_with(&self, memes: &mut [&Meme]) { + macro_rules! sort { + ($list:ident, $field:ident) => { + $list.sort_by(|a, b| { + a.$field + .to_ascii_lowercase() + .cmp(&b.$field.to_ascii_lowercase()) + }); + }; + } + + match self { + Self::Id => sort!(memes, id), + Self::Link => sort!(memes, link), + Self::Category => sort!(memes, category), + Self::User => sort!(memes, user), + } + } +} + +impl FromStr for MemeSorting { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_ref() { + "id" => Ok(Self::Id), + "link" => Ok(Self::Link), + "category" => Ok(Self::Category), + "user" => Ok(Self::User), + _ => bail!("Invalid Meme sorting! options are id, link, category and user!"), + } + } +} + +pub async fn assert_user_exists(http: &Client, user: &str) -> anyhow::Result<()> { + if !api::users(http).await?.iter().any(|u| u.name == user) { + bail!(consts::NO_SUCH_USER_ERROR); + } + Ok(()) +} +pub async fn assert_category_exists(http: &Client, cat: &str) -> anyhow::Result<()> { + if !api::cats(http).await?.iter().any(|c| c.id == cat) { + bail!(consts::NO_SUCH_CATEGORY_ERROR); + } + Ok(()) +} diff --git a/tokencracker/src/api.rs b/tokencracker/src/api.rs index 76d2068..18357b7 100644 --- a/tokencracker/src/api.rs +++ b/tokencracker/src/api.rs @@ -1,6 +1,9 @@ -use serde::de::{Error, Unexpected, Visitor}; use core::fmt::Formatter; -use serde::{Deserialize, Deserializer}; +use serde::{ + de::{Error, Unexpected, Visitor}, + Deserialize, + Deserializer, +}; use std::convert::TryInto; #[derive(Deserialize, Debug)] @@ -36,7 +39,8 @@ where where E: Error, { - v.parse().map_err(|_| E::invalid_type(Unexpected::Str(v), &"a u32")) + v.parse() + .map_err(|_| E::invalid_type(Unexpected::Str(v), &"a u32")) } /// implementing u64 instead of 32 because it is used as fallback @@ -44,7 +48,8 @@ where where E: Error, { - v.try_into().map_err(|_| E::invalid_type(Unexpected::Unsigned(v), &"a u32")) + v.try_into() + .map_err(|_| E::invalid_type(Unexpected::Unsigned(v), &"a u32")) } } diff --git a/tokencracker/src/main.rs b/tokencracker/src/main.rs index 85ca4bd..5adef91 100644 --- a/tokencracker/src/main.rs +++ b/tokencracker/src/main.rs @@ -1,7 +1,10 @@ use anyhow::Result; use clap::{App, Arg}; use reqwest::{Client, Url}; -use tokencracker::{api::{JensmemesUser, UserResponse}, hex_string_hash}; +use tokencracker::{ + api::{JensmemesUser, UserResponse}, + hex_string_hash, +}; #[tokio::main] async fn main() -> Result<()> { @@ -33,7 +36,12 @@ async fn main() -> Result<()> { let (username, userdir) = if let (200..=210, Ok(usr)) = (response.status().as_u16(), response.bytes().await) { - let UserResponse { user: JensmemesUser {name, tokenhash, ..}, .. } = serde_json::from_slice::(&usr)?; + let UserResponse { + user: JensmemesUser { + name, tokenhash, .. + }, + .. + } = serde_json::from_slice::(&usr)?; (name, tokenhash) } else { ("Not in Database".into(), public.clone()) @@ -43,7 +51,8 @@ async fn main() -> Result<()> { "Username: {} Public Token: {} Private Token: {} -User: https://data.tilera.xyz/file/jensmemes/images/{}", +User: https://data.tilera.xyz/file/jensmemes/images/{}\ + ", username, public, private, userdir );