reala_subscription/
lib.rs1#![doc = include_str!("../README.md")]
2
3mod model;
4pub mod util;
5
6use std::sync::{Arc, LazyLock};
7use std::time::Duration;
8
9use tokio::sync::mpsc;
10use tokio::task::JoinHandle;
11use ureq::Agent;
12
13pub use self::model::Proxy;
14
15#[non_exhaustive]
16#[derive(Debug)]
17pub enum Error {
19 Fetch(ureq::Error),
21
22 Parse,
24}
25
26impl std::error::Error for Error {
27 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
28 match self {
29 Error::Fetch(e) => Some(e),
30 Error::Parse => None,
31 }
32 }
33}
34
35impl std::fmt::Display for Error {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Error::Fetch(e) => write!(f, "Failed to fetch subscription content: {}", e),
39 Error::Parse => write!(f, "Failed to parse subscription content"),
40 }
41 }
42}
43
44impl From<ureq::Error> for Error {
45 fn from(e: ureq::Error) -> Self {
46 Error::Fetch(e)
47 }
48}
49
50#[non_exhaustive]
51#[derive(Debug, Clone, Copy)]
52#[derive(serde::Serialize, serde::Deserialize)]
53pub enum SubscriptionKind {
57 #[serde(rename = "clash")]
58 ClashCompatible,
61}
62
63#[derive(Debug, Clone)]
64#[derive(serde::Serialize, serde::Deserialize)]
65pub struct Subscription {
67 pub kind: SubscriptionKind,
69
70 pub url: util::Uri,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub user_agent: Option<Arc<str>>,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub proxy: Option<util::Proxy>,
84}
85
86impl Subscription {
87 pub fn fetch(&self) -> Result<Vec<Proxy>, Error> {
91 static AGENT: LazyLock<Agent> = LazyLock::new(|| {
92 const DEFAULT_USER_AGENT: &str = "mihomo.party/v1.8.4 (clash.meta)";
93
94 Agent::config_builder()
95 .timeout_global(Some(Duration::from_secs(5)))
96 .user_agent(DEFAULT_USER_AGENT)
97 .build()
98 .into()
99 });
100
101 let mut request = AGENT
102 .get(&*self.url)
103 .config()
104 .proxy(self.proxy.as_deref().cloned());
105
106 if let Some(user_agent) = &self.user_agent {
107 request = request.user_agent(user_agent);
108 }
109
110 let response = request.build().call()?.into_body().read_to_vec()?;
111
112 let response = String::from_utf8(response).map_err(|_e| Error::Parse)?;
113
114 match self.kind {
115 SubscriptionKind::ClashCompatible => Proxy::parse_clash_compatible(&response),
116 }
117 }
118}
119
120wrapper_lite::wrapper!(
121 #[wrapper_impl(DebugName)]
122 pub struct Task {
124 pending: Subscription,
126
127 interval: Option<Duration>,
131
132 proxies_tx: mpsc::Sender<Vec<Proxy>>,
134 }
135);
136
137impl Task {
138 pub fn new(
140 pending: Subscription,
141 interval: Option<Duration>,
142 ) -> (Self, mpsc::Receiver<Vec<Proxy>>) {
143 let (proxies_tx, proxies_rx) = mpsc::channel(1);
144
145 (
146 Self {
147 pending,
148 interval,
149 proxies_tx,
150 },
151 proxies_rx,
152 )
153 }
154
155 pub async fn spawn(self) -> JoinHandle<()> {
157 tokio::spawn(async move {
158 loop {
159 let pending = self.pending.clone();
160
161 let proxies = tokio::task::spawn_blocking(move || pending.fetch()).await;
162
163 match proxies {
164 Ok(Ok(proxies)) => {
165 let Ok(()) = self.proxies_tx.send(proxies).await else {
166 tracing::info!("Subscription task channel closed, exiting task.");
167
168 return;
169 };
170 }
171 Ok(Err(e)) => {
172 tracing::error!("Failed to fetch subscription: {:?}", e);
173 }
174 Err(e) => {
175 tracing::error!("Failed to join subscription task: {:?}", e);
176 }
177 }
178
179 tokio::time::sleep(self.interval.unwrap_or(Duration::from_mins(30))).await;
180 }
181 })
182 }
183}