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
22#[derive(Deserialize, Serialize, Debug, Default, Clone)]
24pub struct SubconverterQuery {
25 pub target: Option<String>,
27 #[serde(default = "default_ver")]
29 pub ver: u32,
30 pub new_name: Option<bool>,
32 pub url: Option<String>,
34 pub group: Option<String>,
36 pub upload_path: Option<String>,
38 pub include: Option<String>,
40 pub exclude: Option<String>,
42 pub groups: Option<String>,
44 pub ruleset: Option<String>,
46 pub config: Option<String>,
48
49 pub dev_id: Option<String>,
51 pub insert: Option<bool>,
53 pub prepend: Option<bool>,
55 pub filename: Option<String>,
57 pub append_type: Option<bool>,
59 pub emoji: Option<bool>,
61 pub add_emoji: Option<bool>,
63 pub remove_emoji: Option<bool>,
65 pub list: Option<bool>,
67 pub sort: Option<bool>,
69
70 pub sort_script: Option<String>,
72
73 pub fdn: Option<bool>,
75
76 pub rename: Option<String>,
78 pub tfo: Option<bool>,
80 pub udp: Option<bool>,
82 pub scv: Option<bool>,
84 pub tls13: Option<bool>,
86 pub rename_node: Option<bool>,
88 pub interval: Option<u32>,
90 pub strict: Option<bool>,
92 pub upload: Option<bool>,
94 pub token: Option<String>,
96 pub filter: Option<String>,
98
99 pub script: Option<bool>,
101 pub classic: Option<bool>,
102
103 pub expand: Option<bool>,
104
105 #[serde(default)]
107 pub singbox: HashMap<String, String>,
108
109 pub request_headers: Option<HashMap<String, String>>,
111}
112
113pub fn parse_query_string(query: &str) -> HashMap<String, String> {
115 let mut params = HashMap::new();
116 for pair in query.split('&') {
117 let mut parts = pair.splitn(2, '=');
118 if let Some(key) = parts.next() {
119 let value = parts.next().unwrap_or("");
120 params.insert(key.to_string(), value.to_string());
121 }
122 }
123 params
124}
125
126#[derive(Debug, Serialize)]
128pub struct SubResponse {
129 pub content: String,
130 pub content_type: String,
131 pub headers: HashMap<String, String>,
132 pub status_code: u16,
133 #[serde(skip_serializing_if = "is_not_attempted")] pub upload_status: UploadStatus,
135}
136
137fn is_not_attempted(status: &UploadStatus) -> bool {
139 matches!(status, UploadStatus::NotAttempted)
140}
141
142impl SubResponse {
143 pub fn ok(content: String, content_type: String) -> Self {
144 Self {
145 content,
146 content_type,
147 headers: HashMap::new(),
148 status_code: 200,
149 upload_status: UploadStatus::NotAttempted, }
151 }
152
153 pub fn error(content: String, status_code: u16) -> Self {
154 Self {
155 content,
156 content_type: "text/plain".to_string(),
157 headers: HashMap::new(),
158 status_code,
159 upload_status: UploadStatus::NotAttempted, }
161 }
162
163 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
164 self.headers = headers;
165 self
166 }
167
168 pub fn with_upload_status(mut self, status: UploadStatus) -> Self {
169 self.upload_status = status;
170 self
171 }
172}
173
174pub async fn sub_process(
176 req_url: Option<String>,
177 query: SubconverterQuery,
178) -> Result<SubResponse, Box<dyn std::error::Error>> {
179 let mut global = Settings::current();
180
181 if global.pref_path.is_empty() {
183 debug!("Global config not initialized, reloading");
184 init_settings("").await?;
185 global = Settings::current();
186 } else if global.reload_conf_on_request && !global.api_mode && !global.generator_mode {
187 refresh_configuration().await;
188 global = Settings::current();
189 }
190
191 let mut builder = SubconverterConfigBuilder::new();
193
194 let target;
195 if let Some(_target) = &query.target {
196 match SubconverterTarget::from_str(&_target) {
197 Some(_target) => {
198 target = _target.clone();
199 if _target == SubconverterTarget::Auto {
200 return Ok(SubResponse::error(
212 "Auto user agent is not supported for now.".to_string(),
213 400,
214 ));
215 }
216 builder.target(_target);
217 }
218 None => {
219 return Ok(SubResponse::error(
220 "Invalid target parameter".to_string(),
221 400,
222 ));
223 }
224 }
225 } else {
226 return Ok(SubResponse::error(
227 "Missing target parameter".to_string(),
228 400,
229 ));
230 }
231
232 builder.update_interval(match query.interval {
233 Some(interval) => interval,
234 None => global.update_interval,
235 });
236 #[cfg(not(target_arch = "wasm32"))]
238 let authorized = false;
239
240 #[cfg(target_arch = "wasm32")]
241 let authorized =
242 !global.api_mode || query.token.as_deref().unwrap_or_default() == global.api_access_token;
243 builder.authorized(authorized);
244 builder.update_strict(query.strict.unwrap_or(global.update_strict));
245
246 if query
247 .include
248 .clone()
249 .is_some_and(|include| REGEX_BLACK_LIST.contains(&include))
250 || query
251 .exclude
252 .clone()
253 .is_some_and(|exclude| REGEX_BLACK_LIST.contains(&exclude))
254 {
255 return Ok(SubResponse::error(
256 "Invalid regex in request!".to_string(),
257 400,
258 ));
259 }
260
261 let enable_insert = match query.insert {
262 Some(insert) => insert,
263 None => global.enable_insert,
264 };
265
266 if enable_insert {
267 builder.insert_urls(global.insert_urls.clone());
268 builder.prepend_insert(query.prepend.unwrap_or(global.prepend_insert));
270 }
271
272 let urls = match query.url.as_deref() {
273 Some(query_url) => query_url.split('|').map(|s| s.to_owned()).collect(),
274 None => {
275 if authorized {
276 global.default_urls.clone()
277 } else {
278 vec![]
279 }
280 }
281 };
282 builder.urls(urls);
283
284 let mut template_args = TemplateArgs::default();
288 template_args.global_vars = global.template_vars.clone();
289
290 template_args.request_params = query.clone();
291
292 builder.append_proxy_type(query.append_type.unwrap_or(global.append_type));
293
294 let mut arg_expand_rulesets = query.expand;
295 if target.is_clash() && query.script.is_none() {
296 arg_expand_rulesets = Some(true);
297 }
298
299 builder.tfo(query.tfo.or(global.tfo_flag));
301 builder.udp(query.udp.or(global.udp_flag));
302 builder.skip_cert_verify(query.scv.or(global.skip_cert_verify));
303 builder.tls13(query.tls13.or(global.tls13_flag));
304 builder.sort(query.sort.unwrap_or(global.enable_sort));
305 builder.sort_script(query.sort_script.unwrap_or(global.sort_script.clone()));
306
307 builder.filter_deprecated(query.fdn.unwrap_or(global.filter_deprecated));
308 builder.clash_new_field_name(query.new_name.unwrap_or(global.clash_use_new_field));
309 builder.clash_script(query.script.unwrap_or_default());
310 builder.clash_classical_ruleset(query.classic.unwrap_or_default());
311 let nodelist = query.list.unwrap_or_default();
312 builder.nodelist(nodelist);
313
314 if arg_expand_rulesets != Some(true) {
315 builder.clash_new_field_name(true);
316 } else {
317 builder.managed_config_prefix(global.managed_config_prefix.clone());
318 builder.clash_script(false);
319 }
320
321 let mut ruleset_configs = global.custom_rulesets.clone();
322 let mut custom_group_configs = global.custom_proxy_groups.clone();
323
324 builder.include_remarks(global.include_remarks.clone());
326 builder.exclude_remarks(global.exclude_remarks.clone());
327 builder.rename_array(global.renames.clone());
328 builder.emoji_array(global.emojis.clone());
329 builder.add_emoji(global.add_emoji);
330 builder.remove_emoji(global.remove_emoji);
331 builder.enable_rule_generator(global.enable_rule_gen);
332 let mut rule_bases = RuleBases {
333 clash_rule_base: global.clash_base.clone(),
334 surge_rule_base: global.surge_base.clone(),
335 surfboard_rule_base: global.surfboard_base.clone(),
336 mellow_rule_base: global.mellow_base.clone(),
337 quan_rule_base: global.quan_base.clone(),
338 quanx_rule_base: global.quanx_base.clone(),
339 loon_rule_base: global.loon_base.clone(),
340 sssub_rule_base: global.ssub_base.clone(),
341 singbox_rule_base: global.singbox_base.clone(),
342 };
343 builder.rule_bases(rule_bases.clone());
344 builder.template_args(template_args.clone());
345
346 let ext_config = match query.config.as_deref() {
347 Some(config) => config.to_owned(),
348 None => global.default_ext_config.clone(),
349 };
350 if !ext_config.is_empty() {
351 debug!("Loading external config from {}", ext_config);
352
353 let extconf_result = ExternalSettings::load_from_file(&ext_config).await;
356
357 match extconf_result {
358 Ok(extconf) => {
359 debug!("Successfully loaded external config from {}", ext_config);
360 if !nodelist {
361 rule_bases
362 .check_external_bases(&extconf, &global.base_path)
363 .await;
364 builder.rule_bases(rule_bases);
365
366 if let Some(tpl_args) = extconf.tpl_args {
367 template_args.local_vars = tpl_args;
368 }
369
370 builder.template_args(template_args);
371
372 if !target.is_simple() {
373 if !extconf.custom_rulesets.is_empty() {
374 ruleset_configs = extconf.custom_rulesets;
375 }
376 if !extconf.custom_proxy_groups.is_empty() {
377 custom_group_configs = extconf.custom_proxy_groups;
378 }
379 if let Some(enable_rule_gen) = extconf.enable_rule_generator {
380 builder.enable_rule_generator(enable_rule_gen);
381 }
382 if let Some(overwrite_original_rules) = extconf.overwrite_original_rules {
383 builder.overwrite_original_rules(overwrite_original_rules);
384 }
385 }
386 }
387 if !extconf.rename_nodes.is_empty() {
388 builder.rename_array(extconf.rename_nodes);
389 }
390 if !extconf.emojis.is_empty() {
391 builder.emoji_array(extconf.emojis);
392 }
393 if !extconf.include_remarks.is_empty() {
394 builder.include_remarks(extconf.include_remarks);
395 }
396 if !extconf.exclude_remarks.is_empty() {
397 builder.exclude_remarks(extconf.exclude_remarks);
398 }
399 if extconf.add_emoji.is_some() {
400 builder.add_emoji(extconf.add_emoji.unwrap());
401 }
402 if extconf.remove_old_emoji.is_some() {
403 builder.remove_emoji(extconf.remove_old_emoji.unwrap());
404 }
405 }
406 Err(e) => {
407 error!("Failed to load external config from {}: {}", ext_config, e);
408 }
409 }
410 }
411
412 if let Some(include) = query.include.as_deref() {
414 if reg_valid(&include) {
415 builder.include_remarks(vec![include.to_owned()]);
416 }
417 }
418 if let Some(exclude) = query.exclude.as_deref() {
419 if reg_valid(&exclude) {
420 builder.exclude_remarks(vec![exclude.to_owned()]);
421 }
422 }
423 if let Some(emoji) = query.emoji {
424 builder.add_emoji(emoji);
425 builder.remove_emoji(true);
426 }
427
428 if let Some(add_emoji) = query.add_emoji {
429 builder.add_emoji(add_emoji);
430 }
431 if let Some(remove_emoji) = query.remove_emoji {
432 builder.remove_emoji(remove_emoji);
433 }
434 if let Some(rename) = query.rename.as_deref() {
435 if !rename.is_empty() {
436 let v_array: Vec<String> = rename.split('`').map(|s| s.to_string()).collect();
437 builder.rename_array(RegexMatchConfigs::from_ini_with_delimiter(&v_array, "@"));
438 }
439 }
440
441 if !target.is_simple() {
442 if !query
444 .groups
445 .as_deref()
446 .is_none_or(|groups| groups.is_empty())
447 && !nodelist
448 {
449 if let Some(groups) = query.groups.as_deref() {
450 let v_array: Vec<String> = groups.split('@').map(|s| s.to_string()).collect();
451 custom_group_configs = ProxyGroupConfigs::from_ini(&v_array);
452 }
453 }
454 if !query
456 .ruleset
457 .as_deref()
458 .is_none_or(|ruleset| ruleset.is_empty())
459 && !nodelist
460 {
461 if let Some(ruleset) = query.ruleset.as_deref() {
462 let v_array: Vec<String> = ruleset.split('@').map(|s| s.to_string()).collect();
463 ruleset_configs = RulesetConfigs::from_ini(&v_array);
464 }
465 }
466 }
467 builder.proxy_groups(custom_group_configs);
468 builder.ruleset_configs(ruleset_configs);
469
470 builder.group_name(query.group.clone());
476 builder.filename(query.filename.clone());
477 builder.upload(query.upload.unwrap_or_default());
478
479 let filter = query.filter.unwrap_or(global.filter_script.clone());
481 if !filter.is_empty() {
482 builder.filter_script(Some(filter));
483 }
484
485 if let Some(request_headers) = &query.request_headers {
496 builder.request_headers(request_headers.clone());
497 }
498
499 let config = match builder.build() {
501 Ok(cfg) => cfg,
502 Err(e) => {
503 error!("Failed to build subconverter config: {}", e);
504 return Ok(SubResponse::error(
505 format!("Configuration error: {}", e),
506 400,
507 ));
508 }
509 };
510
511 debug!("Running subconverter with config: {:?}", config);
514 let subconverter_result = subconverter(config).await;
515
516 match subconverter_result {
517 Ok(result) => {
518 let content_type = match target {
520 SubconverterTarget::Clash
521 | SubconverterTarget::ClashR
522 | SubconverterTarget::SingBox => "application/yaml",
523 SubconverterTarget::SSSub | SubconverterTarget::SSD => "application/json",
524 _ => "text/plain",
525 };
526
527 debug!("Subconverter completed successfully");
528 Ok(SubResponse::ok(result.content, content_type.to_string())
529 .with_headers(result.headers)
530 .with_upload_status(result.upload_status))
531 }
532 Err(e) => {
533 error!("Subconverter error: {}", e);
534 Ok(SubResponse::error(format!("Conversion error: {}", e), 500))
535 }
536 }
537}
538
539#[cfg(target_arch = "wasm32")]
540#[wasm_bindgen]
541pub fn sub_process_wasm(query_json: &str) -> Promise {
542 let query = match serde_json::from_str::<SubconverterQuery>(query_json) {
544 Ok(q) => q,
545 Err(e) => {
546 return Promise::reject(&JsValue::from_str(&format!("Failed to parse query: {}", e)));
547 }
548 };
549
550 let query_json_string = Some(query_json.to_string());
551 let future = async move {
553 match sub_process(None, query).await {
554 Ok(response) => {
555 match serde_json::to_string(&response) {
557 Ok(json) => Ok(JsValue::from_str(&json)),
558 Err(e) => Err(JsValue::from_str(&format!(
559 "Failed to serialize response: {}",
560 e
561 ))),
562 }
563 }
564 Err(e) => Err(JsValue::from_str(&format!(
565 "Subscription processing error: {}",
566 e
567 ))),
568 }
569 };
570
571 future_to_promise(future)
573}
574
575#[cfg(target_arch = "wasm32")]
576#[wasm_bindgen]
577pub fn init_settings_wasm(pref_path: &str) -> Promise {
578 let pref_path = pref_path.to_string();
579 let future = async move {
580 match init_settings(&pref_path).await {
581 Ok(_) => Ok(JsValue::from_bool(true)),
582 Err(e) => Err(JsValue::from_str(&format!(
583 "Failed to initialize settings: {}",
584 e
585 ))),
586 }
587 };
588
589 future_to_promise(future)
590}