profiles.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. use crate::utils::{config, dirs, help, tmpl};
  2. use anyhow::{bail, Context, Result};
  3. use serde::{Deserialize, Serialize};
  4. use serde_yaml::{Mapping, Value};
  5. use std::{fs, io::Write};
  6. #[derive(Debug, Clone, Deserialize, Serialize)]
  7. pub struct PrfItem {
  8. pub uid: Option<String>,
  9. /// profile item type
  10. /// enum value: remote | local | script | merge
  11. #[serde(rename = "type")]
  12. pub itype: Option<String>,
  13. /// profile name
  14. pub name: Option<String>,
  15. /// profile description
  16. #[serde(skip_serializing_if = "Option::is_none")]
  17. pub desc: Option<String>,
  18. /// profile file
  19. pub file: Option<String>,
  20. /// source url
  21. #[serde(skip_serializing_if = "Option::is_none")]
  22. pub url: Option<String>,
  23. /// selected infomation
  24. #[serde(skip_serializing_if = "Option::is_none")]
  25. pub selected: Option<Vec<PrfSelected>>,
  26. /// subscription user info
  27. #[serde(skip_serializing_if = "Option::is_none")]
  28. pub extra: Option<PrfExtra>,
  29. /// updated time
  30. pub updated: Option<usize>,
  31. /// some options of the item
  32. #[serde(skip_serializing_if = "Option::is_none")]
  33. pub option: Option<PrfOption>,
  34. /// the file data
  35. #[serde(skip)]
  36. pub file_data: Option<String>,
  37. }
  38. #[derive(Default, Debug, Clone, Deserialize, Serialize)]
  39. pub struct PrfSelected {
  40. pub name: Option<String>,
  41. pub now: Option<String>,
  42. }
  43. #[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
  44. pub struct PrfExtra {
  45. pub upload: usize,
  46. pub download: usize,
  47. pub total: usize,
  48. pub expire: usize,
  49. }
  50. #[derive(Default, Debug, Clone, Deserialize, Serialize)]
  51. pub struct PrfOption {
  52. /// for `remote` profile's http request
  53. /// see issue #13
  54. #[serde(skip_serializing_if = "Option::is_none")]
  55. pub user_agent: Option<String>,
  56. /// for `remote` profile
  57. #[serde(skip_serializing_if = "Option::is_none")]
  58. pub with_proxy: Option<bool>,
  59. }
  60. impl PrfOption {
  61. pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
  62. if one.is_some() && other.is_some() {
  63. let mut one = one.unwrap();
  64. let other = other.unwrap();
  65. if let Some(val) = other.user_agent {
  66. one.user_agent = Some(val);
  67. }
  68. if let Some(val) = other.with_proxy {
  69. one.with_proxy = Some(val);
  70. }
  71. return Some(one);
  72. }
  73. if one.is_none() {
  74. return other;
  75. }
  76. return one;
  77. }
  78. }
  79. impl Default for PrfItem {
  80. fn default() -> Self {
  81. PrfItem {
  82. uid: None,
  83. itype: None,
  84. name: None,
  85. desc: None,
  86. file: None,
  87. url: None,
  88. selected: None,
  89. extra: None,
  90. updated: None,
  91. option: None,
  92. file_data: None,
  93. }
  94. }
  95. }
  96. impl PrfItem {
  97. /// From partial item
  98. /// must contain `itype`
  99. pub async fn from(item: PrfItem) -> Result<PrfItem> {
  100. if item.itype.is_none() {
  101. bail!("type should not be null");
  102. }
  103. match item.itype.unwrap().as_str() {
  104. "remote" => {
  105. if item.url.is_none() {
  106. bail!("url should not be null");
  107. }
  108. let url = item.url.as_ref().unwrap().as_str();
  109. let name = item.name;
  110. let desc = item.desc;
  111. PrfItem::from_url(url, name, desc, item.option).await
  112. }
  113. "local" => {
  114. let name = item.name.unwrap_or("Local File".into());
  115. let desc = item.desc.unwrap_or("".into());
  116. PrfItem::from_local(name, desc)
  117. }
  118. "merge" => {
  119. let name = item.name.unwrap_or("Merge".into());
  120. let desc = item.desc.unwrap_or("".into());
  121. PrfItem::from_merge(name, desc)
  122. }
  123. "script" => {
  124. let name = item.name.unwrap_or("Script".into());
  125. let desc = item.desc.unwrap_or("".into());
  126. PrfItem::from_script(name, desc)
  127. }
  128. typ @ _ => bail!("invalid type \"{typ}\""),
  129. }
  130. }
  131. /// ## Local type
  132. /// create a new item from name/desc
  133. pub fn from_local(name: String, desc: String) -> Result<PrfItem> {
  134. let uid = help::get_uid("l");
  135. let file = format!("{uid}.yaml");
  136. Ok(PrfItem {
  137. uid: Some(uid),
  138. itype: Some("local".into()),
  139. name: Some(name),
  140. desc: Some(desc),
  141. file: Some(file),
  142. url: None,
  143. selected: None,
  144. extra: None,
  145. option: None,
  146. updated: Some(help::get_now()),
  147. file_data: Some(tmpl::ITEM_LOCAL.into()),
  148. })
  149. }
  150. /// ## Remote type
  151. /// create a new item from url
  152. pub async fn from_url(
  153. url: &str,
  154. name: Option<String>,
  155. desc: Option<String>,
  156. option: Option<PrfOption>,
  157. ) -> Result<PrfItem> {
  158. let with_proxy = match option.as_ref() {
  159. Some(opt) => opt.with_proxy.unwrap_or(false),
  160. None => false,
  161. };
  162. let user_agent = match option.as_ref() {
  163. Some(opt) => opt.user_agent.clone(),
  164. None => None,
  165. };
  166. let mut builder = reqwest::ClientBuilder::new();
  167. if !with_proxy {
  168. builder = builder.no_proxy();
  169. }
  170. if let Some(user_agent) = user_agent {
  171. builder = builder.user_agent(user_agent);
  172. }
  173. let resp = builder.build()?.get(url).send().await?;
  174. let header = resp.headers();
  175. // parse the Subscription Userinfo
  176. let extra = match header.get("Subscription-Userinfo") {
  177. Some(value) => {
  178. let sub_info = value.to_str().unwrap_or("");
  179. Some(PrfExtra {
  180. upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
  181. download: help::parse_str(sub_info, "download=").unwrap_or(0),
  182. total: help::parse_str(sub_info, "total=").unwrap_or(0),
  183. expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
  184. })
  185. }
  186. None => None,
  187. };
  188. let uid = help::get_uid("r");
  189. let file = format!("{uid}.yaml");
  190. let name = name.unwrap_or(uid.clone());
  191. let data = resp.text_with_charset("utf-8").await?;
  192. Ok(PrfItem {
  193. uid: Some(uid),
  194. itype: Some("remote".into()),
  195. name: Some(name),
  196. desc,
  197. file: Some(file),
  198. url: Some(url.into()),
  199. selected: None,
  200. extra,
  201. option,
  202. updated: Some(help::get_now()),
  203. file_data: Some(data),
  204. })
  205. }
  206. /// ## Merge type (enhance)
  207. /// create the enhanced item by using `merge` rule
  208. pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
  209. let uid = help::get_uid("m");
  210. let file = format!("{uid}.yaml");
  211. Ok(PrfItem {
  212. uid: Some(uid),
  213. itype: Some("merge".into()),
  214. name: Some(name),
  215. desc: Some(desc),
  216. file: Some(file),
  217. url: None,
  218. selected: None,
  219. extra: None,
  220. option: None,
  221. updated: Some(help::get_now()),
  222. file_data: Some(tmpl::ITEM_MERGE.into()),
  223. })
  224. }
  225. /// ## Script type (enhance)
  226. /// create the enhanced item by using javascript(browserjs)
  227. pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
  228. let uid = help::get_uid("s");
  229. let file = format!("{uid}.js"); // js ext
  230. Ok(PrfItem {
  231. uid: Some(uid),
  232. itype: Some("script".into()),
  233. name: Some(name),
  234. desc: Some(desc),
  235. file: Some(file),
  236. url: None,
  237. selected: None,
  238. extra: None,
  239. option: None,
  240. updated: Some(help::get_now()),
  241. file_data: Some(tmpl::ITEM_SCRIPT.into()),
  242. })
  243. }
  244. }
  245. ///
  246. /// ## Profiles Config
  247. ///
  248. /// Define the `profiles.yaml` schema
  249. ///
  250. #[derive(Default, Debug, Clone, Deserialize, Serialize)]
  251. pub struct Profiles {
  252. /// same as PrfConfig.current
  253. current: Option<String>,
  254. /// same as PrfConfig.chain
  255. chain: Option<Vec<String>>,
  256. /// profile list
  257. items: Option<Vec<PrfItem>>,
  258. }
  259. macro_rules! patch {
  260. ($lv: expr, $rv: expr, $key: tt) => {
  261. if ($rv.$key).is_some() {
  262. $lv.$key = $rv.$key;
  263. }
  264. };
  265. }
  266. impl Profiles {
  267. /// read the config from the file
  268. pub fn read_file() -> Self {
  269. let mut profiles = config::read_yaml::<Self>(dirs::profiles_path());
  270. if profiles.items.is_none() {
  271. profiles.items = Some(vec![]);
  272. }
  273. profiles.items.as_mut().map(|items| {
  274. for mut item in items.iter_mut() {
  275. if item.uid.is_none() {
  276. item.uid = Some(help::get_uid("d"));
  277. }
  278. }
  279. });
  280. profiles
  281. }
  282. /// save the config to the file
  283. pub fn save_file(&self) -> Result<()> {
  284. config::save_yaml(
  285. dirs::profiles_path(),
  286. self,
  287. Some("# Profiles Config for Clash Verge\n\n"),
  288. )
  289. }
  290. /// sync the config between file and memory
  291. pub fn sync_file(&mut self) -> Result<()> {
  292. let data = Self::read_file();
  293. if data.current.is_none() && data.items.is_none() {
  294. bail!("failed to read profiles.yaml");
  295. }
  296. self.current = data.current;
  297. self.chain = data.chain;
  298. self.items = data.items;
  299. Ok(())
  300. }
  301. /// get the current uid
  302. pub fn get_current(&self) -> Option<String> {
  303. self.current.clone()
  304. }
  305. /// only change the main to the target id
  306. pub fn put_current(&mut self, uid: String) -> Result<()> {
  307. if self.items.is_none() {
  308. self.items = Some(vec![]);
  309. }
  310. let items = self.items.as_ref().unwrap();
  311. let some_uid = Some(uid.clone());
  312. for each in items.iter() {
  313. if each.uid == some_uid {
  314. self.current = some_uid;
  315. return self.save_file();
  316. }
  317. }
  318. bail!("invalid uid \"{uid}\"");
  319. }
  320. /// just change the `chain`
  321. pub fn put_chain(&mut self, chain: Option<Vec<String>>) {
  322. self.chain = chain;
  323. }
  324. /// find the item by the uid
  325. pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
  326. if self.items.is_some() {
  327. let items = self.items.as_ref().unwrap();
  328. let some_uid = Some(uid.clone());
  329. for each in items.iter() {
  330. if each.uid == some_uid {
  331. return Ok(each);
  332. }
  333. }
  334. }
  335. bail!("failed to get the item by \"{}\"", uid);
  336. }
  337. /// append new item
  338. /// if the file_data is some
  339. /// then should save the data to file
  340. pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
  341. if item.uid.is_none() {
  342. bail!("the uid should not be null");
  343. }
  344. // save the file data
  345. // move the field value after save
  346. if let Some(file_data) = item.file_data.take() {
  347. if item.file.is_none() {
  348. bail!("the file should not be null");
  349. }
  350. let file = item.file.clone().unwrap();
  351. let path = dirs::app_profiles_dir().join(&file);
  352. fs::File::create(path)
  353. .context(format!("failed to create file \"{}\"", file))?
  354. .write(file_data.as_bytes())
  355. .context(format!("failed to write to file \"{}\"", file))?;
  356. }
  357. if self.items.is_none() {
  358. self.items = Some(vec![]);
  359. }
  360. self.items.as_mut().map(|items| items.push(item));
  361. self.save_file()
  362. }
  363. /// update the item's value
  364. pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
  365. let mut items = self.items.take().unwrap_or(vec![]);
  366. for mut each in items.iter_mut() {
  367. if each.uid == Some(uid.clone()) {
  368. patch!(each, item, itype);
  369. patch!(each, item, name);
  370. patch!(each, item, desc);
  371. patch!(each, item, file);
  372. patch!(each, item, url);
  373. patch!(each, item, selected);
  374. patch!(each, item, extra);
  375. patch!(each, item, updated);
  376. patch!(each, item, option);
  377. self.items = Some(items);
  378. return self.save_file();
  379. }
  380. }
  381. self.items = Some(items);
  382. bail!("failed to found the uid \"{uid}\"")
  383. }
  384. /// be used to update the remote item
  385. /// only patch `updated` `extra` `file_data`
  386. pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
  387. if self.items.is_none() {
  388. self.items = Some(vec![]);
  389. }
  390. // find the item
  391. let _ = self.get_item(&uid)?;
  392. self.items.as_mut().map(|items| {
  393. let some_uid = Some(uid.clone());
  394. for mut each in items.iter_mut() {
  395. if each.uid == some_uid {
  396. each.extra = item.extra;
  397. each.updated = item.updated;
  398. // save the file data
  399. // move the field value after save
  400. if let Some(file_data) = item.file_data.take() {
  401. let file = each.file.take();
  402. let file = file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
  403. // the file must exists
  404. each.file = Some(file.clone());
  405. let path = dirs::app_profiles_dir().join(&file);
  406. fs::File::create(path)
  407. .unwrap()
  408. .write(file_data.as_bytes())
  409. .unwrap();
  410. }
  411. break;
  412. }
  413. }
  414. });
  415. self.save_file()
  416. }
  417. /// delete item
  418. /// if delete the current then return true
  419. pub fn delete_item(&mut self, uid: String) -> Result<bool> {
  420. let current = self.current.as_ref().unwrap_or(&uid);
  421. let current = current.clone();
  422. let mut items = self.items.take().unwrap_or(vec![]);
  423. let mut index = None;
  424. // get the index
  425. for i in 0..items.len() {
  426. if items[i].uid == Some(uid.clone()) {
  427. index = Some(i);
  428. break;
  429. }
  430. }
  431. if let Some(index) = index {
  432. items.remove(index).file.map(|file| {
  433. let path = dirs::app_profiles_dir().join(file);
  434. if path.exists() {
  435. let _ = fs::remove_file(path);
  436. }
  437. });
  438. }
  439. // delete the original uid
  440. if current == uid {
  441. self.current = match items.len() > 0 {
  442. true => items[0].uid.clone(),
  443. false => None,
  444. };
  445. }
  446. self.items = Some(items);
  447. self.save_file()?;
  448. Ok(current == uid)
  449. }
  450. /// only generate config mapping
  451. pub fn gen_activate(&self) -> Result<Mapping> {
  452. let config = Mapping::new();
  453. if self.current.is_none() || self.items.is_none() {
  454. return Ok(config);
  455. }
  456. let current = self.current.clone().unwrap();
  457. for item in self.items.as_ref().unwrap().iter() {
  458. if item.uid == Some(current.clone()) {
  459. let file_path = match item.file.clone() {
  460. Some(file) => dirs::app_profiles_dir().join(file),
  461. None => bail!("failed to get the file field"),
  462. };
  463. if !file_path.exists() {
  464. bail!("failed to read the file \"{}\"", file_path.display());
  465. }
  466. let mut new_config = Mapping::new();
  467. let def_config = config::read_yaml::<Mapping>(file_path.clone());
  468. // Only the following fields are allowed:
  469. // proxies/proxy-providers/proxy-groups/rule-providers/rules
  470. let valid_keys = vec![
  471. "proxies",
  472. "proxy-providers",
  473. "proxy-groups",
  474. "rule-providers",
  475. "rules",
  476. ];
  477. for (key, value) in def_config.into_iter() {
  478. key.as_str().map(|key_str| {
  479. // change to lowercase
  480. let mut key_str = String::from(key_str);
  481. key_str.make_ascii_lowercase();
  482. if valid_keys.contains(&&*key_str) {
  483. new_config.insert(Value::String(key_str), value);
  484. }
  485. });
  486. }
  487. return Ok(new_config);
  488. }
  489. }
  490. bail!("failed to found the uid \"{current}\"");
  491. }
  492. /// gen the enhanced profiles
  493. pub fn gen_enhanced(&self, callback: String) -> Result<PrfEnhanced> {
  494. let current = self.gen_activate()?;
  495. let chain = match self.chain.as_ref() {
  496. Some(chain) => chain
  497. .iter()
  498. .map(|uid| self.get_item(uid))
  499. .filter(|item| item.is_ok())
  500. .map(|item| item.unwrap())
  501. .map(|item| PrfData::from_item(item))
  502. .filter(|o| o.is_some())
  503. .map(|o| o.unwrap())
  504. .collect::<Vec<PrfData>>(),
  505. None => vec![],
  506. };
  507. Ok(PrfEnhanced {
  508. current,
  509. chain,
  510. callback,
  511. })
  512. }
  513. }
  514. #[derive(Default, Debug, Clone, Serialize, Deserialize)]
  515. pub struct PrfEnhanced {
  516. current: Mapping,
  517. chain: Vec<PrfData>,
  518. callback: String,
  519. }
  520. #[derive(Default, Debug, Clone, Serialize, Deserialize)]
  521. pub struct PrfEnhancedResult {
  522. pub data: Option<Mapping>,
  523. pub status: String,
  524. pub error: Option<String>,
  525. }
  526. #[derive(Default, Debug, Clone, Serialize, Deserialize)]
  527. pub struct PrfData {
  528. item: PrfItem,
  529. #[serde(skip_serializing_if = "Option::is_none")]
  530. merge: Option<Mapping>,
  531. #[serde(skip_serializing_if = "Option::is_none")]
  532. script: Option<String>,
  533. }
  534. impl PrfData {
  535. pub fn from_item(item: &PrfItem) -> Option<PrfData> {
  536. match item.itype.as_ref() {
  537. Some(itype) => {
  538. let file = item.file.clone()?;
  539. let path = dirs::app_profiles_dir().join(file);
  540. if !path.exists() {
  541. return None;
  542. }
  543. match itype.as_str() {
  544. "script" => Some(PrfData {
  545. item: item.clone(),
  546. script: Some(fs::read_to_string(path).unwrap_or("".into())),
  547. merge: None,
  548. }),
  549. "merge" => Some(PrfData {
  550. item: item.clone(),
  551. merge: Some(config::read_yaml::<Mapping>(path)),
  552. script: None,
  553. }),
  554. _ => None,
  555. }
  556. }
  557. None => None,
  558. }
  559. }
  560. }