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)]
64#[derive(Default)]
65pub struct TemplatesConfig {
66    /// The `header.html` file. HTML `<head>`
67    pub header: String,
68    /// The `body.html` file. HTML `<body>`
69    pub body: String,
70}
71
72
73pub static TEMPLATE_ADDONS: LazyLock<RwLock<TemplatesConfig>> = LazyLock::new(RwLock::default);
74
75macro_rules! get_tmpl {
76    ($name:ident) => {
77        /// Get the `$ident` template.
78        pub fn $name(&self) -> String {
79            let r = TEMPLATE_ADDONS.read().unwrap();
80            (*r).$name.to_string()
81        }
82    };
83}
84
85macro_rules! read_tmpl {
86    ($self:expr => $rel:ident->$name:ident) => {{
87        let v = &$self.$name;
88
89        if v.is_empty() {
90            String::new()
91        } else {
92            Self::read_template(PathBufD::new().extend(&[$rel, v]))
93        }
94    }};
95}
96
97impl TemplatesConfig {
98    /// Read a template to string given its `path`.
99    pub fn read_template(path: PathBufD) -> String {
100        read_to_string(path).unwrap_or_default()
101    }
102
103    /// Read the configuration and fill the static `template_addons`.
104    pub fn read_config(&self, relative: &str) {
105        let mut w = TEMPLATE_ADDONS.write().unwrap();
106        *w = TemplatesConfig {
107            header: read_tmpl!(&self => relative->header),
108            body: read_tmpl!(&self => relative->body),
109        }
110    }
111
112    // ...
113    get_tmpl!(header);
114    get_tmpl!(body);
115}
116
117/// Configuration file
118#[derive(Clone, Serialize, Deserialize, Debug)]
119pub struct Config {
120    /// The port to serve the server on
121    pub port: u16,
122    /// The name of the site
123    pub name: String,
124    /// The description of the site
125    pub description: String,
126    /// The location of the static directory, should not be supplied manually as it will be overwritten with `./.config/static`
127    #[serde(default)]
128    pub static_dir: PathBufD,
129    /// The location of media uploads on the file system
130    #[serde(default)]
131    pub media_dir: PathBufD,
132    /// HCaptcha configuration
133    pub captcha: HCaptchaConfig,
134    /// The name of the header used for reading user IP address
135    pub real_ip_header: Option<String>,
136    /// If new profile registration is enabled
137    #[serde(default)]
138    pub registration_enabled: bool,
139    /// The origin of the public server (ex: "https://rainbeam.net")
140    ///
141    /// Used in embeds and links.
142    #[serde(default)]
143    pub host: String,
144    /// The server ID for ID generation
145    pub snowflake_server_id: usize,
146    /// A list of image hosts that are blocked
147    #[serde(default)]
148    pub blocked_hosts: Vec<String>,
149    /// Tiered benefits
150    #[serde(default)]
151    pub tiers: Tiers,
152    /// A global site announcement shown at the top of the page
153    #[serde(default)]
154    pub alert: String,
155    /// Template configuration.
156    #[serde(default)]
157    pub templates: TemplatesConfig,
158    /// If plugins are verified through [Neospring](https://neospring.org) assets.
159    /// Disabling this removed plugin verification, but will ensure your server
160    /// doesn't communicate with the main Neospring server at all.
161    #[serde(default = "default_plugin_verify")]
162    pub plugin_verify: bool,
163}
164
165fn default_plugin_verify() -> bool {
166    true
167}
168
169impl Default for Config {
170    fn default() -> Self {
171        Self {
172            port: 8080,
173            name: "Rainbeam".to_string(),
174            description: "Ask, share, socialize!".to_string(),
175            static_dir: PathBufD::new(),
176            media_dir: PathBufD::new(),
177            captcha: HCaptchaConfig::default(),
178            real_ip_header: Option::None,
179            registration_enabled: true,
180            host: String::new(),
181            snowflake_server_id: 1234567890,
182            blocked_hosts: Vec::new(),
183            tiers: Tiers::default(),
184            alert: String::new(),
185            templates: TemplatesConfig::default(),
186            plugin_verify: default_plugin_verify(),
187        }
188    }
189}
190
191impl Config {
192    /// Read configuration file into [`Config`]
193    pub fn read(contents: String) -> Self {
194        toml::from_str::<Self>(&contents).unwrap()
195    }
196
197    /// Pull configuration file
198    pub fn get_config() -> Self {
199        let path = PathBufD::current().extend(&[".config", "config.toml"]);
200
201        match fs::read(&path) {
202            Ok(c) => {
203                let c = Config::read(c);
204
205                // populate TEMPLATE_ADDONS
206                c.templates
207                    .read_config(path.as_path().parent().unwrap().to_str().unwrap());
208
209                // ...
210                c
211            }
212            Err(_) => {
213                Self::update_config(Self::default()).expect("failed to write default config");
214                Self::default()
215            }
216        }
217    }
218
219    /// Update configuration file
220    pub fn update_config(contents: Self) -> Result<()> {
221        let c = fs::canonicalize(".").unwrap();
222        let here = c.to_str().unwrap();
223
224        fs::write(
225            format!("{here}/.config/config.toml"),
226            toml::to_string_pretty::<Self>(&contents).unwrap(),
227        )
228    }
229}