check CHANGELOG.md for a list of variables

This commit is contained in:
MedzikUser 2022-04-03 21:01:58 +02:00
parent b83847ff20
commit b1bfe52a4c
No known key found for this signature in database
GPG Key ID: A5FAC1E185C112DB
19 changed files with 274 additions and 186 deletions

View File

@ -8,6 +8,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
<!-- next-header -->
## [Unreleased]
### CLI
- completions: changed type from String to Shell
- removed `&` from `cli.commands` (line 54 in [parse.rs](./src/cli/parse.rs))
### Library
- removed `.map_err(anyhow::Error::new)` when function returns error
### Added
- commands in the code
- api functions to `impl` in `ImgurClient`
### Breaking Changes
- lib: moved everything to the main package with api submodules (before `imgurs::api::ImgurClient`, after `imgurs::api::ImgurClient`)
## [0.6.0] - 2022-03-14
### CLI

63
src/api/client.rs Normal file
View File

@ -0,0 +1,63 @@
macro_rules! api_url (
($path: expr) => (
format!("{}{}", "https://api.imgur.com/3/", $path)
);
);
use std::{fmt, fs, io, path::Path};
use anyhow::Error;
pub(crate) use api_url;
use reqwest::Client;
use super::*;
pub struct ImgurClient {
pub client_id: String,
pub client: Client,
}
impl fmt::Debug for ImgurClient {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ImgurClient - client_id: {}", self.client_id)
}
}
impl ImgurClient {
pub fn new(client_id: String) -> Self {
let client = Client::new();
ImgurClient { client_id, client }
}
pub async fn upload_image(&self, path: String) -> Result<ImageInfo, Error> {
let mut image: String = path.clone();
// check if the specified file exists if not then check if it is a url
if Path::new(&path).exists() {
image = fs::read_to_string(&path)
.map_err(|err| err.to_string())
.expect("read file");
} else if !validator::validate_url(&path) {
let err = io::Error::new(
io::ErrorKind::Other,
format!("{path} is not url or file path"),
);
Err(anyhow::Error::from(err))?
}
upload_image(self, image).await
}
pub async fn delete_image(&self, delete_hash: String) -> Result<(), Error> {
delete_image(self, delete_hash).await
}
pub async fn rate_limit(&self) -> Result<RateLimitInfo, Error> {
rate_limit(self).await
}
pub async fn image_info(&self, id: String) -> Result<ImageInfo, Error> {
get_image(self, id).await
}
}

View File

@ -1,28 +0,0 @@
use reqwest::Client;
use std::fmt;
macro_rules! api_url (
($path: expr) => (
format!("{}{}", "https://api.imgur.com/3/", $path)
);
);
pub(crate) use api_url;
pub struct ImgurClient {
pub client_id: String,
pub client: Client,
}
impl fmt::Debug for ImgurClient {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ImgurClient - client_id: {}", self.client_id)
}
}
impl ImgurClient {
pub fn new(client_id: String) -> Self {
let client = Client::new();
ImgurClient { client_id, client }
}
}

View File

@ -1,29 +1,35 @@
use super::send_api_request;
use crate::api::configuration::{api_url, ImgurClient};
use std::io;
use anyhow::Error;
use reqwest::Method;
use std::io::{Error, ErrorKind};
pub async fn delete_image(c: ImgurClient, delete_hash: String) -> Result<String, anyhow::Error> {
use super::{client::api_url, send_api_request, ImgurClient};
pub async fn delete_image(client: &ImgurClient, delete_hash: String) -> Result<(), Error> {
// get imgur api url
let uri = api_url!(format!("image/{delete_hash}"));
let res = send_api_request(&c, Method::DELETE, uri, None).await?;
// send request to imgur api
let res = send_api_request(client, Method::DELETE, uri, None).await?;
// get response http code
let status = res.status();
// check if an error has occurred
if status.is_client_error() || status.is_server_error() {
let mut body = res.text().await.map_err(anyhow::Error::new)?;
let mut body = res.text().await?;
if body.chars().count() > 30 {
body = "body is too length".to_string()
}
let err = Error::new(
ErrorKind::Other,
let err = io::Error::new(
io::ErrorKind::Other,
format!("server returned non-successful status code = {status}, body = {body}"),
);
Err(anyhow::Error::from(err))
} else {
Ok("If the delete hash was correct the image was deleted!".to_string())
Err(err)?
}
Ok(())
}

View File

@ -1,25 +1,30 @@
use super::send_api_request;
use crate::api::configuration::{api_url, ImgurClient};
use crate::api::ImageInfo;
use std::io;
use anyhow::Error;
use reqwest::Method;
use std::io::{Error, ErrorKind};
pub async fn get_image(c: ImgurClient, image: &str) -> Result<ImageInfo, anyhow::Error> {
use super::{client::api_url, send_api_request, ImageInfo, ImgurClient};
pub async fn get_image(client: &ImgurClient, image: String) -> Result<ImageInfo, Error> {
// get imgur api url
let uri = api_url!(format!("image/{image}"));
let res = send_api_request(&c, Method::GET, uri, None).await?;
// send request to imgur api
let res = send_api_request(client, Method::GET, uri, None).await?;
// get response http code
let status = res.status();
// check if an error has occurred
if status.is_client_error() || status.is_server_error() {
let err = Error::new(
ErrorKind::Other,
let err = io::Error::new(
io::ErrorKind::Other,
format!("server returned non-successful status code = {status}"),
);
Err(anyhow::Error::from(err))
Err(err)?
} else {
let content: ImageInfo = res.json().await.map_err(anyhow::Error::new)?;
let content: ImageInfo = res.json().await?;
Ok(content)
}
}

View File

@ -1,13 +1,13 @@
use serde_derive::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ImageInfo {
pub data: ImageInfoData,
pub success: bool,
pub status: i32,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ImageInfoData {
pub id: String,
pub title: Option<String>,

View File

@ -1,41 +1,56 @@
mod delete_image;
mod get_image;
mod image_type;
mod rate_limit;
mod upload_image;
pub mod configuration;
pub mod delete_image;
pub mod get_image;
pub mod rate_limit;
pub mod upload_image;
pub mod client;
pub use configuration::ImgurClient;
pub use client::ImgurClient;
pub use delete_image::*;
pub use get_image::*;
pub use image_type::*;
pub use rate_limit::*;
pub use upload_image::*;
use reqwest::Method;
use std::collections::HashMap;
use reqwest::{Response, Method};
use anyhow::Error;
// send request to imgur api
pub async fn send_api_request(
config: &ImgurClient,
method: Method,
uri: String,
form: Option<HashMap<&str, String>>,
) -> Result<reqwest::Response, anyhow::Error> {
) -> Result<Response, Error> {
// get request client
let client = &config.client;
// create request buidler
let mut req = client.request(method, uri.as_str());
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
// get program version
let version: Option<&str> = option_env!("CARGO_PKG_VERSION");
let version = version.unwrap_or("unknown");
// add `Authorization` and `User-Agent` to request
req = req
.header("Authorization", format!("Client-ID {}", config.client_id))
.header(
"User-Agent",
format!("Imgur/{:?}", VERSION.unwrap_or("unknown")),
format!("Imgur/{:?}", version),
);
// if exists add hashmap to request
if form != None {
req = req.form(&form.unwrap())
}
// build request
let req = req.build()?;
client.execute(req).await.map_err(anyhow::Error::from)
// send request
Ok(client.execute(req).await?)
}

View File

@ -1,9 +1,10 @@
use super::send_api_request;
use crate::api::configuration::{api_url, ImgurClient};
use std::io;
use anyhow::Error;
use reqwest::Method;
use serde_derive::{Deserialize, Serialize};
use std::io::{Error, ErrorKind};
use serde::{Deserialize, Serialize};
use super::{client::api_url, send_api_request, ImgurClient};
#[derive(Debug, Serialize, Deserialize)]
pub struct RateLimitInfo {
@ -26,22 +27,28 @@ pub struct RateLimitData {
pub client_remaining: i32,
}
pub async fn rate_limit(c: ImgurClient) -> Result<RateLimitInfo, anyhow::Error> {
pub async fn rate_limit(client: &ImgurClient) -> Result<RateLimitInfo, Error> {
// get imgur api url
let uri = api_url!("credits");
let res = send_api_request(&c, Method::GET, uri, None).await?;
// send request to imgur api
let res = send_api_request(client, Method::GET, uri, None).await?;
// get response http code
let status = res.status();
// check if an error has occurred
if status.is_client_error() || status.is_server_error() {
let body = res.text().await.map_err(anyhow::Error::new)?;
let err = Error::new(
ErrorKind::Other,
let body = res.text().await?;
let err = io::Error::new(
io::ErrorKind::Other,
format!("server returned non-successful status code = {status}, body = {body}"),
);
Err(anyhow::Error::from(err))
Err(err)?
} else {
let content: RateLimitInfo = res.json().await.map_err(anyhow::Error::new)?;
let content = res.json::<RateLimitInfo>().await?;
Ok(content)
}
}

View File

@ -1,40 +1,41 @@
use super::send_api_request;
use crate::api::{
configuration::{api_url, ImgurClient},
ImageInfo,
};
use std::{collections::HashMap, io};
use anyhow::Error;
use reqwest::Method;
use std::{
collections::HashMap,
io::{Error, ErrorKind},
};
pub async fn upload_image(c: ImgurClient, image: &str) -> Result<ImageInfo, anyhow::Error> {
use super::{client::api_url, send_api_request, ImageInfo, ImgurClient};
pub async fn upload_image(c: &ImgurClient, image: String) -> Result<ImageInfo, Error> {
// create http form (hashmap)
let mut form = HashMap::new();
// insert image to form
form.insert("image", image);
form.insert("image", image.to_string());
// get imgur api url
let uri = api_url!("image");
// send request to imgur api
let res = send_api_request(&c, Method::POST, uri, Some(form)).await?;
// get response http code
let status = res.status();
// check if an error has occurred
if status.is_client_error() || status.is_server_error() {
let mut body = res.text().await.map_err(anyhow::Error::new)?;
let mut body = res.text().await?;
if body.chars().count() > 30 {
body = "body is too length".to_string()
if body.chars().count() > 200 {
body = "server returned too long".to_string()
}
let err = Error::new(
ErrorKind::Other,
let err = io::Error::new(
io::ErrorKind::Other,
format!("server returned non-successful status code = {status}, body = {body}"),
);
Err(anyhow::Error::from(err))
Err(err)?
} else {
let content: ImageInfo = res.json().await.map_err(anyhow::Error::new)?;
let content: ImageInfo = res.json().await?;
Ok(content)
}
}

View File

@ -2,25 +2,20 @@
unix,
not(any(target_os = "macos", target_os = "android", target_os = "emscripten"))
))]
fn is_program_in_path(program: &str) -> bool {
use std::{env, fs};
if let Ok(path) = env::var("PATH") {
for p in path.split(':') {
let p_str = format!("{}/{}", p, program);
if fs::metadata(p_str).is_ok() {
return true;
// use xclip (or a similar program that is installed) because the kernel deletes the clipboard after the process ends
pub fn set_clipboard(content: String) {
fn is_program_in_path(program: &str) -> bool {
if let Ok(path) = std::env::var("PATH") {
for p in path.split(':') {
let p_str = format!("{}/{}", p, program);
if std::fs::metadata(p_str).is_ok() {
return true;
}
}
}
false
}
false
}
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "android", target_os = "emscripten"))
))]
pub fn set_clipboard(content: String) {
use std::{
io::Write,
process::{Command, Stdio},
@ -38,6 +33,7 @@ pub fn set_clipboard(content: String) {
.stdin(Stdio::piped())
.spawn()
.expect("execute command xsel")
// xclip
} else if is_program_in_path("xclip") {
child = Command::new("xclip")
@ -47,41 +43,43 @@ pub fn set_clipboard(content: String) {
.stdin(Stdio::piped())
.spawn()
.expect("execute command xclip")
// termux
} else if is_program_in_path("termux-clipboard-set") {
child = Command::new("termux-clipboard-set")
.stdin(Stdio::piped())
.spawn()
.expect("execute command termux-clipboard-set")
// the above programs responsible for the clipboard were not found
} else {
println!(
"{} {}",
"WARN".yellow(),
"command for clipboard not found".magenta()
);
return;
return
}
// copy the content (send it to stdin command)
child
.stdin
.as_mut()
.unwrap()
.write_all(content.as_bytes())
.expect("execute command");
child.wait_with_output().unwrap();
}
#[cfg(not(all(
unix,
not(any(target_os = "macos", target_os = "android", target_os = "emscripten"))
)))]
use arboard::Clipboard;
child
.wait_with_output()
.expect("wait for clipboard command output");
}
#[cfg(not(all(
unix,
not(any(target_os = "macos", target_os = "android", target_os = "emscripten"))
)))]
pub fn set_clipboard(content: String) {
let mut clipboard = Clipboard::new().unwrap();
clipboard.set_text(content).unwrap();
let mut clipboard = arboard::Clipboard::new().unwrap();
clipboard.set_text(content).execute(format!("set clipboard to '{content}'"));
}

View File

@ -1,11 +1,16 @@
use chrono::{prelude::DateTime, Utc};
use colored::Colorize;
use imgurs::api::{rate_limit::rate_limit, ImgurClient};
use imgurs::ImgurClient;
use std::time::{Duration, UNIX_EPOCH};
pub async fn credits(client: ImgurClient) {
let i = rate_limit(client).await.expect("send api request");
// get client ratelimit from imgur api
let i = client
.rate_limit()
.await
.expect("send request to imgur api");
// format image upload date
let date = UNIX_EPOCH + Duration::from_secs(i.data.user_reset.try_into().unwrap());
let datetime = DateTime::<Utc>::from(date);
let timestamp_str = datetime.format("%Y-%m-%d %H:%M:%S").to_string();

View File

@ -1,11 +1,15 @@
use colored::Colorize;
use imgurs::api::{delete_image::delete_image as del_img, ImgurClient};
use imgurs::ImgurClient;
pub async fn delete_image(client: ImgurClient, delete_hash: String) {
let i = del_img(client, delete_hash)
// delete image from imgur
client
.delete_image(delete_hash)
.await
.expect("send api request");
println!("{}", i.magenta());
println!(
"{}",
"If Delete Hash was correct the image was deleted!".magenta()
);
}

View File

@ -1,8 +1,14 @@
use imgurs::api::{get_image::get_image, ImgurClient};
use imgurs::ImgurClient;
use super::print_image_info;
pub async fn image_info(client: ImgurClient, id: &str) {
let info = get_image(client, id).await.expect("send api request");
pub async fn image_info(client: ImgurClient, id: String) {
// get a image info from imgur
let info = client
.image_info(id)
.await
.expect("send request to imfur api");
// print image information from imgur
print_image_info(info);
}

View File

@ -8,14 +8,17 @@ pub mod webhook;
use chrono::{prelude::DateTime, Utc};
use colored::Colorize;
use imgurs::api::ImageInfo;
use imgurs::ImageInfo;
use std::time::{Duration, UNIX_EPOCH};
// print image information from imgur
pub fn print_image_info(i: ImageInfo) {
// format image upload date
let d = UNIX_EPOCH + Duration::from_secs(i.data.datetime.try_into().unwrap());
let datetime = DateTime::<Utc>::from(d);
let timestamp_str = datetime.format("%Y-%m-%d %H:%M:%S").to_string();
// image title
if i.data.title != None {
println!(
"{} {}",
@ -26,6 +29,8 @@ pub fn print_image_info(i: ImageInfo) {
.magenta()
);
}
// image description
if i.data.description != None {
println!(
"{} {}",
@ -36,6 +41,8 @@ pub fn print_image_info(i: ImageInfo) {
.magenta()
);
}
// image deletehas
if i.data.deletehash != None {
println!(
"{} {}",

View File

@ -1,10 +1,11 @@
use clap::{Command, IntoApp, Parser, Subcommand};
use clap_complete::{generate, Generator, Shell};
use imgurs::api::ImgurClient;
use imgurs::ImgurClient;
use std::io::{self, stdout};
use crate::cli::{credits::*, delete_image::*, info_image::*, upload_image::*};
// get program name and varsion from Cargo.toml
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
const NAME: Option<&str> = option_env!("CARGO_PKG_NAME");
@ -21,7 +22,7 @@ struct Cli {
#[derive(Subcommand, Debug)]
enum Commands {
#[clap(about = "Print API Rate Limit", display_order = 1)]
#[clap(about = "Print Client Rate Limit", display_order = 1)]
Credits,
#[clap(about = "Upload image to Imgur", display_order = 2)]
@ -37,7 +38,7 @@ enum Commands {
about = "Generate completion file for a shell [bash, elvish, fish, powershell, zsh]",
display_order = 5
)]
Completions { shell: String },
Completions { shell: Shell },
#[clap(about = "Generate man page", display_order = 6)]
Manpage,
@ -50,41 +51,26 @@ fn print_completions<G: Generator>(gen: G, app: &mut Command) {
pub async fn parse(client: ImgurClient) {
let args = Cli::parse();
match &args.command {
Commands::Credits => {
credits(client).await;
}
match args.command {
Commands::Credits => credits(client).await,
Commands::Upload { path } => {
upload_image(client, path).await;
}
Commands::Upload { path } => upload_image(client, path.to_string()).await,
Commands::Delete { delete_hash } => {
delete_image(client, delete_hash.to_string()).await;
}
Commands::Delete { delete_hash } => delete_image(client, delete_hash.to_string()).await,
Commands::Info { id } => {
image_info(client, id).await;
}
Commands::Info { id } => image_info(client, id.to_string()).await,
Commands::Completions { shell } => {
let mut app = Cli::command();
match shell.as_str() {
"bash" => print_completions(Shell::Bash, &mut app),
"elvish" => print_completions(Shell::Elvish, &mut app),
"fish" => print_completions(Shell::Fish, &mut app),
"powershell" => print_completions(Shell::PowerShell, &mut app),
"zsh" => print_completions(Shell::Zsh, &mut app),
_ => panic!("Completions to shell `{shell}`, not found!"),
}
print_completions(shell, &mut app)
}
Commands::Manpage => {
let clap_app = Cli::command();
let man = clap_mangen::Man::new(clap_app);
man.render(&mut io::stdout()).unwrap();
man.render(&mut io::stdout()).expect("generate manpage")
}
}
}

View File

@ -1,14 +1,12 @@
use super::clipboard::set_clipboard;
use imgurs::api::{upload_image::upload_image as upload_img, ImgurClient};
use imgurs::ImgurClient;
use notify_rust::Notification;
use crate::{cli::webhook::send_discord_webhook, config::toml};
use super::print_image_info;
use base64::encode as base64_encode;
use std::{fs::read as fs_read, path::Path};
// show notification
macro_rules! notify (
($notification: expr) => (
if toml::parse().notification.enabled {
@ -17,43 +15,39 @@ macro_rules! notify (
);
);
pub async fn upload_image(client: ImgurClient, path: &str) {
let mut image: String = path.to_string();
pub async fn upload_image(client: ImgurClient, path: String) {
// parse configuration file
let config = toml::parse();
if Path::new(path).exists() {
let bytes = fs_read(path)
.map_err(|err| err.to_string())
.expect("read file");
image = base64_encode(bytes);
} else if !validator::validate_url(path) {
panic!("{path} is not a url")
}
let mut i = upload_img(client, &image).await.unwrap_or_else(|err| {
// upload a image to imgur
let mut i = client.upload_image(path).await.unwrap_or_else(|err| {
notify!(Notification::new()
.summary("Error!")
.body(&format!("Error: {}", &err.to_string()))
.appname("Imgurs")); // I don't think you can set it to error
panic!("{}", err)
panic!("send request to imagur api: {}", err)
});
// change domain to proxy (to be set in config)
if config.imgur.image_cdn != "i.imgur.com" {
i.data.link = i.data.link.replace("i.imgur.com", "cdn.magicuser.cf")
i.data.link = i.data.link.replace("i.imgur.com", &config.imgur.image_cdn)
}
// print image information from imgur
print_image_info(i.clone());
let body = format!("Uploaded {}", i.data.link);
notify!(Notification::new().summary("Imgurs").body(&body));
// send notification that the image has been uploaded
notify!(Notification::new()
.summary("Imgurs")
.body(&format!("Uploaded {}", i.data.link)));
// if enabled copy link to clipboard
if config.clipboard.enabled {
set_clipboard(i.data.link.clone())
}
// if enabled send embed with link and deletehash to discord (something like logger)
if config.discord_webhook.enabled {
send_discord_webhook(i.data.link, i.data.deletehash.unwrap())
.await

View File

@ -3,13 +3,21 @@ use std::error::Error;
use crate::config::toml;
// send embed with link and deletehash to discord (something like logger)
pub async fn send_discord_webhook(
link: String,
deletehash: String,
) -> Result<bool, Box<dyn Error + Send + Sync>> {
) -> Result<(), Box<dyn Error + Send + Sync>> {
// get discord webhook uri from config
let url = toml::parse().discord_webhook.uri;
// create WebhookClient
let client: WebhookClient = WebhookClient::new(&url);
// get program version
let version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown");
// send discord webhook
client
.send(|message| {
message.username("Imgurs").embed(|embed| {
@ -17,14 +25,10 @@ pub async fn send_discord_webhook(
.title(&link)
.description(&format!("Delete Hash ||{deletehash}||"))
.image(&link)
.footer(
&format!(
"Imgurs v{}",
option_env!("CARGO_PKG_VERSION").unwrap_or("unknown")
),
None,
)
.footer(&format!("Imgurs v{version}"), None)
})
})
.await
.await?;
Ok(())
}

View File

@ -1 +1,3 @@
pub mod api;
mod api;
pub use api::*;

View File

@ -2,7 +2,7 @@ mod cli;
mod config;
use cli::parse::parse;
use imgurs::api::ImgurClient;
use imgurs::ImgurClient;
use simple_logger::SimpleLogger;