1use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::fs;
8use std::path::PathBuf;
9
10pub const DEFAULT_READ_SERVERS: &[&str] = &[
12 "https://cdn.iris.to",
13 "https://hashtree.iris.to",
14 "https://blossom.primal.net",
15];
16
17pub const DEFAULT_WRITE_SERVERS: &[&str] = &["https://upload.iris.to"];
19
20pub const DEFAULT_RELAYS: &[&str] = &[
22 "wss://temp.iris.to",
23 "wss://relay.damus.io",
24 "wss://nos.lol",
25 "wss://relay.primal.net",
26 "wss://offchain.pub",
27 "wss://upload.iris.to/nostr",
28];
29
30#[derive(Debug, Clone, Default, Serialize, Deserialize)]
32pub struct Config {
33 #[serde(default)]
34 pub server: ServerConfig,
35 #[serde(default)]
36 pub storage: StorageConfig,
37 #[serde(default)]
38 pub nostr: NostrConfig,
39 #[serde(default)]
40 pub blossom: BlossomConfig,
41 #[serde(default)]
42 pub sync: SyncConfig,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ServerConfig {
48 #[serde(default = "default_bind_address")]
49 pub bind_address: String,
50 #[serde(default = "default_true")]
51 pub enable_auth: bool,
52 #[serde(default)]
53 pub public_writes: bool,
54 #[serde(default)]
55 pub enable_webrtc: bool,
56 #[serde(default)]
57 pub stun_port: u16,
58}
59
60impl Default for ServerConfig {
61 fn default() -> Self {
62 Self {
63 bind_address: default_bind_address(),
64 enable_auth: true,
65 public_writes: false,
66 enable_webrtc: false,
67 stun_port: 0,
68 }
69 }
70}
71
72fn default_bind_address() -> String {
73 "127.0.0.1:8080".to_string()
74}
75
76fn default_true() -> bool {
77 true
78}
79
80#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "lowercase")]
83pub enum StorageBackend {
84 #[default]
86 Lmdb,
87 Fs,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct StorageConfig {
94 #[serde(default)]
96 pub backend: StorageBackend,
97 #[serde(default = "default_data_dir")]
98 pub data_dir: String,
99 #[serde(default = "default_max_size_gb")]
100 pub max_size_gb: u64,
101 #[serde(default)]
102 pub s3: Option<S3Config>,
103}
104
105impl Default for StorageConfig {
106 fn default() -> Self {
107 Self {
108 backend: StorageBackend::default(),
109 data_dir: default_data_dir(),
110 max_size_gb: default_max_size_gb(),
111 s3: None,
112 }
113 }
114}
115
116fn default_data_dir() -> String {
117 get_hashtree_dir()
118 .join("data")
119 .to_string_lossy()
120 .to_string()
121}
122
123fn default_max_size_gb() -> u64 {
124 10
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct S3Config {
130 pub endpoint: String,
131 pub bucket: String,
132 pub region: String,
133 #[serde(default)]
134 pub prefix: Option<String>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct NostrConfig {
140 #[serde(default = "default_relays")]
141 pub relays: Vec<String>,
142 #[serde(default)]
143 pub allowed_npubs: Vec<String>,
144 #[serde(default = "default_nostr_db_max_size_gb")]
146 pub db_max_size_gb: u64,
147 #[serde(default = "default_nostr_spambox_max_size_gb")]
150 pub spambox_max_size_gb: u64,
151}
152
153impl Default for NostrConfig {
154 fn default() -> Self {
155 Self {
156 relays: default_relays(),
157 allowed_npubs: vec![],
158 db_max_size_gb: default_nostr_db_max_size_gb(),
159 spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
160 }
161 }
162}
163
164fn default_nostr_db_max_size_gb() -> u64 {
165 10
166}
167
168fn default_nostr_spambox_max_size_gb() -> u64 {
169 1
170}
171
172fn default_relays() -> Vec<String> {
173 DEFAULT_RELAYS.iter().map(|s| s.to_string()).collect()
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct BlossomConfig {
179 #[serde(default)]
181 pub servers: Vec<String>,
182 #[serde(default = "default_read_servers")]
184 pub read_servers: Vec<String>,
185 #[serde(default = "default_write_servers")]
187 pub write_servers: Vec<String>,
188 #[serde(default = "default_max_upload_mb")]
190 pub max_upload_mb: u64,
191 #[serde(default)]
193 pub force_upload: bool,
194}
195
196impl Default for BlossomConfig {
197 fn default() -> Self {
198 Self {
199 servers: vec![],
200 read_servers: default_read_servers(),
201 write_servers: default_write_servers(),
202 max_upload_mb: default_max_upload_mb(),
203 force_upload: false,
204 }
205 }
206}
207
208fn default_read_servers() -> Vec<String> {
209 let mut servers: Vec<String> = DEFAULT_READ_SERVERS.iter().map(|s| s.to_string()).collect();
210 servers.sort();
211 servers
212}
213
214fn default_write_servers() -> Vec<String> {
215 DEFAULT_WRITE_SERVERS
216 .iter()
217 .map(|s| s.to_string())
218 .collect()
219}
220
221fn default_max_upload_mb() -> u64 {
222 100
223}
224
225impl BlossomConfig {
226 pub fn all_read_servers(&self) -> Vec<String> {
228 let mut servers = self.servers.clone();
229 servers.extend(self.read_servers.clone());
230 if servers.is_empty() {
231 servers = default_read_servers();
232 }
233 servers.sort();
234 servers.dedup();
235 servers
236 }
237
238 pub fn all_write_servers(&self) -> Vec<String> {
240 let mut servers = self.servers.clone();
241 servers.extend(self.write_servers.clone());
242 if servers.is_empty() {
243 servers = default_write_servers();
244 }
245 servers.sort();
246 servers.dedup();
247 servers
248 }
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct SyncConfig {
254 #[serde(default)]
255 pub enabled: bool,
256 #[serde(default = "default_true")]
257 pub sync_own: bool,
258 #[serde(default)]
259 pub sync_followed: bool,
260 #[serde(default = "default_max_concurrent")]
261 pub max_concurrent: usize,
262 #[serde(default = "default_webrtc_timeout_ms")]
263 pub webrtc_timeout_ms: u64,
264 #[serde(default = "default_blossom_timeout_ms")]
265 pub blossom_timeout_ms: u64,
266}
267
268impl Default for SyncConfig {
269 fn default() -> Self {
270 Self {
271 enabled: false,
272 sync_own: true,
273 sync_followed: false,
274 max_concurrent: default_max_concurrent(),
275 webrtc_timeout_ms: default_webrtc_timeout_ms(),
276 blossom_timeout_ms: default_blossom_timeout_ms(),
277 }
278 }
279}
280
281fn default_max_concurrent() -> usize {
282 4
283}
284
285fn default_webrtc_timeout_ms() -> u64 {
286 5000
287}
288
289fn default_blossom_timeout_ms() -> u64 {
290 10000
291}
292
293impl Config {
294 pub fn load() -> Result<Self> {
296 let config_path = get_config_path();
297
298 if config_path.exists() {
299 let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
300 toml::from_str(&content).context("Failed to parse config file")
301 } else {
302 let config = Config::default();
303 config.save()?;
304 Ok(config)
305 }
306 }
307
308 pub fn load_or_default() -> Self {
310 Self::load().unwrap_or_default()
311 }
312
313 pub fn save(&self) -> Result<()> {
315 let config_path = get_config_path();
316
317 if let Some(parent) = config_path.parent() {
318 fs::create_dir_all(parent)?;
319 }
320
321 let content = toml::to_string_pretty(self)?;
322 fs::write(&config_path, content)?;
323
324 Ok(())
325 }
326}
327
328pub fn get_hashtree_dir() -> PathBuf {
330 if let Ok(dir) = std::env::var("HTREE_CONFIG_DIR") {
331 return PathBuf::from(dir);
332 }
333 dirs::home_dir()
334 .unwrap_or_else(|| PathBuf::from("."))
335 .join(".hashtree")
336}
337
338pub fn get_config_path() -> PathBuf {
340 get_hashtree_dir().join("config.toml")
341}
342
343pub fn get_keys_path() -> PathBuf {
345 get_hashtree_dir().join("keys")
346}
347
348pub fn get_aliases_path() -> PathBuf {
350 get_hashtree_dir().join("aliases")
351}
352
353#[derive(Debug, Clone)]
355pub struct KeyEntry {
356 pub secret: String,
358 pub alias: Option<String>,
360}
361
362pub fn parse_keys_file(content: &str) -> Vec<KeyEntry> {
366 let mut entries = Vec::new();
367 for line in content.lines() {
368 let line = line.trim();
369 if line.is_empty() || line.starts_with('#') {
370 continue;
371 }
372 let parts: Vec<&str> = line.splitn(2, ' ').collect();
373 let secret = parts[0].to_string();
374 let alias = parts.get(1).map(|s| s.trim().to_string());
375 entries.push(KeyEntry { secret, alias });
376 }
377 entries
378}
379
380pub fn read_first_key() -> Option<String> {
383 let keys_path = get_keys_path();
384 let content = std::fs::read_to_string(&keys_path).ok()?;
385 let entries = parse_keys_file(&content);
386 entries.into_iter().next().map(|e| e.secret)
387}
388
389pub fn get_auth_cookie_path() -> PathBuf {
391 get_hashtree_dir().join("auth.cookie")
392}
393
394pub fn get_data_dir() -> PathBuf {
397 if let Ok(dir) = std::env::var("HTREE_DATA_DIR") {
398 return PathBuf::from(dir);
399 }
400 let config = Config::load_or_default();
401 PathBuf::from(&config.storage.data_dir)
402}
403
404pub fn detect_local_daemon_url(bind_address: Option<&str>) -> Option<String> {
406 use std::net::{SocketAddr, TcpStream};
407 use std::time::Duration;
408
409 if !prefer_local_daemon() {
410 return None;
411 }
412
413 let port = local_daemon_port(bind_address);
414 if port == 0 {
415 return None;
416 }
417
418 let addr = SocketAddr::from(([127, 0, 0, 1], port));
419 let timeout = Duration::from_millis(100);
420 TcpStream::connect_timeout(&addr, timeout).ok()?;
421 Some(format!("http://127.0.0.1:{}", port))
422}
423
424pub fn detect_local_relay_urls(bind_address: Option<&str>) -> Vec<String> {
426 let mut relays = Vec::new();
427
428 if let Some(list) =
429 parse_env_list("NOSTR_LOCAL_RELAY").or_else(|| parse_env_list("HTREE_LOCAL_RELAY"))
430 {
431 for raw in list {
432 if let Some(url) = normalize_relay_url(&raw) {
433 relays.push(url);
434 }
435 }
436 }
437
438 if let Some(base) = detect_local_daemon_url(bind_address) {
439 if let Some(ws) = normalize_relay_url(&base) {
440 let ws = ws.trim_end_matches('/');
441 let ws = if ws.contains("/ws") {
442 ws.to_string()
443 } else {
444 format!("{}/ws", ws)
445 };
446 relays.push(ws);
447 }
448 }
449
450 let mut ports = parse_env_ports("NOSTR_LOCAL_RELAY_PORTS");
451 if ports.is_empty() {
452 ports.push(4869);
453 }
454
455 let daemon_port = local_daemon_port(bind_address);
456 for port in ports {
457 if port == 0 || port == daemon_port {
458 continue;
459 }
460 if local_port_open(port) {
461 relays.push(format!("ws://127.0.0.1:{port}"));
462 }
463 }
464
465 dedupe_relays(relays)
466}
467
468pub fn resolve_relays(config_relays: &[String], bind_address: Option<&str>) -> Vec<String> {
470 let mut base = match parse_env_list("NOSTR_RELAYS") {
471 Some(list) => list,
472 None => config_relays.to_vec(),
473 };
474
475 base = base
476 .into_iter()
477 .filter_map(|r| normalize_relay_url(&r))
478 .collect();
479
480 if !prefer_local_relay() {
481 return dedupe_relays(base);
482 }
483
484 let mut combined = detect_local_relay_urls(bind_address);
485 combined.extend(base);
486 dedupe_relays(combined)
487}
488
489fn local_daemon_port(bind_address: Option<&str>) -> u16 {
490 let default_port = 8080;
491 let Some(addr) = bind_address else {
492 return default_port;
493 };
494 if let Ok(sock) = addr.parse::<std::net::SocketAddr>() {
495 return sock.port();
496 }
497 if let Some((_, port_str)) = addr.rsplit_once(':') {
498 if let Ok(port) = port_str.parse::<u16>() {
499 return port;
500 }
501 }
502 default_port
503}
504
505fn prefer_local_relay() -> bool {
506 for key in ["NOSTR_PREFER_LOCAL", "HTREE_PREFER_LOCAL_RELAY"] {
507 if let Ok(val) = std::env::var(key) {
508 let val = val.trim().to_lowercase();
509 return !matches!(val.as_str(), "0" | "false" | "no" | "off");
510 }
511 }
512 true
513}
514
515fn prefer_local_daemon() -> bool {
516 for key in [
517 "HTREE_PREFER_LOCAL_DAEMON",
518 "NOSTR_PREFER_LOCAL",
519 "HTREE_PREFER_LOCAL_RELAY",
520 ] {
521 if let Ok(val) = std::env::var(key) {
522 let val = val.trim().to_lowercase();
523 return !matches!(val.as_str(), "0" | "false" | "no" | "off");
524 }
525 }
526 false
527}
528
529fn parse_env_list(var: &str) -> Option<Vec<String>> {
530 let value = std::env::var(var).ok()?;
531 let mut items = Vec::new();
532 for part in value.split([',', ';', '\n', '\t', ' ']) {
533 let trimmed = part.trim();
534 if !trimmed.is_empty() {
535 items.push(trimmed.to_string());
536 }
537 }
538 if items.is_empty() {
539 None
540 } else {
541 Some(items)
542 }
543}
544
545fn parse_env_ports(var: &str) -> Vec<u16> {
546 let Some(list) = parse_env_list(var) else {
547 return Vec::new();
548 };
549 list.into_iter()
550 .filter_map(|item| item.parse::<u16>().ok())
551 .collect()
552}
553
554fn normalize_relay_url(raw: &str) -> Option<String> {
555 let trimmed = raw.trim();
556 if trimmed.is_empty() {
557 return None;
558 }
559 let trimmed = trimmed.trim_end_matches('/');
560 let lower = trimmed.to_lowercase();
561 if lower.starts_with("ws://") || lower.starts_with("wss://") {
562 return Some(trimmed.to_string());
563 }
564 if lower.starts_with("http://") {
565 return Some(format!("ws://{}", &trimmed[7..]));
566 }
567 if lower.starts_with("https://") {
568 return Some(format!("wss://{}", &trimmed[8..]));
569 }
570 Some(format!("ws://{}", trimmed))
571}
572
573fn local_port_open(port: u16) -> bool {
574 use std::net::{SocketAddr, TcpStream};
575 use std::time::Duration;
576
577 let addr = SocketAddr::from(([127, 0, 0, 1], port));
578 let timeout = Duration::from_millis(100);
579 TcpStream::connect_timeout(&addr, timeout).is_ok()
580}
581
582fn dedupe_relays(relays: Vec<String>) -> Vec<String> {
583 use std::collections::HashSet;
584 let mut seen = HashSet::new();
585 let mut out = Vec::new();
586 for relay in relays {
587 let key = relay.trim_end_matches('/').to_lowercase();
588 if seen.insert(key) {
589 out.push(relay);
590 }
591 }
592 out
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598 use std::net::TcpListener;
599 use std::sync::Mutex;
600
601 static ENV_LOCK: Mutex<()> = Mutex::new(());
602
603 struct EnvGuard {
604 key: &'static str,
605 prev: Option<String>,
606 }
607
608 impl EnvGuard {
609 fn set(key: &'static str, value: &str) -> Self {
610 let prev = std::env::var(key).ok();
611 std::env::set_var(key, value);
612 Self { key, prev }
613 }
614
615 fn clear(key: &'static str) -> Self {
616 let prev = std::env::var(key).ok();
617 std::env::remove_var(key);
618 Self { key, prev }
619 }
620 }
621
622 impl Drop for EnvGuard {
623 fn drop(&mut self) {
624 if let Some(prev) = &self.prev {
625 std::env::set_var(self.key, prev);
626 } else {
627 std::env::remove_var(self.key);
628 }
629 }
630 }
631
632 #[test]
633 fn test_default_config() {
634 let config = Config::default();
635 assert!(!config.blossom.read_servers.is_empty());
636 assert!(!config.blossom.write_servers.is_empty());
637 assert!(!config.nostr.relays.is_empty());
638 assert!(config
639 .nostr
640 .relays
641 .contains(&"wss://upload.iris.to/nostr".to_string()));
642 }
643
644 #[test]
645 fn test_parse_empty_config() {
646 let config: Config = toml::from_str("").unwrap();
647 assert!(!config.blossom.read_servers.is_empty());
648 }
649
650 #[test]
651 fn test_parse_partial_config() {
652 let toml = r#"
653[blossom]
654write_servers = ["https://custom.server"]
655"#;
656 let config: Config = toml::from_str(toml).unwrap();
657 assert_eq!(config.blossom.write_servers, vec!["https://custom.server"]);
658 assert!(!config.blossom.read_servers.is_empty());
659 }
660
661 #[test]
662 fn test_all_servers() {
663 let mut config = BlossomConfig::default();
664 config.servers = vec!["https://legacy.server".to_string()];
665
666 let read = config.all_read_servers();
667 assert!(read.contains(&"https://legacy.server".to_string()));
668 assert!(read.contains(&"https://cdn.iris.to".to_string()));
669 assert!(read.contains(&"https://blossom.primal.net".to_string()));
670
671 let write = config.all_write_servers();
672 assert!(write.contains(&"https://legacy.server".to_string()));
673 assert!(write.contains(&"https://upload.iris.to".to_string()));
674 }
675
676 #[test]
677 fn test_all_servers_fall_back_to_defaults_when_explicitly_empty() {
678 let config = BlossomConfig {
679 servers: vec![],
680 read_servers: vec![],
681 write_servers: vec![],
682 max_upload_mb: default_max_upload_mb(),
683 force_upload: false,
684 };
685
686 assert_eq!(config.all_read_servers(), default_read_servers());
687 assert_eq!(config.all_write_servers(), default_write_servers());
688 }
689
690 #[test]
691 fn test_storage_backend_default() {
692 let config = Config::default();
693 assert_eq!(config.storage.backend, StorageBackend::Lmdb);
694 }
695
696 #[test]
697 fn test_storage_backend_lmdb() {
698 let toml = r#"
699[storage]
700backend = "lmdb"
701"#;
702 let config: Config = toml::from_str(toml).unwrap();
703 assert_eq!(config.storage.backend, StorageBackend::Lmdb);
704 }
705
706 #[test]
707 fn test_storage_backend_fs_explicit() {
708 let toml = r#"
709[storage]
710backend = "fs"
711"#;
712 let config: Config = toml::from_str(toml).unwrap();
713 assert_eq!(config.storage.backend, StorageBackend::Fs);
714 }
715
716 #[test]
717 fn test_parse_keys_file() {
718 let content = r#"
719nsec1abc123 self
720# comment line
721nsec1def456 work
722
723nsec1ghi789
724"#;
725 let entries = parse_keys_file(content);
726 assert_eq!(entries.len(), 3);
727 assert_eq!(entries[0].secret, "nsec1abc123");
728 assert_eq!(entries[0].alias, Some("self".to_string()));
729 assert_eq!(entries[1].secret, "nsec1def456");
730 assert_eq!(entries[1].alias, Some("work".to_string()));
731 assert_eq!(entries[2].secret, "nsec1ghi789");
732 assert_eq!(entries[2].alias, None);
733 }
734
735 #[test]
736 fn test_local_daemon_port_default() {
737 assert_eq!(local_daemon_port(None), 8080);
738 }
739
740 #[test]
741 fn test_local_daemon_port_parses_ipv4() {
742 assert_eq!(local_daemon_port(Some("127.0.0.1:9090")), 9090);
743 }
744
745 #[test]
746 fn test_local_daemon_port_parses_anyhost() {
747 assert_eq!(local_daemon_port(Some("0.0.0.0:7070")), 7070);
748 }
749
750 #[test]
751 fn test_local_daemon_port_parses_ipv6() {
752 assert_eq!(local_daemon_port(Some("[::1]:6060")), 6060);
753 }
754
755 #[test]
756 fn test_local_daemon_port_parses_hostname() {
757 assert_eq!(local_daemon_port(Some("localhost:5050")), 5050);
758 }
759
760 #[test]
761 fn test_local_daemon_port_invalid() {
762 assert_eq!(local_daemon_port(Some("localhost")), 8080);
763 }
764
765 #[test]
766 fn test_detect_local_daemon_url_respects_prefer_local_flag() {
767 let _lock = ENV_LOCK.lock().unwrap();
768 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
769 let port = listener.local_addr().unwrap().port();
770 let _prefer = EnvGuard::set("NOSTR_PREFER_LOCAL", "0");
771
772 assert_eq!(
773 detect_local_daemon_url(Some(&format!("127.0.0.1:{port}"))),
774 None
775 );
776 }
777
778 #[test]
779 fn test_detect_local_daemon_url_requires_opt_in() {
780 let _lock = ENV_LOCK.lock().unwrap();
781 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
782 let port = listener.local_addr().unwrap().port();
783 let _prefer = EnvGuard::clear("HTREE_PREFER_LOCAL_DAEMON");
784 let _prefer_nostr = EnvGuard::clear("NOSTR_PREFER_LOCAL");
785 let _prefer_relay = EnvGuard::clear("HTREE_PREFER_LOCAL_RELAY");
786
787 assert_eq!(
788 detect_local_daemon_url(Some(&format!("127.0.0.1:{port}"))),
789 None
790 );
791 }
792
793 #[test]
794 fn test_detect_local_daemon_url_uses_opt_in_flag() {
795 let _lock = ENV_LOCK.lock().unwrap();
796 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
797 let port = listener.local_addr().unwrap().port();
798 let _prefer = EnvGuard::set("HTREE_PREFER_LOCAL_DAEMON", "1");
799
800 assert_eq!(
801 detect_local_daemon_url(Some(&format!("127.0.0.1:{port}"))),
802 Some(format!("http://127.0.0.1:{port}"))
803 );
804 }
805
806 #[test]
807 fn test_resolve_relays_prefers_local() {
808 let _lock = ENV_LOCK.lock().unwrap();
809 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
810 let port = listener.local_addr().unwrap().port();
811
812 let _prefer = EnvGuard::set("NOSTR_PREFER_LOCAL", "1");
813 let _ports = EnvGuard::set("NOSTR_LOCAL_RELAY_PORTS", &port.to_string());
814 let _relays = EnvGuard::clear("NOSTR_RELAYS");
815
816 let base = vec!["wss://relay.example".to_string()];
817 let resolved = resolve_relays(&base, Some("127.0.0.1:0"));
818
819 assert!(!resolved.is_empty());
820 assert_eq!(resolved[0], format!("ws://127.0.0.1:{port}"));
821 assert!(resolved.contains(&"wss://relay.example".to_string()));
822 }
823
824 #[test]
825 fn test_resolve_relays_env_override() {
826 let _lock = ENV_LOCK.lock().unwrap();
827 let _prefer = EnvGuard::set("NOSTR_PREFER_LOCAL", "0");
828 let _relays = EnvGuard::set("NOSTR_RELAYS", "wss://relay.one,wss://relay.two");
829
830 let base = vec!["wss://relay.example".to_string()];
831 let resolved = resolve_relays(&base, Some("127.0.0.1:0"));
832
833 assert_eq!(
834 resolved,
835 vec!["wss://relay.one".to_string(), "wss://relay.two".to_string()]
836 );
837 }
838}