assistant_daemon 0.1.0

Daemon program for providing many features.
use crate::config::{get_config_dir, get_feature_dir};
use crate::feature::FeatureControl;
use crate::generated::{
    GetWallpaperRequest, GetWallpaperResponse, SetWallpaperRequest, WallpaperUsecase, WpSetting,
};
use anyhow::{bail, Ok, Result};
use async_trait::async_trait;
use axum::http::response;
use chrono::Local;
use reqwest::{self, Client};
use serde::{Deserialize, Serialize};
use std::cell::Cell;
use std::ffi::CString;
use std::fs::{self, File};
use std::io::Cursor;
use std::io::{copy, Write};
use std::path::{self, Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use tokio::time::sleep;
use tracing::{debug, error, info};
#[cfg(target_os = "windows")]
use winapi::um::winuser::{SystemParametersInfoA, SPIF_UPDATEINIFILE, SPI_SETDESKWALLPAPER};

#[cfg(target_os = "windows")]
fn windows_set_wallpaper(path: &Path) -> Result<()> {
    let path_cstring = CString::new(path.to_str().unwrap())?;
    let result = unsafe {
        SystemParametersInfoA(
            SPI_SETDESKWALLPAPER,
            0,
            path_cstring.as_ptr() as *mut _,
            SPIF_UPDATEINIFILE,
        )
    };

    if result == 0 {
        bail!(std::io::Error::last_os_error())
    } else {
        Ok(())
    }
}

/// Note that this is merely for gnome,
/// not for Linux since not all Linux will have a desktop
fn gnome_set_wallpaper(path: &Path) -> Result<()> {
    let output = Command::new("gsettings")
        .arg("set")
        .arg("org.gnome.desktop.background")
        .arg("picture-uri")
        .arg(format!("file://{}", path.to_str().unwrap()))
        .output()
        .expect("Failed to execute command");

    if !output.status.success() {
        bail!(
            "Stdout: {}\nStderr: {}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        )
    }

    // required after Ubuntu 22
    let output = Command::new("gsettings")
        .arg("set")
        .arg("org.gnome.desktop.background")
        .arg("picture-uri-dark")
        .arg(format!("file://{}", path.to_str().unwrap()))
        .output()
        .expect("Failed to execute command");

    if !output.status.success() {
        bail!(
            "Stdout: {}\nStderr: {}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        )
    }

    Ok(())
}

fn set_wallpaper(path: &Path) -> Result<()> {
    #[cfg(target_os = "macos")]
    return macos_set_wallpaper(path);
    #[cfg(target_os = "windows")]
    return windows_set_wallpaper(path);
    #[cfg(target_os = "linux")]
    return gnome_set_wallpaper(path);

    bail!("set wallpaper at unsupported platform")
}

fn macos_set_wallpaper(path: &Path) -> Result<()> {
    let apple_script = format!(
        r#"
    tell application "System Events"
tell every desktop
 set picture to "{}"
end tell
end tell
    "#,
        path.to_str().unwrap()
    );

    // Execute the AppleScript command using `osascript`
    let output = Command::new("/usr/bin/osascript")
        .arg("-e")
        .arg(&apple_script)
        .output()
        .expect("Failed to execute command");

    if !output.status.success() {
        bail!(
            "Stdout: {}\nStderr: {}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        )
    }
    Ok(())
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct GetWallpaperInfoResponse {
    start_date: String,
    end_date: String,
    url: String,
    copyright: String,
    copyright_link: String,
}

fn get_wallpaper_id(start_date: String, mkt: &str, resolution: u32) -> String {
    format!("{}-{}-{}", start_date, mkt, resolution)
}

async fn get_wallpaper(resolution: u32, region: &str) -> Result<GetWallpaperInfoResponse> {
    let url = "https://bing.biturl.top";
    let client = Client::new();
    let response = client
        .get(url)
        .query(&[
            ("resolution", resolution.to_string()),
            ("format", "json".to_string()),
            ("index", "0".to_string()),
            ("mkt", region.to_string()),
        ])
        .send()
        .await?
        .json::<GetWallpaperInfoResponse>()
        .await?;

    Ok(response)
}

pub struct WallpaperUsecaseImpl {
    settings: RwLock<Option<WpSetting>>,
    scheduler_handle: RwLock<Option<JoinHandle<()>>>,
}

impl WallpaperUsecaseImpl {
    pub fn new() -> Self {
        Self {
            settings: Default::default(),
            scheduler_handle: Default::default(),
        }
    }
}

impl Default for WpSetting {
    fn default() -> Self {
        Self {
            res_w: Some(3840),
            mkt: Some("en-US".to_string()),
        }
    }
}

#[async_trait]
impl WallpaperUsecase for WallpaperUsecaseImpl {
    async fn set_wallpaper(&self, request: SetWallpaperRequest) -> Result<()> {
        // Implement the logic to set the wallpaper using the request.id
        Ok(())
    }

    async fn get_wallpaper(&self, request: GetWallpaperRequest) -> Result<GetWallpaperResponse> {
        let wallpaper_info = get_wallpaper(request.res_w, &request.mkt).await?;
        let id = get_wallpaper_id(wallpaper_info.start_date, &request.mkt, request.res_w);
        Ok(GetWallpaperResponse {
            data: wallpaper_info.url,
            id,
        })
    }
}

async fn download_to_file(url: &str, file_path: &Path) -> Result<()> {
    let response = reqwest::get(url).await?;
    let mut file = std::fs::File::create(file_path)?;
    let mut content = Cursor::new(response.bytes().await?);
    std::io::copy(&mut content, &mut file)?;
    Ok(())
}

async fn refresh_wallpaper(res_w: u32, mkt: &String, img_dir: PathBuf) -> Result<()> {
    let res = get_wallpaper(res_w, mkt).await?;
    let id = get_wallpaper_id(res.start_date, mkt, res_w);

    if !img_dir.exists() {
        std::fs::create_dir_all(&img_dir)?;
    }
    let dst = img_dir.join(format!("{}.png", id));
    if !dst.exists() {
        download_to_file(&res.url, &dst).await?;
    } else {
        debug!("skip download wallpaper[id={}] since already existed", id);
    }
    set_wallpaper(&dst)?;
    info!("refreshed wallpaper");
    Ok(())
}

fn duration_until_2359() -> Duration {
    let now = Local::now();
    let end_of_day = now.date_naive().and_hms_opt(23, 59, 0).unwrap();
    let duration_until_end_of_day = end_of_day.signed_duration_since(now.naive_local());
    duration_until_end_of_day.to_std().unwrap()
}

#[async_trait]
impl FeatureControl for WallpaperUsecaseImpl {
    fn name(&self) -> &str {
        "wp"
    }
    async fn enable(&self, settings: Option<serde_json::Value>) -> Result<()> {
        self.update(None, settings).await?;

        let settings = match &*self.settings.read().await {
            Some(settings) => {
                // use user defined settings
                settings.clone()
            }
            None => {
                // use default settings
                Default::default()
            }
        };
        let img_dir = get_feature_dir(self.name());
        // refresh_wallpaper(settings.res_w, &settings.mkt, img_dir.clone()).await?;
        let mut handle = self.scheduler_handle.write().await;
        *handle = Some(tokio::spawn({
            let mkt = settings.mkt.unwrap_or("en-US".to_string());
            let img_dir = img_dir;
            let res_w = settings.res_w.unwrap_or(3840);

            async move {
                loop {
                    match refresh_wallpaper(res_w, &mkt, img_dir.clone()).await {
                        std::result::Result::Ok(_) => {}
                        Err(err) => {
                            error!("failed to refresh wallpaper: {}", err)
                        }
                    };
                    let dur = duration_until_2359();
                    debug!("wait {:?} to refresh wallpaper", dur);
                    sleep(dur).await;
                }
            }
        }));

        Ok(())
    }
    async fn disable(&self) -> Result<()> {
        Ok(())
    }
    async fn update(
        &self,
        old_settings: Option<serde_json::Value>,
        settings: Option<serde_json::Value>,
    ) -> Result<()> {
        let mut sl = self.settings.write().await;
        *sl = if settings.is_none() {
            None
        } else {
            serde_json::from_value(settings.unwrap())?
        };
        Ok(())
    }
    async fn lid_change(&self, open: bool) {}
}