Skip to main content

tetratto_core2/
config.rs

1use oiseau::config::{Configuration, DatabaseConfig};
2use pathbufd::PathBufD;
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::io::Result;
6
7/// Security configuration.
8#[derive(Clone, Serialize, Deserialize, Debug)]
9pub struct SecurityConfig {
10    /// If registrations are enabled.
11    #[serde(default = "default_security_registration_enabled")]
12    pub registration_enabled: bool,
13    /// The name of the header which will contain the real IP of the connecting user.
14    #[serde(default = "default_real_ip_header")]
15    pub real_ip_header: String,
16}
17
18fn default_security_registration_enabled() -> bool {
19    true
20}
21
22fn default_real_ip_header() -> String {
23    "CF-Connecting-IP".to_string()
24}
25
26impl Default for SecurityConfig {
27    fn default() -> Self {
28        Self {
29            registration_enabled: default_security_registration_enabled(),
30            real_ip_header: default_real_ip_header(),
31        }
32    }
33}
34
35/// Directories configuration.
36#[derive(Clone, Serialize, Deserialize, Debug)]
37pub struct DirsConfig {
38    /// HTML templates directory.
39    #[serde(default = "default_dir_templates")]
40    pub templates: String,
41    /// Static files directory.
42    #[serde(default = "default_dir_assets")]
43    pub assets: String,
44    /// Media (user avatars/banners) files directory.
45    #[serde(default = "default_dir_media")]
46    pub media: String,
47    /// The icons files directory.
48    #[serde(default = "default_dir_icons")]
49    pub icons: String,
50    /// The directory which holds your `rustdoc` (`cargo doc`) output. The directory should
51    /// exist, but it isn't required to actually have anything in it.
52    #[serde(default = "default_dir_rustdoc")]
53    pub rustdoc: String,
54}
55
56fn default_dir_templates() -> String {
57    "html".to_string()
58}
59
60fn default_dir_assets() -> String {
61    "public".to_string()
62}
63
64fn default_dir_media() -> String {
65    "media".to_string()
66}
67
68fn default_dir_icons() -> String {
69    "icons".to_string()
70}
71
72fn default_dir_rustdoc() -> String {
73    "reference".to_string()
74}
75
76impl Default for DirsConfig {
77    fn default() -> Self {
78        Self {
79            templates: default_dir_templates(),
80            assets: default_dir_assets(),
81            media: default_dir_media(),
82            icons: default_dir_icons(),
83            rustdoc: default_dir_rustdoc(),
84        }
85    }
86}
87
88impl Configuration for Config {
89    fn db_config(&self) -> DatabaseConfig {
90        self.database.to_owned()
91    }
92}
93
94/// Policies config (TOS/privacy)
95#[derive(Clone, Serialize, Deserialize, Debug)]
96pub struct PoliciesConfig {
97    /// The link to your terms of service page.
98    /// This is relative to `/auth/register` on the site.
99    ///
100    /// If your TOS is an HTML file located in `./public`, you can put
101    /// `/public/tos.html` here (or something).
102    pub terms_of_service: String,
103    /// The link to your privacy policy page.
104    /// This is relative to `/auth/register` on the site.
105    ///
106    /// Same deal as terms of service page.
107    pub privacy: String,
108    /// The link to your refunds policy page.
109    pub refunds: String,
110    /// The time (in ms since unix epoch) in which the site's policies last updated.
111    ///
112    /// This is required to automatically ask users to re-consent to policies.
113    ///
114    /// In user whose consent time in LESS THAN this date will be shown a dialog to re-consent to the policies.
115    ///
116    /// You can get this easily by running `echo "console.log(new Date().getTime())" | node`.
117    #[serde(default)]
118    pub last_updated: usize,
119}
120
121impl Default for PoliciesConfig {
122    fn default() -> Self {
123        Self {
124            terms_of_service: "/public/tos.html".to_string(),
125            privacy: "/public/privacy.html".to_string(),
126            refunds: "/public/refunds.html".to_string(),
127            last_updated: 0,
128        }
129    }
130}
131
132/// Cloudflare Turnstile configuration
133#[derive(Clone, Serialize, Deserialize, Debug)]
134pub struct TurnstileConfig {
135    pub site_key: String,
136    pub secret_key: String,
137}
138
139impl Default for TurnstileConfig {
140    fn default() -> Self {
141        Self {
142            site_key: "1x00000000000000000000AA".to_string(), // always passing, visible
143            secret_key: "1x0000000000000000000000000000000AA".to_string(), // always passing
144        }
145    }
146}
147
148#[derive(Clone, Serialize, Deserialize, Debug, Default)]
149pub struct ConnectionsConfig {
150    /// <https://www.last.fm/api/authspec>
151    /// <https://www.last.fm/api/account/create>
152    #[serde(default)]
153    pub last_fm_key: Option<String>,
154    /// <https://www.last.fm/api/authspec>
155    /// <https://www.last.fm/api/account/create>
156    #[serde(default)]
157    pub last_fm_secret: Option<String>,
158}
159
160/// Configuration for Stripe integration.
161///
162/// User IDs are sent to Stripe through the payment link.
163/// <https://docs.stripe.com/payment-links/url-parameters#streamline-reconciliation-with-a-url-parameter>
164///
165/// # Testing
166///
167/// - Run `stripe login` using the Stripe CLI
168/// - Run `stripe listen --forward-to localhost:4118/api/v2/service_hooks/stripe`
169/// - Use testing card numbers: <https://docs.stripe.com/testing?testing-method=card-numbers#visa>
170#[derive(Clone, Serialize, Deserialize, Debug, Default)]
171pub struct StripeConfig {
172    /// Your Stripe API secret.
173    pub secret: String,
174    /// Payment links from the Stripe dashboard.
175    ///
176    /// 1. Create a product and set the price for your membership
177    /// 2. Set the product price to a recurring subscription
178    /// 3. Create a payment link for the new product
179    /// 4. The payment link pasted into this config field should NOT include a query string
180    pub payment_links: StripePaymentLinks,
181    /// To apply benefits to user accounts, you should then go into the Stripe developer
182    /// "workbench" and create a new webhook. The webhook needs the scopes:
183    /// `invoice.payment_succeeded`, `customer.subscription.deleted`, `checkout.session.completed`, `charge.succeeded`.
184    ///
185    /// The webhook's destination address should be `{your server origin}/api/v2/service_hooks/stripe`.
186    ///
187    /// The signing secret can be found on the right after you have created the webhook.
188    pub webhook_signing_secret: String,
189    /// The URL of your customer billing portal.
190    ///
191    /// <https://docs.stripe.com/no-code/customer-portal>
192    pub billing_portal_url: String,
193    /// The text representation of prices. (like `$4 USD`)
194    pub price_texts: StripePriceTexts,
195    /// Product IDs from the Stripe dashboard.
196    ///
197    /// These are checked when we receive a webhook to ensure we provide the correct product.
198    pub product_ids: StripeProductIds,
199}
200
201#[derive(Clone, Serialize, Deserialize, Debug, Default)]
202pub struct StripePriceTexts {
203    pub supporter: String,
204}
205
206#[derive(Clone, Serialize, Deserialize, Debug, Default)]
207pub struct StripePaymentLinks {
208    pub supporter: String,
209}
210
211#[derive(Clone, Serialize, Deserialize, Debug, Default)]
212pub struct StripeProductIds {
213    pub supporter: String,
214}
215
216/// Manuals config (search help, etc)
217#[derive(Clone, Serialize, Deserialize, Debug)]
218pub struct ManualsConfig {
219    /// The page shown for help with search syntax.
220    pub search_help: String,
221}
222
223impl Default for ManualsConfig {
224    fn default() -> Self {
225        Self {
226            search_help: "".to_string(),
227        }
228    }
229}
230
231#[derive(Clone, Serialize, Deserialize, Debug, Default)]
232pub struct ServiceHostsConfig {
233    /// Buckets host <https://trisua.com/t/buckets>.
234    pub buckets: String,
235    /// Tawny host <https://trisua.com/t/tawny>.
236    #[serde(default)]
237    pub tawny: String,
238    /// Shrimpcamp Autter host <https://shrimpcamp.com>.
239    #[serde(default)]
240    pub dashboard: String,
241    #[serde(default = "default_overkit")]
242    pub overkit: String,
243}
244
245fn default_overkit() -> String {
246    "http://localhost:8026".to_string()
247}
248
249#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
250pub enum StringBan {
251    /// An exact string.
252    String(String),
253    /// A unicode codepoint.
254    Unicode(u32),
255}
256
257impl Default for StringBan {
258    fn default() -> Self {
259        Self::String(String::new())
260    }
261}
262
263/// Configuration file
264#[derive(Clone, Serialize, Deserialize, Debug)]
265pub struct Config {
266    /// The name of the app.
267    #[serde(default = "default_name")]
268    pub name: String,
269    /// The description of the app.
270    #[serde(default = "default_description")]
271    pub description: String,
272    /// The port to serve the server on.
273    #[serde(default = "default_port")]
274    pub port: u16,
275    /// A list of hosts which cannot be proxied through the image proxy.
276    ///
277    /// They will return the default banner image instead of proxying.
278    ///
279    /// It is recommended to put the host of your own public server in this list in
280    /// order to prevent a way too easy DOS.
281    #[serde(default = "default_banned_hosts")]
282    pub banned_hosts: Vec<String>,
283    /// The main public host of the server. **Not** used to check against banned hosts,
284    /// so this host should be included in there as well.
285    #[serde(default = "default_host")]
286    pub host: String,
287    /// The main public host of the required microservices.
288    #[serde(default = "default_service_hosts")]
289    pub service_hosts: ServiceHostsConfig,
290    /// Database security.
291    #[serde(default = "default_security")]
292    pub security: SecurityConfig,
293    /// The locations where different files should be matched.
294    #[serde(default = "default_dirs")]
295    pub dirs: DirsConfig,
296    /// Database configuration.
297    #[serde(default = "default_database")]
298    pub database: DatabaseConfig,
299    /// A list of usernames which cannot be used. This also includes community names.
300    #[serde(default = "default_banned_usernames")]
301    pub banned_usernames: Vec<String>,
302    /// Configuration for your site's policies (terms of service, privacy).
303    #[serde(default = "default_policies")]
304    pub policies: PoliciesConfig,
305    /// Configuration for Cloudflare Turnstile.
306    #[serde(default = "default_turnstile")]
307    pub turnstile: TurnstileConfig,
308    #[serde(default)]
309    pub connections: ConnectionsConfig,
310    #[serde(default)]
311    pub stripe: Option<StripeConfig>,
312    /// The relative paths to manuals.
313    #[serde(default)]
314    pub manuals: ManualsConfig,
315    /// A list of banned content in posts.
316    #[serde(default)]
317    pub banned_data: Vec<StringBan>,
318    /// A banner shown on every page of the site.
319    #[serde(default)]
320    pub persistent_banner: String,
321}
322
323fn default_name() -> String {
324    "Tetratto".to_string()
325}
326
327fn default_description() -> String {
328    "Tetratto (next-gen)".to_string()
329}
330
331fn default_port() -> u16 {
332    4118
333}
334
335fn default_banned_hosts() -> Vec<String> {
336    Vec::new()
337}
338
339fn default_host() -> String {
340    String::new()
341}
342
343fn default_service_hosts() -> ServiceHostsConfig {
344    ServiceHostsConfig::default()
345}
346
347fn default_security() -> SecurityConfig {
348    SecurityConfig::default()
349}
350
351fn default_dirs() -> DirsConfig {
352    DirsConfig::default()
353}
354
355fn default_database() -> DatabaseConfig {
356    DatabaseConfig::default()
357}
358
359fn default_banned_usernames() -> Vec<String> {
360    vec![
361        "admin".to_string(),
362        "owner".to_string(),
363        "moderator".to_string(),
364        "api".to_string(),
365        "communities".to_string(),
366        "community".to_string(),
367        "notification".to_string(),
368        "post".to_string(),
369        "anonymous".to_string(),
370        "search".to_string(),
371        "app".to_string(),
372    ]
373}
374
375fn default_policies() -> PoliciesConfig {
376    PoliciesConfig::default()
377}
378
379fn default_turnstile() -> TurnstileConfig {
380    TurnstileConfig::default()
381}
382
383fn default_connections() -> ConnectionsConfig {
384    ConnectionsConfig::default()
385}
386
387fn default_manuals() -> ManualsConfig {
388    ManualsConfig::default()
389}
390
391fn default_banned_data() -> Vec<StringBan> {
392    Vec::new()
393}
394
395impl Default for Config {
396    fn default() -> Self {
397        Self {
398            name: default_name(),
399            description: default_description(),
400            port: default_port(),
401            banned_hosts: default_banned_hosts(),
402            host: default_host(),
403            service_hosts: default_service_hosts(),
404            database: default_database(),
405            security: default_security(),
406            dirs: default_dirs(),
407            banned_usernames: default_banned_usernames(),
408            policies: default_policies(),
409            turnstile: default_turnstile(),
410            connections: default_connections(),
411            stripe: None,
412            manuals: default_manuals(),
413            banned_data: default_banned_data(),
414            persistent_banner: String::new(),
415        }
416    }
417}
418
419impl Config {
420    /// Read configuration file into [`Config`]
421    pub fn read(contents: String) -> Self {
422        toml::from_str::<Self>(&contents).unwrap()
423    }
424
425    /// Pull configuration file
426    pub fn get_config() -> Self {
427        let path = PathBufD::current().join("app.toml");
428
429        match fs::read_to_string(&path) {
430            Ok(c) => Config::read(c),
431            Err(_) => {
432                Self::update_config(Self::default()).expect("failed to write default config");
433                Self::default()
434            }
435        }
436    }
437
438    /// Update configuration file
439    pub fn update_config(contents: Self) -> Result<()> {
440        let c = fs::canonicalize(".").unwrap();
441        let here = c.to_str().unwrap();
442
443        fs::write(
444            format!("{here}/app.toml"),
445            toml::to_string_pretty::<Self>(&contents).unwrap(),
446        )
447    }
448}