123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 |
- use crate::utils::{config, dirs, help, tmpl};
- use anyhow::{bail, Context, Result};
- use serde::{Deserialize, Serialize};
- use serde_yaml::{Mapping, Value};
- use std::{fs, io::Write};
- #[derive(Debug, Clone, Deserialize, Serialize)]
- pub struct PrfItem {
- pub uid: Option<String>,
- /// profile item type
- /// enum value: remote | local | script | merge
- #[serde(rename = "type")]
- pub itype: Option<String>,
- /// profile name
- pub name: Option<String>,
- /// profile description
- #[serde(skip_serializing_if = "Option::is_none")]
- pub desc: Option<String>,
- /// profile file
- pub file: Option<String>,
- /// source url
- #[serde(skip_serializing_if = "Option::is_none")]
- pub url: Option<String>,
- /// selected infomation
- #[serde(skip_serializing_if = "Option::is_none")]
- pub selected: Option<Vec<PrfSelected>>,
- /// subscription user info
- #[serde(skip_serializing_if = "Option::is_none")]
- pub extra: Option<PrfExtra>,
- /// updated time
- pub updated: Option<usize>,
- /// some options of the item
- #[serde(skip_serializing_if = "Option::is_none")]
- pub option: Option<PrfOption>,
- /// the file data
- #[serde(skip)]
- pub file_data: Option<String>,
- }
- #[derive(Default, Debug, Clone, Deserialize, Serialize)]
- pub struct PrfSelected {
- pub name: Option<String>,
- pub now: Option<String>,
- }
- #[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
- pub struct PrfExtra {
- pub upload: usize,
- pub download: usize,
- pub total: usize,
- pub expire: usize,
- }
- #[derive(Default, Debug, Clone, Deserialize, Serialize)]
- pub struct PrfOption {
- /// for `remote` profile's http request
- /// see issue #13
- #[serde(skip_serializing_if = "Option::is_none")]
- pub user_agent: Option<String>,
- /// for `remote` profile
- #[serde(skip_serializing_if = "Option::is_none")]
- pub with_proxy: Option<bool>,
- }
- impl PrfOption {
- pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
- if one.is_some() && other.is_some() {
- let mut one = one.unwrap();
- let other = other.unwrap();
- if let Some(val) = other.user_agent {
- one.user_agent = Some(val);
- }
- if let Some(val) = other.with_proxy {
- one.with_proxy = Some(val);
- }
- return Some(one);
- }
- if one.is_none() {
- return other;
- }
- return one;
- }
- }
- impl Default for PrfItem {
- fn default() -> Self {
- PrfItem {
- uid: None,
- itype: None,
- name: None,
- desc: None,
- file: None,
- url: None,
- selected: None,
- extra: None,
- updated: None,
- option: None,
- file_data: None,
- }
- }
- }
- impl PrfItem {
- /// From partial item
- /// must contain `itype`
- pub async fn from(item: PrfItem) -> Result<PrfItem> {
- if item.itype.is_none() {
- bail!("type should not be null");
- }
- match item.itype.unwrap().as_str() {
- "remote" => {
- if item.url.is_none() {
- bail!("url should not be null");
- }
- let url = item.url.as_ref().unwrap().as_str();
- let name = item.name;
- let desc = item.desc;
- PrfItem::from_url(url, name, desc, item.option).await
- }
- "local" => {
- let name = item.name.unwrap_or("Local File".into());
- let desc = item.desc.unwrap_or("".into());
- PrfItem::from_local(name, desc)
- }
- "merge" => {
- let name = item.name.unwrap_or("Merge".into());
- let desc = item.desc.unwrap_or("".into());
- PrfItem::from_merge(name, desc)
- }
- "script" => {
- let name = item.name.unwrap_or("Script".into());
- let desc = item.desc.unwrap_or("".into());
- PrfItem::from_script(name, desc)
- }
- typ @ _ => bail!("invalid type \"{typ}\""),
- }
- }
- /// ## Local type
- /// create a new item from name/desc
- pub fn from_local(name: String, desc: String) -> Result<PrfItem> {
- let uid = help::get_uid("l");
- let file = format!("{uid}.yaml");
- Ok(PrfItem {
- uid: Some(uid),
- itype: Some("local".into()),
- name: Some(name),
- desc: Some(desc),
- file: Some(file),
- url: None,
- selected: None,
- extra: None,
- option: None,
- updated: Some(help::get_now()),
- file_data: Some(tmpl::ITEM_LOCAL.into()),
- })
- }
- /// ## Remote type
- /// create a new item from url
- pub async fn from_url(
- url: &str,
- name: Option<String>,
- desc: Option<String>,
- option: Option<PrfOption>,
- ) -> Result<PrfItem> {
- let with_proxy = match option.as_ref() {
- Some(opt) => opt.with_proxy.unwrap_or(false),
- None => false,
- };
- let user_agent = match option.as_ref() {
- Some(opt) => opt.user_agent.clone(),
- None => None,
- };
- let mut builder = reqwest::ClientBuilder::new();
- if !with_proxy {
- builder = builder.no_proxy();
- }
- if let Some(user_agent) = user_agent {
- builder = builder.user_agent(user_agent);
- }
- let resp = builder.build()?.get(url).send().await?;
- let header = resp.headers();
- // parse the Subscription Userinfo
- let extra = match header.get("Subscription-Userinfo") {
- Some(value) => {
- let sub_info = value.to_str().unwrap_or("");
- Some(PrfExtra {
- upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
- download: help::parse_str(sub_info, "download=").unwrap_or(0),
- total: help::parse_str(sub_info, "total=").unwrap_or(0),
- expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
- })
- }
- None => None,
- };
- let uid = help::get_uid("r");
- let file = format!("{uid}.yaml");
- let name = name.unwrap_or(uid.clone());
- let data = resp.text_with_charset("utf-8").await?;
- Ok(PrfItem {
- uid: Some(uid),
- itype: Some("remote".into()),
- name: Some(name),
- desc,
- file: Some(file),
- url: Some(url.into()),
- selected: None,
- extra,
- option,
- updated: Some(help::get_now()),
- file_data: Some(data),
- })
- }
- /// ## Merge type (enhance)
- /// create the enhanced item by using `merge` rule
- pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
- let uid = help::get_uid("m");
- let file = format!("{uid}.yaml");
- Ok(PrfItem {
- uid: Some(uid),
- itype: Some("merge".into()),
- name: Some(name),
- desc: Some(desc),
- file: Some(file),
- url: None,
- selected: None,
- extra: None,
- option: None,
- updated: Some(help::get_now()),
- file_data: Some(tmpl::ITEM_MERGE.into()),
- })
- }
- /// ## Script type (enhance)
- /// create the enhanced item by using javascript(browserjs)
- pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
- let uid = help::get_uid("s");
- let file = format!("{uid}.js"); // js ext
- Ok(PrfItem {
- uid: Some(uid),
- itype: Some("script".into()),
- name: Some(name),
- desc: Some(desc),
- file: Some(file),
- url: None,
- selected: None,
- extra: None,
- option: None,
- updated: Some(help::get_now()),
- file_data: Some(tmpl::ITEM_SCRIPT.into()),
- })
- }
- }
- ///
- /// ## Profiles Config
- ///
- /// Define the `profiles.yaml` schema
- ///
- #[derive(Default, Debug, Clone, Deserialize, Serialize)]
- pub struct Profiles {
- /// same as PrfConfig.current
- current: Option<String>,
- /// same as PrfConfig.chain
- chain: Option<Vec<String>>,
- /// profile list
- items: Option<Vec<PrfItem>>,
- }
- macro_rules! patch {
- ($lv: expr, $rv: expr, $key: tt) => {
- if ($rv.$key).is_some() {
- $lv.$key = $rv.$key;
- }
- };
- }
- impl Profiles {
- /// read the config from the file
- pub fn read_file() -> Self {
- let mut profiles = config::read_yaml::<Self>(dirs::profiles_path());
- if profiles.items.is_none() {
- profiles.items = Some(vec![]);
- }
- profiles.items.as_mut().map(|items| {
- for mut item in items.iter_mut() {
- if item.uid.is_none() {
- item.uid = Some(help::get_uid("d"));
- }
- }
- });
- profiles
- }
- /// save the config to the file
- pub fn save_file(&self) -> Result<()> {
- config::save_yaml(
- dirs::profiles_path(),
- self,
- Some("# Profiles Config for Clash Verge\n\n"),
- )
- }
- /// sync the config between file and memory
- pub fn sync_file(&mut self) -> Result<()> {
- let data = Self::read_file();
- if data.current.is_none() && data.items.is_none() {
- bail!("failed to read profiles.yaml");
- }
- self.current = data.current;
- self.chain = data.chain;
- self.items = data.items;
- Ok(())
- }
- /// get the current uid
- pub fn get_current(&self) -> Option<String> {
- self.current.clone()
- }
- /// only change the main to the target id
- pub fn put_current(&mut self, uid: String) -> Result<()> {
- if self.items.is_none() {
- self.items = Some(vec![]);
- }
- let items = self.items.as_ref().unwrap();
- let some_uid = Some(uid.clone());
- for each in items.iter() {
- if each.uid == some_uid {
- self.current = some_uid;
- return self.save_file();
- }
- }
- bail!("invalid uid \"{uid}\"");
- }
- /// just change the `chain`
- pub fn put_chain(&mut self, chain: Option<Vec<String>>) {
- self.chain = chain;
- }
- /// find the item by the uid
- pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
- if self.items.is_some() {
- let items = self.items.as_ref().unwrap();
- let some_uid = Some(uid.clone());
- for each in items.iter() {
- if each.uid == some_uid {
- return Ok(each);
- }
- }
- }
- bail!("failed to get the item by \"{}\"", uid);
- }
- /// append new item
- /// if the file_data is some
- /// then should save the data to file
- pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
- if item.uid.is_none() {
- bail!("the uid should not be null");
- }
- // save the file data
- // move the field value after save
- if let Some(file_data) = item.file_data.take() {
- if item.file.is_none() {
- bail!("the file should not be null");
- }
- let file = item.file.clone().unwrap();
- let path = dirs::app_profiles_dir().join(&file);
- fs::File::create(path)
- .context(format!("failed to create file \"{}\"", file))?
- .write(file_data.as_bytes())
- .context(format!("failed to write to file \"{}\"", file))?;
- }
- if self.items.is_none() {
- self.items = Some(vec![]);
- }
- self.items.as_mut().map(|items| items.push(item));
- self.save_file()
- }
- /// update the item's value
- pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
- let mut items = self.items.take().unwrap_or(vec![]);
- for mut each in items.iter_mut() {
- if each.uid == Some(uid.clone()) {
- patch!(each, item, itype);
- patch!(each, item, name);
- patch!(each, item, desc);
- patch!(each, item, file);
- patch!(each, item, url);
- patch!(each, item, selected);
- patch!(each, item, extra);
- patch!(each, item, updated);
- patch!(each, item, option);
- self.items = Some(items);
- return self.save_file();
- }
- }
- self.items = Some(items);
- bail!("failed to found the uid \"{uid}\"")
- }
- /// be used to update the remote item
- /// only patch `updated` `extra` `file_data`
- pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
- if self.items.is_none() {
- self.items = Some(vec![]);
- }
- // find the item
- let _ = self.get_item(&uid)?;
- self.items.as_mut().map(|items| {
- let some_uid = Some(uid.clone());
- for mut each in items.iter_mut() {
- if each.uid == some_uid {
- each.extra = item.extra;
- each.updated = item.updated;
- // save the file data
- // move the field value after save
- if let Some(file_data) = item.file_data.take() {
- let file = each.file.take();
- let file = file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
- // the file must exists
- each.file = Some(file.clone());
- let path = dirs::app_profiles_dir().join(&file);
- fs::File::create(path)
- .unwrap()
- .write(file_data.as_bytes())
- .unwrap();
- }
- break;
- }
- }
- });
- self.save_file()
- }
- /// delete item
- /// if delete the current then return true
- pub fn delete_item(&mut self, uid: String) -> Result<bool> {
- let current = self.current.as_ref().unwrap_or(&uid);
- let current = current.clone();
- let mut items = self.items.take().unwrap_or(vec![]);
- let mut index = None;
- // get the index
- for i in 0..items.len() {
- if items[i].uid == Some(uid.clone()) {
- index = Some(i);
- break;
- }
- }
- if let Some(index) = index {
- items.remove(index).file.map(|file| {
- let path = dirs::app_profiles_dir().join(file);
- if path.exists() {
- let _ = fs::remove_file(path);
- }
- });
- }
- // delete the original uid
- if current == uid {
- self.current = match items.len() > 0 {
- true => items[0].uid.clone(),
- false => None,
- };
- }
- self.items = Some(items);
- self.save_file()?;
- Ok(current == uid)
- }
- /// only generate config mapping
- pub fn gen_activate(&self) -> Result<Mapping> {
- let config = Mapping::new();
- if self.current.is_none() || self.items.is_none() {
- return Ok(config);
- }
- let current = self.current.clone().unwrap();
- for item in self.items.as_ref().unwrap().iter() {
- if item.uid == Some(current.clone()) {
- let file_path = match item.file.clone() {
- Some(file) => dirs::app_profiles_dir().join(file),
- None => bail!("failed to get the file field"),
- };
- if !file_path.exists() {
- bail!("failed to read the file \"{}\"", file_path.display());
- }
- let mut new_config = Mapping::new();
- let def_config = config::read_yaml::<Mapping>(file_path.clone());
- // Only the following fields are allowed:
- // proxies/proxy-providers/proxy-groups/rule-providers/rules
- let valid_keys = vec![
- "proxies",
- "proxy-providers",
- "proxy-groups",
- "rule-providers",
- "rules",
- ];
- for (key, value) in def_config.into_iter() {
- key.as_str().map(|key_str| {
- // change to lowercase
- let mut key_str = String::from(key_str);
- key_str.make_ascii_lowercase();
- if valid_keys.contains(&&*key_str) {
- new_config.insert(Value::String(key_str), value);
- }
- });
- }
- return Ok(new_config);
- }
- }
- bail!("failed to found the uid \"{current}\"");
- }
- /// gen the enhanced profiles
- pub fn gen_enhanced(&self, callback: String) -> Result<PrfEnhanced> {
- let current = self.gen_activate()?;
- let chain = match self.chain.as_ref() {
- Some(chain) => chain
- .iter()
- .map(|uid| self.get_item(uid))
- .filter(|item| item.is_ok())
- .map(|item| item.unwrap())
- .map(|item| PrfData::from_item(item))
- .filter(|o| o.is_some())
- .map(|o| o.unwrap())
- .collect::<Vec<PrfData>>(),
- None => vec![],
- };
- Ok(PrfEnhanced {
- current,
- chain,
- callback,
- })
- }
- }
- #[derive(Default, Debug, Clone, Serialize, Deserialize)]
- pub struct PrfEnhanced {
- current: Mapping,
- chain: Vec<PrfData>,
- callback: String,
- }
- #[derive(Default, Debug, Clone, Serialize, Deserialize)]
- pub struct PrfEnhancedResult {
- pub data: Option<Mapping>,
- pub status: String,
- pub error: Option<String>,
- }
- #[derive(Default, Debug, Clone, Serialize, Deserialize)]
- pub struct PrfData {
- item: PrfItem,
- #[serde(skip_serializing_if = "Option::is_none")]
- merge: Option<Mapping>,
- #[serde(skip_serializing_if = "Option::is_none")]
- script: Option<String>,
- }
- impl PrfData {
- pub fn from_item(item: &PrfItem) -> Option<PrfData> {
- match item.itype.as_ref() {
- Some(itype) => {
- let file = item.file.clone()?;
- let path = dirs::app_profiles_dir().join(file);
- if !path.exists() {
- return None;
- }
- match itype.as_str() {
- "script" => Some(PrfData {
- item: item.clone(),
- script: Some(fs::read_to_string(path).unwrap_or("".into())),
- merge: None,
- }),
- "merge" => Some(PrfData {
- item: item.clone(),
- merge: Some(config::read_yaml::<Mapping>(path)),
- script: None,
- }),
- _ => None,
- }
- }
- None => None,
- }
- }
- }
|