ascella-cli 0.5.2

Ascella commandline app
use std::fmt::Display;
use std::io::{Error, ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::{env, fs};

use crate::{session_type, take_ss, ScreenshotKind, SessionKind};
use anyhow::Result;
use clap::crate_version;
use home::home_dir;
use lazy_static::lazy_static;
use native_dialog::{MessageDialog, MessageType};
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::multipart::{Form, Part};
use reqwest::{Client, Method, RequestBuilder};
use serde::{Deserialize, Serialize};
use serde_json::Value;

lazy_static! {
    static ref CLIENT: OnceCell<Client> = OnceCell::new();
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AscellaConfig {
    #[serde(rename = "authorization")]
    pub auth: Option<String>,
    pub headers: Option<String>,
    pub command: Option<String>,
}

pub fn update_config<T: Into<PathBuf>>(path: T) -> Result<(), Error> {
    let path: PathBuf = path.into();
    let r: Value = std::fs::read_to_string(&path)
        .map(|r| serde_json::from_str(&r))
        .map_err(|x| Error::new(ErrorKind::Other, x.to_string()))?
        .map_err(|x| Error::new(ErrorKind::Other, x.to_string()))?;

    let config: AscellaConfig = serde_json::from_str(
        &serde_json::to_string(&r["Headers"])
            .map_err(|x| Error::new(ErrorKind::Other, x.to_string()))?,
    )
    .map_err(|x| Error::new(ErrorKind::Other, x.to_string()))?;

    let mut write_path = home_dir().unwrap();

    write_path.extend(&[".ascella"]);
    fs::create_dir_all(&write_path)?;
    write_path.extend(&["config.toml"]);
    std::fs::write(
        &write_path,
        toml::to_string_pretty(&config).map_err(|x| Error::new(ErrorKind::Other, x.to_string()))?,
    )
    .map_err(|x| Error::new(ErrorKind::Other, x.to_string()))?;
    Ok(())
}

pub async fn screenshot(t: ScreenshotKind) -> Result<()> {
    let mut write_path = home_dir().unwrap();
    write_path.extend(&[".ascella", "config.toml"]);

    let config: AscellaConfig = if let Ok(config_raw) = std::fs::read_to_string(write_path) {
        if let Ok(config) = toml::from_str(&config_raw) {
            config
        } else {
            println!("Your config is invalid please use a valid ascella config");
            MessageDialog::new()
                .set_type(MessageType::Info)
                .set_title("invalid config")
                .set_text("Your config is invalid please use a valid ascella config")
                .show_alert()
                .unwrap();
            return Ok(());
        }
    } else {
        println!("config not detected please upload your config");
        MessageDialog::new()
      .set_type(MessageType::Info)
      .set_title("config not detected please upload your config")
      .set_text("config not detected please upload your config\n\nPlease add a config file you can do this using the gui")
      .show_alert()
      .unwrap();
        return Ok(());
    };

    let mut path = home_dir().unwrap();

    path.extend(&[".ascella", "images"]);
    std::fs::create_dir_all(&path).unwrap();
    let filename = chrono::offset::Local::now()
        .format("%Y-%m-%d_%H-%M-%S.png")
        .to_string();
    path.extend(&[&filename]);
    if let Some(command) = config.command {
        let replaced = command.replace(
            "%image",
            &path.clone().into_os_string().into_string().unwrap(),
        );
        let mut parts = replaced.trim().split_whitespace();

        let command = parts.next().unwrap();

        let args = parts;

        Command::new(command).args(args).spawn().unwrap();
    } else {
        take_ss(
            t,
            path.clone().into_os_string().into_string().unwrap(),
            true,
        );
    }
    upload(path).await.unwrap();
    Ok(())
}

use thiserror::Error;
use tokio::sync::OnceCell;

#[derive(Error, Debug)]
pub enum AscellaError {
    #[error("Invalid config, config file not found. use the config subcommand to set the config!")]
    NoInvalidConfig,
    #[error("Config is not valid toml!")]
    ConfigParsingError,
    #[error("Invalid auth token! please upload your new config!")]
    InvalidAuthToken,
}

pub fn get_config() -> Result<AscellaConfig, AscellaError> {
    let mut write_path = home_dir().unwrap();
    write_path.extend(&[".ascella", "config.toml"]);

    let config_raw = if let Ok(config_raw) = std::fs::read_to_string(write_path) {
        config_raw
    } else {
        return Err(AscellaError::NoInvalidConfig);
    };
    if let Ok(config) = toml::from_str(&config_raw) {
        Ok(config)
    } else {
        Err(AscellaError::ConfigParsingError)
    }
}

pub fn get_client() -> Result<Client> {
    match CLIENT.get() {
        Some(client) => Ok(client.clone()),
        None => {
            let config = get_config()?;
            let mut headers = HeaderMap::new();
            headers.append(
                "authorization",
                HeaderValue::from_str(&config.auth.expect("No auth"))?,
            );

            let client = reqwest::ClientBuilder::new()
                .default_headers(headers)
                .user_agent(format!(
                    "Ascella-uploader/{} ( {} )",
                    crate_version!(),
                    env::consts::OS
                ))
                .build()?;
            CLIENT.set(client)?;
            get_client()
        }
    }
}

/// Change this if your self hosting ascella or using it for your own project!
const PATH: &str = "https://ascella.wtf/v2/ascella";

#[inline]
pub fn do_req<T: Display>(method: Method, path: T) -> Result<RequestBuilder> {
    let req = get_client()?.request(method, format!("{PATH}/{}", path));

    Ok(req)
}

pub async fn upload<P: AsRef<Path>>(path: P) -> Result<String> {
    let bytes = fs::read(path);
    if bytes.is_err() {
        return Ok(String::new());
    }

    let form = Form::new().part("file", Part::bytes(bytes.unwrap()));

    let resp = do_req(Method::POST, "upload")?
        .multipart(form)
        .send()
        .await?;

    let text = resp.text().await.unwrap();
    let r: Value = serde_json::from_str(&text).unwrap();
    let url = r["url"].as_str().expect("Invalid image type");
    println!("{url}");

    let session_kid = session_type();

    let backend = match session_kid {
        SessionKind::Wayland => Some(ClipboardBackend::Wayland),
        SessionKind::X11 => Some(ClipboardBackend::Xorg),
        _ => None,
    };

    copy(url.to_owned(), backend);

    Ok(url.to_owned())
}

pub enum ClipboardBackend {
    Wayland,
    Xorg,
}

#[cfg(not(target_os = "linux"))]
fn copy(t: String, backend: Option<ClipboardBackend>) {
    use clipboard2::{Clipboard, SystemClipboard};
    let clipboard = SystemClipboard::new().unwrap();
    clipboard.set_string_contents(t).unwrap();
}

#[cfg(target_os = "linux")]
fn copy(t: String, backend: Option<ClipboardBackend>) {
    if let Some(backend) = backend {
        match backend {
            ClipboardBackend::Xorg => {
                let child = Command::new("xclip")
                    .arg("-selection")
                    .arg("clipboard")
                    .stdin(Stdio::piped())
                    .stdout(Stdio::piped())
                    .spawn();
                if let Ok(mut child) = child {
                    {
                        let child_stdin = child.stdin.as_mut();
                        if let Some(child_stdin) = child_stdin {
                            child_stdin.write_all((&t).to_string().as_bytes()).ok();
                        }
                    }
                    let _ = child.wait().ok();
                }
            }
            ClipboardBackend::Wayland => {
                Command::new("wl-copy").arg(&t).spawn().ok();
            }
        }
    }
}