nu_plugin_twitch 0.1.1

A Nu Shell plugin to interact with Twitch IRC and API, making scripting easier.
Documentation
use std::path::PathBuf;

use async_trait::async_trait;

use nu_protocol::LabeledError;
use serde::{Deserialize, Serialize};
use twitch_irc::login::{TokenStorage, UserAccessToken};

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ApplicationCredentials {
    pub client_id: String,
    pub(crate) client_secret: String,
}

impl ApplicationCredentials {
    pub fn credentials_path() -> PathBuf {
        dirs::data_local_dir()
            .unwrap()
            .join("nu_plugin_twitch")
            .join("creds.json")
    }

    /// Load Twitch application credentials from a file path
    pub fn load() -> Result<Self, LabeledError> {
        let json = std::fs::read_to_string(Self::credentials_path()).map_err(|e| {
            LabeledError::new("Failed to read app.json file")
                .with_inner(LabeledError::new(e.to_string()))
        })?;
        serde_json::from_str(&json).map_err(|e| {
            LabeledError::new("Failed to read app credentials")
                .with_inner(LabeledError::new(e.to_string()))
        })
    }

    /// Save Twitch application credential to the dedicated path
    pub fn save(&self) -> Result<(), LabeledError> {
        let creds_path = Self::credentials_path();
        std::fs::create_dir_all(creds_path.clone().parent().unwrap()).map_err(|e| {
            LabeledError::new("Failed to create LocalData directory")
                .with_inner(LabeledError::new(e.to_string()))
        })?;
        let json = serde_json::to_string(self).map_err(|e| {
            LabeledError::new("Failed to serialize app credentials")
                .with_inner(LabeledError::new(e.to_string()))
        })?;
        std::fs::write(creds_path.clone(), json).map_err(|e| {
            LabeledError::new(format!("Failed to write credentials to {creds_path:?}"))
                .with_inner(LabeledError::new(e.to_string()))
        })
    }
}

#[derive(Debug)]
pub(crate) struct ProfileTokenProvider {
    pub profile: Option<String>,
}

impl ProfileTokenProvider {
    pub fn profile_name(&self) -> String {
        self.profile.clone().unwrap_or("default".to_string())
    }

    pub fn profile_path(&self) -> PathBuf {
        dirs::data_local_dir()
            .unwrap()
            .join("nu_plugin_twitch")
            .join(format!("token_{}.json", self.profile_name()))
    }
}

#[async_trait]
impl TokenStorage for ProfileTokenProvider {
    type LoadError = LabeledError;
    type UpdateError = LabeledError;

    async fn load_token(&mut self) -> Result<UserAccessToken, Self::LoadError> {
        let path = self.profile_path();
        let json = std::fs::read_to_string(path.clone()).map_err(|e| {
            LabeledError::new(format!("Failed to read token file {path:?}"))
                .with_inner(LabeledError::new(e.to_string()))
                .with_help(format!(
                    "Did you meant to use another profile than `{}`",
                    self.profile_name()
                ))
        })?;
        serde_json::from_str(&json).map_err(|e| {
            LabeledError::new(format!("Failed to parse token file {path:?}"))
                .with_inner(LabeledError::new(e.to_string()))
        })
    }

    async fn update_token(&mut self, token: &UserAccessToken) -> Result<(), Self::UpdateError> {
        let token_path = self.profile_path();
        std::fs::create_dir_all(token_path.clone().parent().unwrap()).map_err(|e| {
            LabeledError::new("Failed to create LocalData directory")
                .with_inner(LabeledError::new(e.to_string()))
        })?;
        let json = serde_json::to_string(token).map_err(|e| {
            LabeledError::new("Failed to serialize token")
                .with_inner(LabeledError::new(e.to_string()))
        })?;
        std::fs::write(token_path.clone(), json).map_err(|e| {
            LabeledError::new(format!("Failed to write token ot file {token_path:?}"))
                .with_inner(LabeledError::new(e.to_string()))
        })
    }
}