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::{CatsResp, Meme, MemesResp, User, UsersResp}; use anyhow::Result; use log::info; use once_cell::sync::OnceCell; use reqwest::Client; use url::Url; // cached api responses static CATS: OnceCell> = OnceCell::new(); static MEMES: OnceCell> = OnceCell::new(); static USERS: OnceCell> = OnceCell::new(); 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 = serde_json::from_slice::(&res.bytes().await?)?; cats.categories.into_iter().map(|c| c.id).collect() })) } pub async fn memes<'a>( http: &Client, cat_filter: Option<&str>, usr_filter: Option<&str>, ) -> Result<&'a Vec> { Ok(init_once_cell!(MEMES, { let mut url = Url::options().parse("https://data.tilera.xyz/api/jensmemes/memes")?; let mut pairs = url.query_pairs_mut(); if let Some(cat) = cat_filter { pairs.append_pair("category", cat); } if let Some(usr) = usr_filter { 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 = serde_json::from_slice::(&res.bytes().await?)?; memes.memes })) } 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 = serde_json::from_slice::(&res.bytes().await?)?; users.users })) } } pub async fn open_link(url: &str) -> anyhow::Result<()> { match std::env::var_os("BROWSER") { Some(browser) => { Command::new(&browser).arg(url).status().await?; }, None => opener::open(&url)?, } Ok(()) } /// returns an empty table with the correct format settings for lists pub fn list_table<'a>() -> Table<'a> { TableBuilder::new() .style(TableStyle::simple()) .separate_rows(false) .has_top_boarder(false) .has_bottom_boarder(false) .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 == cat) { bail!(consts::NO_SUCH_CATEGORY_ERROR); } Ok(()) }