convertor 2.6.12

A profile converter for surge/clash.
Documentation
use crate::common::cache::{
    CACHED_AUTH_TOKEN_KEY, CACHED_PROFILE_KEY, CACHED_SUB_LOGS_KEY, CACHED_SUB_URL_KEY, Cache, CacheKey,
};
use crate::common::ext::NonEmptyOptStr;
use crate::config::client_config::ProxyClient;
use crate::config::provider_config::ApiConfig;
use crate::error::ProviderApiError;
use crate::provider_api::api_response::ApiFailed;
use crate::provider_api::boslife_api::BosLifeLogs;
use headers::UserAgent;
use reqwest::header::{HeaderName, HeaderValue};
use reqwest::{Method, Request};
use std::fmt::Debug;
use std::str::FromStr;
use tracing::error;
use url::Url;

type Result<T> = core::result::Result<T, ProviderApiError>;

#[async_trait::async_trait]
pub trait ProviderApiTrait: Clone + Send {
    fn api_config(&self) -> &ApiConfig;

    fn build_raw_url(&self, client: ProxyClient) -> Url;

    fn client(&self) -> &reqwest::Client;

    fn get_raw_profile_request(&self, raw_sub_url: Url, user_agent: UserAgent) -> Result<Request> {
        self.client()
            .request(Method::GET, raw_sub_url)
            .header("user-agent", user_agent.as_str())
            .build()
            .map_err(|e| ProviderApiError::BuildRequestError {
                name: "[get_raw_profile]".to_string(),
                source: e,
            })
    }

    fn login_request(&self) -> Result<Request>;

    fn get_sub_request(&self, auth_token: impl AsRef<str>) -> Result<Request>;

    fn reset_sub_request(&self, auth_token: impl AsRef<str>) -> Result<Request>;

    fn get_sub_logs_request(&self, auth_token: impl AsRef<str>) -> Result<Request>;

    fn cached_profile(&self) -> &Cache<String, String>;

    fn cached_auth_token(&self) -> &Cache<String, String>;

    fn cached_sub_url(&self) -> &Cache<String, String>;

    fn cached_sub_logs(&self) -> &Cache<String, BosLifeLogs>;

    async fn execute<Req, Resp, ReqFut, RepFut, R>(
        &self,
        name: String,
        request_future: Req,
        response_future: Resp,
    ) -> Result<R>
    where
        R: Debug + Clone + Send,
        Req: FnOnce() -> ReqFut + Send,
        Resp: FnOnce(String) -> RepFut + Send,
        ReqFut: Future<Output = Result<Request>> + Send,
        RepFut: Future<Output = Result<R>> + Send,
    {
        let mut request = request_future().await?;
        self.api_config().headers.iter().for_each(|(key, value)| {
            if let (Ok(name), Ok(value)) = (
                HeaderName::from_str(key.as_str()),
                HeaderValue::from_str(value.as_str()),
            ) {
                if !name.as_str().is_empty() && !value.as_bytes().is_empty() {
                    request.headers_mut().insert(name, value);
                }
            }
        });
        let method = request.method().clone();
        let url = request.url().clone();
        let request_headers = request.headers().clone();
        let response = self
            .client()
            .execute(request)
            .await
            .map_err(|e| ProviderApiError::ResponseError {
                name: format!("[{}] {}", method.as_str().to_uppercase(), url),
                source: e,
            })?;
        let response_status = response.status();
        let response_headers = response.headers().clone();
        let response_text = response.text().await.map_err(|e| ProviderApiError::ResponseError {
            name: format!("[{}] {}", method.as_str().to_uppercase(), url),
            source: e,
        })?;
        if response_status.is_success() {
            let resp = response_future(response_text).await?;
            Ok(resp)
        } else {
            let failed = ApiFailed {
                request_url: url,
                request_method: method,
                request_headers,
                response_status,
                response_headers,
                response_body: response_text,
            };
            error!("{}", failed);
            Err(ProviderApiError::ApiFailed {
                name,
                source: Box::new(failed),
            })
        }
    }

    async fn get_raw_profile(&self, client: ProxyClient, user_agent: UserAgent) -> Result<String> {
        let raw_sub_url = self.build_raw_url(client);
        let key = CacheKey::new(CACHED_PROFILE_KEY, raw_sub_url.to_string(), Some(client));
        let result = self
            .cached_profile()
            .try_get_with(key, async {
                self.execute(
                    "获取原始订阅内容".to_string(),
                    || async { self.get_raw_profile_request(raw_sub_url.clone(), user_agent.clone()) },
                    |text| async { Ok(text) },
                )
                .await
            })
            .await?;
        Ok(result)
    }

    async fn login(&self) -> Result<String> {
        if let Some(auth_token) = self.api_config().headers.get("Authorization").filter_non_empty() {
            return Ok(auth_token.to_string());
        }
        let key = CacheKey::new(CACHED_AUTH_TOKEN_KEY, self.api_config().login_url().to_string(), None);
        let result = self
            .cached_auth_token()
            .try_get_with(key, async {
                self.execute(
                    "登录服务商".to_string(),
                    || async { self.login_request() },
                    |text| async move {
                        let json_path = &self.api_config().login_api.json_path;
                        jsonpath_lib::select_as::<String>(&text, json_path)
                            .map_err(|e| ProviderApiError::JsonPathError {
                                name: "[登录服务商]".to_string(),
                                path: json_path.clone(),
                                source: e,
                            })?
                            .into_iter()
                            .next()
                            .ok_or(ProviderApiError::JsonPathNotFound {
                                name: "[登录服务商]".to_string(),
                                path: json_path.clone(),
                            })
                    },
                )
                .await
            })
            .await
            .map_err(|e| ProviderApiError::InnerError(e))?;
        Ok(result)
    }

    async fn get_sub_url(&self) -> Result<Url> {
        let key = CacheKey::new(CACHED_SUB_URL_KEY, self.api_config().get_sub_url().to_string(), None);
        let result = self
            .cached_sub_url()
            .try_get_with(key, async {
                self.execute(
                    "获取原始订阅链接".to_string(),
                    || async {
                        let auth_token = self.login().await?;
                        self.get_sub_request(auth_token)
                    },
                    |text| async move {
                        let json_path = &self.api_config().get_sub_api.json_path;
                        jsonpath_lib::select_as::<String>(&text, json_path)
                            .map_err(|e| ProviderApiError::JsonPathError {
                                name: "[获取原始订阅链接]".to_string(),
                                path: json_path.clone(),
                                source: e,
                            })?
                            .into_iter()
                            .next()
                            .ok_or(ProviderApiError::JsonPathNotFound {
                                name: "[获取原始订阅链接]".to_string(),
                                path: json_path.clone(),
                            })
                    },
                )
                .await
            })
            .await?;

        let result = Url::parse(result.as_str())?;
        Ok(result)
    }

    async fn reset_sub_url(&self) -> Result<Url> {
        let response = self
            .execute(
                "重置订阅链接".to_string(),
                || async {
                    let auth_token = self.login().await?;
                    self.reset_sub_request(auth_token)
                },
                |text| async move {
                    let json_path = &self.api_config().reset_sub_api.json_path;
                    jsonpath_lib::select_as::<String>(&text, json_path)
                        .map_err(|e| ProviderApiError::JsonPathError {
                            name: "[重置订阅链接]".to_string(),
                            path: json_path.clone(),
                            source: e,
                        })?
                        .into_iter()
                        .next()
                        .ok_or(ProviderApiError::JsonPathNotFound {
                            name: "[重置订阅链接]".to_string(),
                            path: json_path.clone(),
                        })
                },
            )
            .await?;
        Ok(Url::parse(&response)?)
    }

    async fn get_sub_logs(&self) -> Result<BosLifeLogs> {
        let sub_logs_url = self
            .api_config()
            .sub_logs_url()
            .ok_or(ProviderApiError::Other("订阅日志接口未配置".to_string()))?;
        let key = CacheKey::new(CACHED_SUB_LOGS_KEY, sub_logs_url.to_string(), None);
        let result = self
            .cached_sub_logs()
            .try_get_with(key, async {
                self.execute(
                    "获取订阅日志".to_string(),
                    || async {
                        let auth_token = self.login().await?;
                        self.get_sub_logs_request(auth_token)
                    },
                    |text| async move {
                        let json_path = self
                            .api_config()
                            .sub_logs_api
                            .as_ref()
                            .map(|a| &a.json_path)
                            .ok_or(ProviderApiError::Other("订阅日志接口未配置 json_path".to_string()))?
                            .clone();
                        jsonpath_lib::select_as::<BosLifeLogs>(&text, &json_path)
                            .map_err(|e| ProviderApiError::JsonPathError {
                                name: "[获取订阅日志]".to_string(),
                                path: json_path.clone(),
                                source: e,
                            })?
                            .into_iter()
                            .next()
                            .ok_or(ProviderApiError::JsonPathNotFound {
                                name: "[获取订阅日志]".to_string(),
                                path: json_path.clone(),
                            })
                    },
                )
                .await
            })
            .await?;
        Ok(result)
    }
}