Initial commit

This commit is contained in:
MedzikUser 2022-01-22 22:02:51 +01:00
commit 9cbb2ea149
No known key found for this signature in database
GPG Key ID: A5FAC1E185C112DB
19 changed files with 1716 additions and 0 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = false

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1268
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

38
Cargo.toml Normal file
View File

@ -0,0 +1,38 @@
[package]
name = "imgurs"
version = "0.1.0"
edition = "2021"
[dependencies]
dirs = "4.0.0"
serde = "1.0.134"
serde_derive = "1.0.134"
shadow = "0.0.1"
toml = "0.5.8"
serde_json = "1.0.75"
chrono = "0.4.19"
anyhow = "1.0.53"
base64 = "0.13.0"
[dependencies.clap]
version = "3.0.7"
features = ["derive", "cargo", "unicode"]
[dependencies.log]
version = "0.4.14"
features = ["release_max_level_info"]
[dependencies.simple_logger]
version = "2.1.0"
default-features = false
features = ["colors"]
[dependencies.reqwest]
version = "0.11.9"
features = ["json"]
[dependencies.tokio]
version = "1.15.0"
features = ["full"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

29
LICENSE Normal file
View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2022, MedzikUser
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

31
src/api/configuration.rs Normal file
View File

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

26
src/api/delete_image.rs Normal file
View File

@ -0,0 +1,26 @@
use crate::api::configuration::{api_url, ImgurHandle};
pub async fn delete_image(c: ImgurHandle, delete_hash: String) -> Result<String, String> {
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
let res = c
.client
.delete(api_url!(format!("image/{delete_hash}")))
.header("Authorization", format!("Client-ID {}", c.client_id))
.header(
"User-Agent",
format!("Imgur/{:?}", VERSION.unwrap_or("unknown")),
)
.send()
.await
.map_err(|err| err.to_string())?;
let status = res.status();
if status.is_client_error() || status.is_server_error() {
let body = res.text().await.map_err(|err| err.to_string())?;
Err(format!("server returned non-successful status code = {status}. body = {body}"))
} else {
Ok("If the delete hash was correct the image was deleted!".to_string())
}
}

4
src/api/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod rate_limit;
pub mod configuration;
pub mod upload_image;
pub mod delete_image;

48
src/api/rate_limit.rs Normal file
View File

@ -0,0 +1,48 @@
use crate::api::configuration::{api_url, ImgurHandle};
use serde_derive::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct RateLimitInfo {
pub data: RateLimitData,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RateLimitData {
#[serde(rename = "UserLimit")]
pub user_limit: i32,
#[serde(rename = "UserRemaining")]
pub user_remaining: i32,
#[serde(rename = "UserReset")]
pub user_reset: i32,
#[serde(rename = "ClientLimit")]
pub client_limit: i32,
#[serde(rename = "ClientRemaining")]
pub client_remaining: i32,
}
pub async fn rate_limit(c: ImgurHandle) -> Result<RateLimitInfo, String> {
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
let res = c
.client
.get(api_url!("credits"))
.header("Authorization", format!("Client-ID {}", c.client_id))
.header(
"User-Agent",
format!("Imgur/{:?}", VERSION.unwrap_or("unknown")),
)
.send()
.await
.map_err(|err| err.to_string())?;
let status = res.status();
if status.is_client_error() || status.is_server_error() {
let body = res.text().await.map_err(|err| err.to_string())?;
Err(format!("server returned non-successful status code = {status}. body = {body}"))
} else {
let content: RateLimitInfo = res.json().await.map_err(|err| err.to_string())?;
Ok(content)
}
}

59
src/api/upload_image.rs Normal file
View File

@ -0,0 +1,59 @@
use crate::api::configuration::{api_url, ImgurHandle};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize)]
pub struct ImageInfo {
pub data: ImageInfoData,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ImageInfoData {
pub id: String,
pub title: Option<String>,
pub description: Option<String>,
pub datetime: i32,
#[serde(rename = "type")]
pub img_type: String,
pub animated: bool,
pub width: i32,
pub height: i32,
pub size: i32,
pub views: i32,
pub bandwidth: i64,
pub favorite: bool,
pub deletehash: String,
pub link: String,
}
pub async fn upload_image(c: ImgurHandle, image: &str) -> Result<ImageInfo, String> {
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
let mut form = HashMap::new();
form.insert("image", image.to_string());
let res = c
.client
.post(api_url!("image"))
.header("Authorization", format!("Client-ID {}", c.client_id))
.header(
"User-Agent",
format!("Imgurs/{:?}", VERSION.unwrap_or("unknown")),
)
.form(&form)
.send()
.await
.map_err(|err| err.to_string())?;
let status = res.status();
if status.is_client_error() || status.is_server_error() {
let body = res.text().await.map_err(|err| err.to_string())?;
Err(format!("server returned non-successful status code = {status}. body = {body}"))
} else {
let content: ImageInfo = res.json().await.map_err(|err| err.to_string())?;
Ok(content)
}
}

61
src/cli/cli.rs Normal file
View File

@ -0,0 +1,61 @@
use clap::{Parser, Subcommand, AppSettings};
use imgurs::api::configuration::ImgurHandle;
use crate::cli::*;
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
#[derive(Parser, Debug)]
#[clap(
name = "imgur",
about = "Imgur API CLI", long_about = None,
version = VERSION.unwrap_or("unknown")
)]
struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
#[clap(about = "Get API Rate Limit")]
Credits,
#[clap(
setting(AppSettings::ArgRequiredElseHelp),
about = "Upload image to imgur"
)]
Upload {
path: String
},
#[clap(
setting(AppSettings::ArgRequiredElseHelp),
about = "Upload image to imgur"
)]
Delete {
delete_hash: String
},
}
pub async fn parse(client: ImgurHandle) {
let args = Cli::parse();
match &args.command {
Commands::Credits => {
credits::credits(client)
.await;
}
Commands::Upload { path } => {
upload_image::upload_image(client, path)
.await;
}
Commands::Delete { delete_hash } => {
delete_image::delete_image(client, delete_hash.to_string())
.await;
}
}
}

28
src/cli/credits.rs Normal file
View File

@ -0,0 +1,28 @@
use imgurs::api::configuration::ImgurHandle;
use imgurs::api::rate_limit::*;
use log::{info, error};
use chrono::prelude::DateTime;
use chrono::Utc;
use std::time::{UNIX_EPOCH, Duration};
pub async fn credits(client: ImgurHandle) {
match rate_limit(client).await {
Ok(i) => {
let d = UNIX_EPOCH + Duration::from_secs(i.data.user_reset.try_into().unwrap());
let datetime = DateTime::<Utc>::from(d);
let timestamp_str = datetime.format("%Y-%m-%d %H:%M:%S").to_string();
info!("User Limit {}", i.data.user_limit);
info!("User Remaining {}", i.data.user_remaining);
info!("User Reset {} (UTC)", timestamp_str);
info!("Client Limit {}", i.data.client_limit);
info!("Client Remaining {}", i.data.client_remaining);
}
Err(e) => {
error!("{}", e);
}
}
}

16
src/cli/delete_image.rs Normal file
View File

@ -0,0 +1,16 @@
use imgurs::api::configuration::ImgurHandle;
use imgurs::api;
use log::{info, error};
pub async fn delete_image(client: ImgurHandle, delete_hash: String) {
match api::delete_image::delete_image(client, delete_hash).await {
Ok(i) => {
info!("{i}")
}
Err(e) => {
error!("{}", e);
}
}
}

4
src/cli/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod cli;
pub mod credits;
pub mod upload_image;
pub mod delete_image;

45
src/cli/upload_image.rs Normal file
View File

@ -0,0 +1,45 @@
use imgurs::api::configuration::ImgurHandle;
use log::{info, error};
use std::path::Path;
use std::fs;
use base64;
use chrono::prelude::DateTime;
use chrono::Utc;
use std::time::{UNIX_EPOCH, Duration};
pub async fn upload_image(client: ImgurHandle, path: &str) {
let image: String;
if Path::new(path).exists() {
let bytes = fs::read(path).map_err(|err| err.to_string()).unwrap();
image = base64::encode(bytes);
} else {
image = path.to_string()
}
match imgurs::api::upload_image::upload_image(client, &image).await {
Ok(i) => {
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();
info!("ID {}", i.data.id);
info!("Upload Date {} (UTC)", timestamp_str);
info!("Type {}", i.data.img_type);
info!("Width {}", i.data.width);
info!("Height {}", i.data.height);
info!("File Size {} KB", i.data.size / 1000);
info!("Views {}", i.data.views);
info!("Bandwidth {}", i.data.bandwidth);
info!("Delete Hash {}", i.data.deletehash);
info!("Link {}", i.data.link);
}
Err(e) => {
error!("{e}");
}
}
}

1
src/config/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod toml;

25
src/config/toml.rs Normal file
View File

@ -0,0 +1,25 @@
use toml::from_str;
use serde_derive::Deserialize;
use dirs::config_dir;
use std::fs::read_to_string;
#[derive(Debug, Deserialize)]
pub struct Config {
pub imgur: ConfigImgur,
}
#[derive(Debug, Deserialize)]
pub struct ConfigImgur {
pub id: String,
}
pub fn parse() -> Result<Config, String> {
let config_dir = config_dir().unwrap();
let file_dir: String = String::from(config_dir.to_string_lossy()) + "/imgur/config.toml";
let toml_str = read_to_string(file_dir).map_err(|err| err.to_string())?;
let decode = from_str(&toml_str).map_err(|err| err.to_string())?;
Ok(decode)
}

1
src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod api;

19
src/main.rs Normal file
View File

@ -0,0 +1,19 @@
mod config;
mod cli;
use cli::cli::parse;
use simple_logger::SimpleLogger;
use imgurs::api::configuration::ImgurHandle;
#[tokio::main]
async fn main() {
SimpleLogger::new().init().unwrap();
let config = config::toml::parse().unwrap();
let client = ImgurHandle::new((&config.imgur.id).to_string());
parse(client).await;
}