Skip to main content

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