#![doc = include_str!("../README.md")]
mod model;
pub mod util;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use ureq::Agent;
pub use self::model::Proxy;
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
Fetch(ureq::Error),
Parse,
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Fetch(e) => Some(e),
Error::Parse => None,
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Fetch(e) => write!(f, "Failed to fetch subscription content: {}", e),
Error::Parse => write!(f, "Failed to parse subscription content"),
}
}
}
impl From<ureq::Error> for Error {
fn from(e: ureq::Error) -> Self {
Error::Fetch(e)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
#[derive(serde::Serialize, serde::Deserialize)]
pub enum SubscriptionKind {
#[serde(rename = "clash")]
ClashCompatible,
}
#[derive(Debug, Clone)]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Subscription {
pub kind: SubscriptionKind,
pub url: util::Uri,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_agent: Option<Arc<str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proxy: Option<util::Proxy>,
}
impl Subscription {
pub fn fetch(&self) -> Result<Vec<Proxy>, Error> {
static AGENT: LazyLock<Agent> = LazyLock::new(|| {
const DEFAULT_USER_AGENT: &str = "mihomo.party/v1.8.4 (clash.meta)";
Agent::config_builder()
.timeout_global(Some(Duration::from_secs(5)))
.user_agent(DEFAULT_USER_AGENT)
.build()
.into()
});
let mut request = AGENT
.get(&*self.url)
.config()
.proxy(self.proxy.as_deref().cloned());
if let Some(user_agent) = &self.user_agent {
request = request.user_agent(user_agent);
}
let response = request.build().call()?.into_body().read_to_vec()?;
let response = String::from_utf8(response).map_err(|_e| Error::Parse)?;
match self.kind {
SubscriptionKind::ClashCompatible => Proxy::parse_clash_compatible(&response),
}
}
}
wrapper_lite::wrapper!(
#[wrapper_impl(DebugName)]
pub struct Task {
pending: Subscription,
interval: Option<Duration>,
proxies_tx: mpsc::Sender<Vec<Proxy>>,
}
);
impl Task {
pub fn new(
pending: Subscription,
interval: Option<Duration>,
) -> (Self, mpsc::Receiver<Vec<Proxy>>) {
let (proxies_tx, proxies_rx) = mpsc::channel(1);
(
Self {
pending,
interval,
proxies_tx,
},
proxies_rx,
)
}
pub async fn spawn(self) -> JoinHandle<()> {
tokio::spawn(async move {
loop {
let pending = self.pending.clone();
let proxies = tokio::task::spawn_blocking(move || pending.fetch()).await;
match proxies {
Ok(Ok(proxies)) => {
let Ok(()) = self.proxies_tx.send(proxies).await else {
tracing::info!("Subscription task channel closed, exiting task.");
return;
};
}
Ok(Err(e)) => {
tracing::error!("Failed to fetch subscription: {:?}", e);
}
Err(e) => {
tracing::error!("Failed to join subscription task: {:?}", e);
}
}
tokio::time::sleep(self.interval.unwrap_or(Duration::from_mins(30))).await;
}
})
}
}