mirror of
https://github.com/MedzikUser/imgurs
synced 2024-08-15 00:03:19 +00:00
Initial commit
This commit is contained in:
commit
9cbb2ea149
19 changed files with 1716 additions and 0 deletions
12
.editorconfig
Normal file
12
.editorconfig
Normal 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
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
1268
Cargo.lock
generated
Normal file
1268
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
38
Cargo.toml
Normal file
38
Cargo.toml
Normal 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
29
LICENSE
Normal 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
31
src/api/configuration.rs
Normal 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
26
src/api/delete_image.rs
Normal 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
4
src/api/mod.rs
Normal 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
48
src/api/rate_limit.rs
Normal 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
59
src/api/upload_image.rs
Normal 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
61
src/cli/cli.rs
Normal 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
28
src/cli/credits.rs
Normal 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
16
src/cli/delete_image.rs
Normal 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
4
src/cli/mod.rs
Normal 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
45
src/cli/upload_image.rs
Normal 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
1
src/config/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod toml;
|
25
src/config/toml.rs
Normal file
25
src/config/toml.rs
Normal 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
1
src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod api;
|
19
src/main.rs
Normal file
19
src/main.rs
Normal 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;
|
||||||
|
}
|
Loading…
Reference in a new issue