1use std::{collections::HashMap, str::FromStr};
4
5const APP_NAME: &str = "bddap-aoc";
6pub const DEFAULT_PROFILE: &str = "default";
7
8#[derive(serde::Deserialize, serde::Serialize, Clone)]
9struct ProfileSettings {
10 session_token: String,
11}
12
13#[derive(serde::Serialize, PartialEq, Eq, Hash)]
14struct FileSafeString(String);
15
16impl FromStr for FileSafeString {
17 type Err = &'static str;
18
19 fn from_str(name: &str) -> Result<Self, Self::Err> {
20 let err = "Profile name must be filename-safe";
21 if !name
22 .chars()
23 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == ' ')
24 {
25 return Err(err);
26 }
27 if name.starts_with('-') || name.starts_with(' ') {
28 return Err(err);
29 }
30 Ok(Self(name.to_owned()))
31 }
32}
33
34impl<'de> serde::Deserialize<'de> for FileSafeString {
35 fn deserialize<D>(deserializer: D) -> Result<FileSafeString, D::Error>
36 where
37 D: serde::Deserializer<'de>,
38 {
39 let s = String::deserialize(deserializer)?;
40 s.parse().map_err(serde::de::Error::custom)
41 }
42}
43
44#[derive(serde::Deserialize, serde::Serialize, Default)]
45pub struct Config {
46 profile: HashMap<FileSafeString, ProfileSettings>,
47}
48
49impl Config {
50 pub fn load() -> anyhow::Result<Self> {
51 let config_path = config_file()?;
52
53 if !config_path.exists() {
55 return Ok(Config::default());
56 }
57
58 let config = std::fs::read_to_string(config_path)?;
59 let config: Config = toml::from_str(&config)?;
60 Ok(config)
61 }
62
63 fn save(&self) -> anyhow::Result<()> {
64 let config_path = config_file()?;
65
66 if let Some(parent) = config_path.parent() {
68 std::fs::create_dir_all(parent)?;
69 }
70
71 let config = toml::to_string_pretty(self)?;
72 std::fs::write(config_path, config)?;
73 Ok(())
74 }
75
76 fn get_profile(&self, name: FileSafeString) -> Profile {
77 let settings = self.profile.get(&name).cloned();
78 Profile { name, settings }
79 }
80
81 pub fn get_default_profile(&self) -> Profile {
82 self.get_profile(DEFAULT_PROFILE.parse().unwrap())
83 }
84}
85
86fn config_file() -> anyhow::Result<std::path::PathBuf> {
87 let base = xdg::BaseDirectories::with_prefix(APP_NAME)?;
88 Ok(base.get_cache_home().join("config.toml"))
89}
90
91fn cache_dir() -> anyhow::Result<std::path::PathBuf> {
92 let base = xdg::BaseDirectories::with_prefix(APP_NAME)?;
93 Ok(base.get_cache_home())
94}
95
96pub fn login() -> Result<(), anyhow::Error> {
102 println!("{}", include_str!("how_to_find_session_token.txt"));
103 let session_token = rpassword::prompt_password("Enter session cookie: ")?;
104 let mut config = Config::load()?;
105 let default_profile = DEFAULT_PROFILE.parse().unwrap();
106 let profile = config
107 .profile
108 .entry(default_profile)
109 .or_insert_with(|| ProfileSettings {
110 session_token: "".to_string(),
111 });
112 profile.session_token = session_token;
113 config.save()?;
114 println!(
115 "Session cookie has been saved to {}",
116 config_file()?.display()
117 );
118 Ok(())
119}
120
121fn get_or_create_cache(
122 profile: &FileSafeString,
123 year: usize,
124 day: usize,
125) -> anyhow::Result<std::path::PathBuf> {
126 let profile_cache_dir = cache_dir()?.join("inputs").join(&profile.0);
127 std::fs::create_dir_all(&profile_cache_dir)?;
128 Ok(profile_cache_dir.join(format!("{}-{}.txt", year, day)))
129}
130
131pub struct Profile {
132 name: FileSafeString,
133 settings: Option<ProfileSettings>,
134}
135
136impl Profile {
137 pub fn set_cached(&self, year: usize, day: usize, input: &str) -> anyhow::Result<()> {
138 let location = get_or_create_cache(&self.name, year, day)?;
139 std::fs::write(location, input)?;
140 Ok(())
141 }
142
143 pub fn get_cached(&self, year: usize, day: usize) -> anyhow::Result<Option<String>> {
144 let location = get_or_create_cache(&self.name, year, day)?;
145 if !location.exists() {
146 return Ok(None);
147 }
148 let input = std::fs::read_to_string(location)?;
149 Ok(Some(input))
150 }
151
152 pub fn get_session_token(&self) -> Option<&str> {
153 let settings = self.settings.as_ref()?;
154 Some(&settings.session_token)
155 }
156}