rainbeam_shared/
config.rs

1//! Application config manager
2use pathbufd::PathBufD;
3use serde::{Deserialize, Serialize};
4use std::fs::read_to_string;
5use std::io::Result;
6use std::sync::{LazyLock, RwLock};
7use crate::fs;
8
9#[derive(Clone, Serialize, Deserialize, Debug)]
10pub struct HCaptchaConfig {
11    /// HCaptcha site key
12    ///
13    /// Testing: 10000000-ffff-ffff-ffff-000000000001
14    pub site_key: String,
15    /// HCaptcha secret
16    ///
17    /// Testing: 0x0000000000000000000000000000000000000000
18    pub secret: String,
19}
20
21impl Default for HCaptchaConfig {
22    fn default() -> Self {
23        Self {
24            // these are testing keys - do NOT use them in production!
25            site_key: "10000000-ffff-ffff-ffff-000000000001".to_string(),
26            secret: "0x0000000000000000000000000000000000000000".to_string(),
27        }
28    }
29}
30
31/// Premium features
32#[derive(Clone, Serialize, Deserialize, Debug)]
33pub struct Tiers {
34    /// Doubled character limits for everything
35    ///
36    /// * Questions: ~~2048~~ **4096**
37    /// * Responses: ~~4096~~ **8192**
38    /// * Comments: ~~2048~~ **4096**
39    ///
40    /// *\*Carpgraph drawings stay at 32kb maximum*
41    #[serde(default)]
42    pub double_limits: i32,
43    /// A small little crown shown on the user's profile avatar
44    #[serde(default)]
45    pub avatar_crown: i32,
46    /// A small badge shwon on the user's profile
47    #[serde(default)]
48    pub profile_badge: i32,
49}
50
51impl Default for Tiers {
52    /// Everything is tier 1 by default
53    fn default() -> Self {
54        Self {
55            double_limits: 1,
56            avatar_crown: 1,
57            profile_badge: 1,
58        }
59    }
60}
61
62/// File locations for template files. Relative to the config file's parent directory.
63#[derive(Clone, Serialize, Deserialize, Debug)]
64pub struct TemplatesConfig {
65    /// The `header.html` file. HTML `<head>`
66    pub header: String,
67    /// The `body.html` file. HTML `<body>`
68    pub body: String,
69}
70
71impl Default for TemplatesConfig {
72    fn default() -> Self {
73        Self {
74            header: String::new(),
75            body: String::new(),
76        }
77    }
78}
79
80pub static TEMPLATE_ADDONS: LazyLock<RwLock<TemplatesConfig>> = LazyLock::new(|| RwLock::default());
81
82macro_rules! get_tmpl {
83    ($name:ident) => {
84        /// Get the `$ident` template.
85        pub fn $name(&self) -> String {
86            let r = TEMPLATE_ADDONS.read().unwrap();
87            (*r).$name.to_string()
88        }
89    };
90}
91
92macro_rules! read_tmpl {
93    ($self:expr => $rel:ident->$name:ident) => {{
94        let v = &$self.$name;
95
96        if v.is_empty() {
97            String::new()
98        } else {
99            Self::read_template(PathBufD::new().extend(&[$rel, v]))
100        }
101    }};
102}
103
104impl TemplatesConfig {
105    /// Read a template to string given its `path`.
106    pub fn read_template(path: PathBufD) -> String {
107        match read_to_string(path) {
108            Ok(s) => s,
109            Err(_) => String::new(),
110        }
111    }
112
113    /// Read the configuration and fill the static `template_addons`.
114    pub fn read_config(&self, relative: &str) -> () {
115        let mut w = TEMPLATE_ADDONS.write().unwrap();
116        *w = TemplatesConfig {
117            header: read_tmpl!(&self => relative->header),
118            body: read_tmpl!(&self => relative->body),
119        }
120    }
121
122    // ...
123    get_tmpl!(header);
124    get_tmpl!(body);
125}
126
127/// Configuration file
128#[derive(Clone, Serialize, Deserialize, Debug)]
129pub struct Config {
130    /// The port to serve the server on
131    pub port: u16,
132    /// The name of the site
133    pub name: String,
134    /// The description of the site
135    pub description: String,
136    /// The location of the static directory, should not be supplied manually as it will be overwritten with `./.config/static`
137    #[serde(default)]
138    pub static_dir: PathBufD,
139    /// The location of media uploads on the file system
140    #[serde(default)]
141    pub media_dir: PathBufD,
142    /// HCaptcha configuration
143    pub captcha: HCaptchaConfig,
144    /// The name of the header used for reading user IP address
145    pub real_ip_header: Option<String>,
146    /// If new profile registration is enabled
147    #[serde(default)]
148    pub registration_enabled: bool,
149    /// The origin of the public server (ex: "https://rainbeam.net")
150    ///
151    /// Used in embeds and links.
152    #[serde(default)]
153    pub host: String,
154    /// The server ID for ID generation
155    pub snowflake_server_id: usize,
156    /// A list of image hosts that are blocked
157    #[serde(default)]
158    pub blocked_hosts: Vec<String>,
159    /// Tiered benefits
160    #[serde(default)]
161    pub tiers: Tiers,
162    /// A global site announcement shown at the top of the page
163    #[serde(default)]
164    pub alert: String,
165    /// Template configuration.
166    #[serde(default)]
167    pub templates: TemplatesConfig,
168    /// If plugins are verified through [Neospring](https://neospring.org) assets.
169    /// Disabling this removed plugin verification, but will ensure your server
170    /// doesn't communicate with the main Neospring server at all.
171    #[serde(default = "default_plugin_verify")]
172    pub plugin_verify: bool,
173}
174
175fn default_plugin_verify() -> bool {
176    true
177}
178
179impl Default for Config {
180    fn default() -> Self {
181        Self {
182            port: 8080,
183            name: "Rainbeam".to_string(),
184            description: "Ask, share, socialize!".to_string(),
185            static_dir: PathBufD::new(),
186            media_dir: PathBufD::new(),
187            captcha: HCaptchaConfig::default(),
188            real_ip_header: Option::None,
189            registration_enabled: true,
190            host: String::new(),
191            snowflake_server_id: 1234567890,
192            blocked_hosts: Vec::new(),
193            tiers: Tiers::default(),
194            alert: String::new(),
195            templates: TemplatesConfig::default(),
196            plugin_verify: default_plugin_verify(),
197        }
198    }
199}
200
201impl Config {
202    /// Read configuration file into [`Config`]
203    pub fn read(contents: String) -> Self {
204        toml::from_str::<Self>(&contents).unwrap()
205    }
206
207    /// Pull configuration file
208    pub fn get_config() -> Self {
209        let path = PathBufD::current().extend(&[".config", "config.toml"]);
210
211        match fs::read(&path) {
212            Ok(c) => {
213                let c = Config::read(c);
214
215                // populate TEMPLATE_ADDONS
216                c.templates
217                    .read_config(path.as_path().parent().unwrap().to_str().unwrap());
218
219                // ...
220                c
221            }
222            Err(_) => {
223                Self::update_config(Self::default()).expect("failed to write default config");
224                Self::default()
225            }
226        }
227    }
228
229    /// Update configuration file
230    pub fn update_config(contents: Self) -> Result<()> {
231        let c = fs::canonicalize(".").unwrap();
232        let here = c.to_str().unwrap();
233
234        fs::write(
235            format!("{here}/.config/config.toml"),
236            toml::to_string_pretty::<Self>(&contents).unwrap(),
237        )
238    }
239}