1use log::{debug, error};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::constants::regex_black_list::REGEX_BLACK_LIST;
6use crate::interfaces::subconverter::{subconverter, SubconverterConfigBuilder, UploadStatus};
7use crate::models::ruleset::RulesetConfigs;
8use crate::models::{ProxyGroupConfigs, RegexMatchConfigs, SubconverterTarget};
9use crate::settings::external::ExternalSettings;
10use crate::settings::settings::init_settings;
11use crate::settings::{refresh_configuration, FromIni, FromIniWithDelimiter};
12use crate::utils::reg_valid;
13use crate::{RuleBases, Settings, TemplateArgs};
14
15#[cfg(target_arch = "wasm32")]
16use {js_sys::Promise, wasm_bindgen::prelude::*, wasm_bindgen_futures::future_to_promise};
17
18fn default_ver() -> u32 {
19 3
20}
21
22mod bool_deserializer {
24 use serde::{self, Deserialize, Deserializer};
25
26 pub fn deserialize_option_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
27 where
28 D: Deserializer<'de>,
29 {
30 #[derive(Deserialize)]
31 #[serde(untagged)]
32 enum BoolOrString {
33 Bool(bool),
34 String(String),
35 Int(i64),
36 }
37
38 match Option::<BoolOrString>::deserialize(deserializer)? {
39 Some(BoolOrString::Bool(b)) => Ok(Some(b)),
40 Some(BoolOrString::Int(i)) => match i {
41 0 => Ok(Some(false)),
42 1 => Ok(Some(true)),
43 _ => Ok(None), },
46 Some(BoolOrString::String(s)) => match s.to_lowercase().as_str() {
47 "true" | "yes" | "1" | "on" => Ok(Some(true)),
48 "false" | "no" | "0" | "off" => Ok(Some(false)),
49 _ => Ok(None), },
53 None => Ok(None),
54 }
55 }
56}
57#[derive(Deserialize, Serialize, Debug, Default, Clone)]
61pub struct SubconverterQuery {
62 pub target: Option<String>,
64 #[serde(default = "default_ver")]
66 pub ver: u32,
67 #[serde(
69 default,
70 deserialize_with = "bool_deserializer::deserialize_option_bool"
71 )]
72 pub new_name: Option<bool>,
73 pub url: Option<String>,
75 pub group: Option<String>,
77 pub upload_path: Option<String>,
79 pub include: Option<String>,
81 pub exclude: Option<String>,
83 pub groups: Option<String>,
85 pub ruleset: Option<String>,
87 pub config: Option<String>,
89
90 pub dev_id: Option<String>,
92 #[serde(
94 default,
95 deserialize_with = "bool_deserializer::deserialize_option_bool"
96 )]
97 pub insert: Option<bool>,
98 #[serde(
100 default,
101 deserialize_with = "bool_deserializer::deserialize_option_bool"
102 )]
103 pub prepend: Option<bool>,
104 pub filename: Option<String>,
106 #[serde(
108 default,
109 deserialize_with = "bool_deserializer::deserialize_option_bool"
110 )]
111 pub append_type: Option<bool>,
112 #[serde(
114 default,
115 deserialize_with = "bool_deserializer::deserialize_option_bool"
116 )]
117 pub emoji: Option<bool>,
118 #[serde(
120 default,
121 deserialize_with = "bool_deserializer::deserialize_option_bool"
122 )]
123 pub add_emoji: Option<bool>,
124 #[serde(
126 default,
127 deserialize_with = "bool_deserializer::deserialize_option_bool"
128 )]
129 pub remove_emoji: Option<bool>,
130 #[serde(
132 default,
133 deserialize_with = "bool_deserializer::deserialize_option_bool"
134 )]
135 pub list: Option<bool>,
136 #[serde(
138 default,
139 deserialize_with = "bool_deserializer::deserialize_option_bool"
140 )]
141 pub sort: Option<bool>,
142
143 pub sort_script: Option<String>,
145
146 #[serde(
148 default,
149 deserialize_with = "bool_deserializer::deserialize_option_bool"
150 )]
151 pub fdn: Option<bool>,
152
153 pub rename: Option<String>,
155 #[serde(
157 default,
158 deserialize_with = "bool_deserializer::deserialize_option_bool"
159 )]
160 pub tfo: Option<bool>,
161 #[serde(
163 default,
164 deserialize_with = "bool_deserializer::deserialize_option_bool"
165 )]
166 pub udp: Option<bool>,
167 #[serde(
169 default,
170 deserialize_with = "bool_deserializer::deserialize_option_bool"
171 )]
172 pub scv: Option<bool>,
173 #[serde(
175 default,
176 deserialize_with = "bool_deserializer::deserialize_option_bool"
177 )]
178 pub tls13: Option<bool>,
179 #[serde(
181 default,
182 deserialize_with = "bool_deserializer::deserialize_option_bool"
183 )]
184 pub rename_node: Option<bool>,
185 pub interval: Option<u32>,
187 #[serde(
189 default,
190 deserialize_with = "bool_deserializer::deserialize_option_bool"
191 )]
192 pub strict: Option<bool>,
193 #[serde(
195 default,
196 deserialize_with = "bool_deserializer::deserialize_option_bool"
197 )]
198 pub upload: Option<bool>,
199 pub token: Option<String>,
201 pub filter: Option<String>,
203
204 #[serde(
206 default,
207 deserialize_with = "bool_deserializer::deserialize_option_bool"
208 )]
209 pub script: Option<bool>,
210 #[serde(
211 default,
212 deserialize_with = "bool_deserializer::deserialize_option_bool"
213 )]
214 pub classic: Option<bool>,
215
216 #[serde(
217 default,
218 deserialize_with = "bool_deserializer::deserialize_option_bool"
219 )]
220 pub expand: Option<bool>,
221
222 #[serde(default)]
224 pub singbox: HashMap<String, String>,
225
226 pub request_headers: Option<HashMap<String, String>>,
228}
229
230pub fn parse_query_string(query: &str) -> HashMap<String, String> {
232 let mut params = HashMap::new();
233 for pair in query.split('&') {
234 let mut parts = pair.splitn(2, '=');
235 if let Some(key) = parts.next() {
236 let value = parts.next().unwrap_or("");
237 params.insert(key.to_string(), value.to_string());
238 }
239 }
240 params
241}
242
243#[derive(Debug, Serialize)]
245pub struct SubResponse {
246 pub content: String,
247 pub content_type: String,
248 pub headers: HashMap<String, String>,
249 pub status_code: u16,
250 #[serde(skip_serializing_if = "is_not_attempted")] pub upload_status: UploadStatus,
252}
253
254fn is_not_attempted(status: &UploadStatus) -> bool {
256 matches!(status, UploadStatus::NotAttempted)
257}
258
259impl SubResponse {
260 pub fn ok(content: String, content_type: String) -> Self {
261 Self {
262 content,
263 content_type,
264 headers: HashMap::new(),
265 status_code: 200,
266 upload_status: UploadStatus::NotAttempted, }
268 }
269
270 pub fn error(content: String, status_code: u16) -> Self {
271 Self {
272 content,
273 content_type: "text/plain".to_string(),
274 headers: HashMap::new(),
275 status_code,
276 upload_status: UploadStatus::NotAttempted, }
278 }
279
280 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
281 self.headers = headers;
282 self
283 }
284
285 pub fn with_upload_status(mut self, status: UploadStatus) -> Self {
286 self.upload_status = status;
287 self
288 }
289}
290
291pub async fn sub_process(
293 req_url: Option<String>,
294 query: SubconverterQuery,
295) -> Result<SubResponse, Box<dyn std::error::Error>> {
296 let mut global = Settings::current();
297
298 if global.pref_path.is_empty() {
300 debug!("Global config not initialized, reloading");
301 init_settings("").await?;
302 global = Settings::current();
303 } else if global.reload_conf_on_request && !global.api_mode && !global.generator_mode {
304 refresh_configuration().await;
305 global = Settings::current();
306 }
307
308 let mut builder = SubconverterConfigBuilder::new();
310
311 let target;
312 if let Some(_target) = &query.target {
313 match SubconverterTarget::from_str(&_target) {
314 Some(_target) => {
315 target = _target.clone();
316 if _target == SubconverterTarget::Auto {
317 return Ok(SubResponse::error(
329 "Auto user agent is not supported for now.".to_string(),
330 400,
331 ));
332 }
333 builder.target(_target);
334 }
335 None => {
336 return Ok(SubResponse::error(
337 "Invalid target parameter".to_string(),
338 400,
339 ));
340 }
341 }
342 } else {
343 return Ok(SubResponse::error(
344 "Missing target parameter".to_string(),
345 400,
346 ));
347 }
348
349 builder.update_interval(match query.interval {
350 Some(interval) => interval,
351 None => global.update_interval,
352 });
353 #[cfg(not(feature = "js-runtime"))]
355 let authorized = false;
356
357 #[cfg(feature = "js-runtime")]
358 let authorized =
359 !global.api_mode || query.token.as_deref().unwrap_or_default() == global.api_access_token;
360 builder.authorized(authorized);
361 builder.update_strict(query.strict.unwrap_or(global.update_strict));
362
363 if query
364 .include
365 .clone()
366 .is_some_and(|include| REGEX_BLACK_LIST.contains(&include))
367 || query
368 .exclude
369 .clone()
370 .is_some_and(|exclude| REGEX_BLACK_LIST.contains(&exclude))
371 {
372 return Ok(SubResponse::error(
373 "Invalid regex in request!".to_string(),
374 400,
375 ));
376 }
377
378 let enable_insert = match query.insert {
379 Some(insert) => insert,
380 None => global.enable_insert,
381 };
382
383 if enable_insert {
384 builder.insert_urls(global.insert_urls.clone());
385 builder.prepend_insert(query.prepend.unwrap_or(global.prepend_insert));
387 }
388
389 let urls = match query.url.as_deref() {
390 Some(query_url) => query_url.split('|').map(|s| s.to_owned()).collect(),
391 None => {
392 if authorized {
393 global.default_urls.clone()
394 } else {
395 vec![]
396 }
397 }
398 };
399 builder.urls(urls);
400
401 let mut template_args = TemplateArgs::default();
405 template_args.global_vars = global.template_vars.clone();
406
407 template_args.request_params = query.clone();
408
409 builder.append_proxy_type(query.append_type.unwrap_or(global.append_type));
410
411 let mut arg_expand_rulesets = query.expand;
412 if target.is_clash() && query.script.is_none() {
413 arg_expand_rulesets = Some(true);
414 }
415
416 builder.tfo(query.tfo.or(global.tfo_flag));
418 builder.udp(query.udp.or(global.udp_flag));
419 builder.skip_cert_verify(query.scv.or(global.skip_cert_verify));
420 builder.tls13(query.tls13.or(global.tls13_flag));
421 builder.sort(query.sort.unwrap_or(global.enable_sort));
422 builder.sort_script(query.sort_script.unwrap_or(global.sort_script.clone()));
423
424 builder.filter_deprecated(query.fdn.unwrap_or(global.filter_deprecated));
425 builder.clash_new_field_name(query.new_name.unwrap_or(global.clash_use_new_field));
426 builder.clash_script(query.script.unwrap_or_default());
427 builder.clash_classical_ruleset(query.classic.unwrap_or_default());
428 let nodelist = query.list.unwrap_or_default();
429 builder.nodelist(nodelist);
430
431 if arg_expand_rulesets != Some(true) {
432 builder.clash_new_field_name(true);
433 } else {
434 builder.managed_config_prefix(global.managed_config_prefix.clone());
435 builder.clash_script(false);
436 }
437
438 let mut ruleset_configs = global.custom_rulesets.clone();
439 let mut custom_group_configs = global.custom_proxy_groups.clone();
440
441 builder.include_remarks(global.include_remarks.clone());
443 builder.exclude_remarks(global.exclude_remarks.clone());
444 builder.rename_array(global.renames.clone());
445 builder.emoji_array(global.emojis.clone());
446 builder.add_emoji(global.add_emoji);
447 builder.remove_emoji(global.remove_emoji);
448 builder.enable_rule_generator(global.enable_rule_gen);
449 let mut rule_bases = RuleBases {
450 clash_rule_base: global.clash_base.clone(),
451 surge_rule_base: global.surge_base.clone(),
452 surfboard_rule_base: global.surfboard_base.clone(),
453 mellow_rule_base: global.mellow_base.clone(),
454 quan_rule_base: global.quan_base.clone(),
455 quanx_rule_base: global.quanx_base.clone(),
456 loon_rule_base: global.loon_base.clone(),
457 sssub_rule_base: global.ssub_base.clone(),
458 singbox_rule_base: global.singbox_base.clone(),
459 };
460 builder.rule_bases(rule_bases.clone());
461 builder.template_args(template_args.clone());
462
463 let ext_config = match query.config.as_deref() {
464 Some(config) => config.to_owned(),
465 None => global.default_ext_config.clone(),
466 };
467 if !ext_config.is_empty() {
468 debug!("Loading external config from {}", ext_config);
469
470 let extconf_result = ExternalSettings::load_from_file(&ext_config).await;
473
474 match extconf_result {
475 Ok(extconf) => {
476 debug!("Successfully loaded external config from {}", ext_config);
477 if !nodelist {
478 rule_bases
479 .check_external_bases(&extconf, &global.base_path)
480 .await;
481 builder.rule_bases(rule_bases);
482
483 if let Some(tpl_args) = extconf.tpl_args {
484 template_args.local_vars = tpl_args;
485 }
486
487 builder.template_args(template_args);
488
489 if !target.is_simple() {
490 if !extconf.custom_rulesets.is_empty() {
491 ruleset_configs = extconf.custom_rulesets;
492 }
493 if !extconf.custom_proxy_groups.is_empty() {
494 custom_group_configs = extconf.custom_proxy_groups;
495 }
496 if let Some(enable_rule_gen) = extconf.enable_rule_generator {
497 builder.enable_rule_generator(enable_rule_gen);
498 }
499 if let Some(overwrite_original_rules) = extconf.overwrite_original_rules {
500 builder.overwrite_original_rules(overwrite_original_rules);
501 }
502 }
503 }
504 if !extconf.rename_nodes.is_empty() {
505 builder.rename_array(extconf.rename_nodes);
506 }
507 if !extconf.emojis.is_empty() {
508 builder.emoji_array(extconf.emojis);
509 }
510 if !extconf.include_remarks.is_empty() {
511 builder.include_remarks(extconf.include_remarks);
512 }
513 if !extconf.exclude_remarks.is_empty() {
514 builder.exclude_remarks(extconf.exclude_remarks);
515 }
516 if extconf.add_emoji.is_some() {
517 builder.add_emoji(extconf.add_emoji.unwrap());
518 }
519 if extconf.remove_old_emoji.is_some() {
520 builder.remove_emoji(extconf.remove_old_emoji.unwrap());
521 }
522 }
523 Err(e) => {
524 error!("Failed to load external config from {}: {}", ext_config, e);
525 }
526 }
527 }
528
529 if let Some(include) = query.include.as_deref() {
531 if reg_valid(&include) {
532 builder.include_remarks(vec![include.to_owned()]);
533 }
534 }
535 if let Some(exclude) = query.exclude.as_deref() {
536 if reg_valid(&exclude) {
537 builder.exclude_remarks(vec![exclude.to_owned()]);
538 }
539 }
540 if let Some(emoji) = query.emoji {
541 builder.add_emoji(emoji);
542 builder.remove_emoji(true);
543 }
544
545 if let Some(add_emoji) = query.add_emoji {
546 builder.add_emoji(add_emoji);
547 }
548 if let Some(remove_emoji) = query.remove_emoji {
549 builder.remove_emoji(remove_emoji);
550 }
551 if let Some(rename) = query.rename.as_deref() {
552 if !rename.is_empty() {
553 let v_array: Vec<String> = rename.split('`').map(|s| s.to_string()).collect();
554 builder.rename_array(RegexMatchConfigs::from_ini_with_delimiter(&v_array, "@"));
555 }
556 }
557
558 if !target.is_simple() {
559 if !query
561 .groups
562 .as_deref()
563 .is_none_or(|groups| groups.is_empty())
564 && !nodelist
565 {
566 if let Some(groups) = query.groups.as_deref() {
567 let v_array: Vec<String> = groups.split('@').map(|s| s.to_string()).collect();
568 custom_group_configs = ProxyGroupConfigs::from_ini(&v_array);
569 }
570 }
571 if !query
573 .ruleset
574 .as_deref()
575 .is_none_or(|ruleset| ruleset.is_empty())
576 && !nodelist
577 {
578 if let Some(ruleset) = query.ruleset.as_deref() {
579 let v_array: Vec<String> = ruleset.split('@').map(|s| s.to_string()).collect();
580 ruleset_configs = RulesetConfigs::from_ini(&v_array);
581 }
582 }
583 }
584 builder.proxy_groups(custom_group_configs);
585 builder.ruleset_configs(ruleset_configs);
586
587 builder.group_name(query.group.clone());
593 builder.filename(query.filename.clone());
594 builder.upload(query.upload.unwrap_or_default());
595
596 let filter = query.filter.unwrap_or(global.filter_script.clone());
598 if !filter.is_empty() {
599 builder.filter_script(Some(filter));
600 }
601
602 if let Some(request_headers) = &query.request_headers {
613 builder.request_headers(request_headers.clone());
614 }
615
616 let config = match builder.build() {
618 Ok(cfg) => cfg,
619 Err(e) => {
620 error!("Failed to build subconverter config: {}", e);
621 return Ok(SubResponse::error(
622 format!("Configuration error: {}", e),
623 400,
624 ));
625 }
626 };
627
628 debug!("Running subconverter with config: {:?}", config);
631 let subconverter_result = subconverter(config).await;
632
633 match subconverter_result {
634 Ok(result) => {
635 let content_type = match target {
637 SubconverterTarget::Clash
638 | SubconverterTarget::ClashR
639 | SubconverterTarget::SingBox => "application/yaml",
640 SubconverterTarget::SSSub | SubconverterTarget::SSD => "application/json",
641 _ => "text/plain",
642 };
643
644 debug!("Subconverter completed successfully");
645 Ok(SubResponse::ok(result.content, content_type.to_string())
646 .with_headers(result.headers)
647 .with_upload_status(result.upload_status))
648 }
649 Err(e) => {
650 error!("Subconverter error: {}", e);
651 Ok(SubResponse::error(format!("Conversion error: {}", e), 500))
652 }
653 }
654}
655
656#[cfg(target_arch = "wasm32")]
657#[wasm_bindgen]
658pub fn sub_process_wasm(query_json: &str) -> Promise {
659 let query = match serde_json::from_str::<SubconverterQuery>(query_json) {
661 Ok(q) => q,
662 Err(e) => {
663 return Promise::reject(&JsValue::from_str(&format!("Failed to parse query: {}", e)));
664 }
665 };
666
667 let query_json_string = Some(query_json.to_string());
668 let future = async move {
670 match sub_process(None, query).await {
671 Ok(response) => {
672 match serde_json::to_string(&response) {
674 Ok(json) => Ok(JsValue::from_str(&json)),
675 Err(e) => Err(JsValue::from_str(&format!(
676 "Failed to serialize response: {}",
677 e
678 ))),
679 }
680 }
681 Err(e) => Err(JsValue::from_str(&format!(
682 "Subscription processing error: {}",
683 e
684 ))),
685 }
686 };
687
688 future_to_promise(future)
690}
691
692#[cfg(target_arch = "wasm32")]
693#[wasm_bindgen]
694pub fn init_settings_wasm(pref_path: &str) -> Promise {
695 let pref_path = pref_path.to_string();
696 let future = async move {
697 match init_settings(&pref_path).await {
698 Ok(_) => Ok(JsValue::from_bool(true)),
699 Err(e) => Err(JsValue::from_str(&format!(
700 "Failed to initialize settings: {}",
701 e
702 ))),
703 }
704 };
705
706 future_to_promise(future)
707}