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, PartialEq, Eq)]
261pub enum StringBan {
262    /// An exact string.
263    String(String),
264    /// A unicode codepoint.
265    Unicode(u32),
266}
267
268impl Default for StringBan {
269    fn default() -> Self {
270        Self::String(String::new())
271    }
272}
273
274/// Configuration file
275#[derive(Clone, Serialize, Deserialize, Debug)]
276pub struct Config {
277    /// The name of the app.
278    #[serde(default = "default_name")]
279    pub name: String,
280    /// The description of the app.
281    #[serde(default = "default_description")]
282    pub description: String,
283    /// The theme color of the app.
284    #[serde(default = "default_color")]
285    pub color: String,
286    /// The port to serve the server on.
287    #[serde(default = "default_port")]
288    pub port: u16,
289    /// A list of hosts which cannot be proxied through the image proxy.
290    ///
291    /// They will return the default banner image instead of proxying.
292    ///
293    /// It is recommended to put the host of your own public server in this list in
294    /// order to prevent a way too easy DOS.
295    #[serde(default = "default_banned_hosts")]
296    pub banned_hosts: Vec<String>,
297    /// The main public host of the server. **Not** used to check against banned hosts,
298    /// so this host should be included in there as well.
299    #[serde(default = "default_host")]
300    pub host: String,
301    /// The main public host of the littleweb server. **Not** used to check against banned hosts,
302    /// so this host should be included in there as well.
303    #[serde(default = "default_lw_host")]
304    pub lw_host: String,
305    /// Database security.
306    #[serde(default = "default_security")]
307    pub security: SecurityConfig,
308    /// The locations where different files should be matched.
309    #[serde(default = "default_dirs")]
310    pub dirs: DirsConfig,
311    /// Database configuration.
312    #[serde(default = "default_database")]
313    pub database: DatabaseConfig,
314    /// A list of files (just their name, no full path) which are NOT updated to match the
315    /// version built with the server binary.
316    #[serde(default = "default_no_track")]
317    pub no_track: Vec<String>,
318    /// A list of usernames which cannot be used. This also includes community names.
319    #[serde(default = "default_banned_usernames")]
320    pub banned_usernames: Vec<String>,
321    /// Configuration for your site's policies (terms of service, privacy).
322    #[serde(default = "default_policies")]
323    pub policies: PoliciesConfig,
324    /// Configuration for Cloudflare Turnstile.
325    #[serde(default = "default_turnstile")]
326    pub turnstile: TurnstileConfig,
327    /// The ID of the "town square" community. This community is required to allow
328    /// people to post from their profiles.
329    ///
330    /// This community **must** have open write access.
331    #[serde(default)]
332    pub town_square: usize,
333    /// The ID of the town square forum community.
334    #[serde(default)]
335    pub town_square_forum: usize,
336    /// The ID of the topic within the town square forum community that users are prompted
337    /// to post in by default. This should be some sort of "general" topic.
338    #[serde(default)]
339    pub town_square_forum_topic: usize,
340    /// The ID of the "system" user which will send system mails to users.
341    #[serde(default)]
342    pub system_user: usize,
343    #[serde(default)]
344    pub connections: ConnectionsConfig,
345    /// The path to the HTML footer file. The contents of this file are embedded
346    /// into every HTML template. They support access to template fields like `{{ user }}`.
347    #[serde(default)]
348    pub html_footer_path: String,
349    #[serde(default)]
350    pub stripe: Option<StripeConfig>,
351    /// The relative paths to manuals.
352    #[serde(default)]
353    pub manuals: ManualsConfig,
354    /// A list of banned content in posts.
355    #[serde(default)]
356    pub banned_data: Vec<StringBan>,
357    /// If user ads are enabled.
358    #[serde(default)]
359    pub enable_user_ads: bool,
360}
361
362fn default_name() -> String {
363    "Tetratto".to_string()
364}
365
366fn default_description() -> String {
367    "🐇 tetratto!".to_string()
368}
369
370fn default_color() -> String {
371    "#c9b1bc".to_string()
372}
373fn default_port() -> u16 {
374    4118
375}
376
377fn default_banned_hosts() -> Vec<String> {
378    Vec::new()
379}
380
381fn default_host() -> String {
382    String::new()
383}
384
385fn default_lw_host() -> String {
386    String::new()
387}
388
389fn default_security() -> SecurityConfig {
390    SecurityConfig::default()
391}
392
393fn default_dirs() -> DirsConfig {
394    DirsConfig::default()
395}
396
397fn default_database() -> DatabaseConfig {
398    DatabaseConfig::default()
399}
400
401fn default_no_track() -> Vec<String> {
402    Vec::new()
403}
404
405fn default_banned_usernames() -> Vec<String> {
406    vec![
407        "admin".to_string(),
408        "owner".to_string(),
409        "moderator".to_string(),
410        "api".to_string(),
411        "communities".to_string(),
412        "community".to_string(),
413        "notifs".to_string(),
414        "notification".to_string(),
415        "post".to_string(),
416        "void".to_string(),
417        "anonymous".to_string(),
418        "stacks".to_string(),
419        "stack".to_string(),
420        "search".to_string(),
421        "journals".to_string(),
422        "links".to_string(),
423        "app".to_string(),
424        "services".to_string(),
425        "domains".to_string(),
426        "mail".to_string(),
427        "product".to_string(),
428        "wallet".to_string(),
429        "products".to_string(),
430    ]
431}
432
433fn default_policies() -> PoliciesConfig {
434    PoliciesConfig::default()
435}
436
437fn default_turnstile() -> TurnstileConfig {
438    TurnstileConfig::default()
439}
440
441fn default_connections() -> ConnectionsConfig {
442    ConnectionsConfig::default()
443}
444
445fn default_manuals() -> ManualsConfig {
446    ManualsConfig::default()
447}
448
449fn default_banned_data() -> Vec<StringBan> {
450    Vec::new()
451}
452
453impl Default for Config {
454    fn default() -> Self {
455        Self {
456            name: default_name(),
457            description: default_description(),
458            color: default_color(),
459            port: default_port(),
460            banned_hosts: default_banned_hosts(),
461            host: default_host(),
462            lw_host: default_lw_host(),
463            database: default_database(),
464            security: default_security(),
465            dirs: default_dirs(),
466            no_track: default_no_track(),
467            banned_usernames: default_banned_usernames(),
468            policies: default_policies(),
469            turnstile: default_turnstile(),
470            town_square: 0,
471            town_square_forum: 0,
472            town_square_forum_topic: 0,
473            system_user: 0,
474            connections: default_connections(),
475            html_footer_path: String::new(),
476            stripe: None,
477            manuals: default_manuals(),
478            banned_data: default_banned_data(),
479            enable_user_ads: false,
480        }
481    }
482}
483
484impl Config {
485    /// Read configuration file into [`Config`]
486    pub fn read(contents: String) -> Self {
487        toml::from_str::<Self>(&contents).unwrap()
488    }
489
490    /// Pull configuration file
491    pub fn get_config() -> Self {
492        let path = PathBufD::current().join("tetratto.toml");
493
494        match fs::read_to_string(&path) {
495            Ok(c) => Config::read(c),
496            Err(_) => {
497                Self::update_config(Self::default()).expect("failed to write default config");
498                Self::default()
499            }
500        }
501    }
502
503    /// Update configuration file
504    pub fn update_config(contents: Self) -> Result<()> {
505        let c = fs::canonicalize(".").unwrap();
506        let here = c.to_str().unwrap();
507
508        fs::write(
509            format!("{here}/tetratto.toml"),
510            toml::to_string_pretty::<Self>(&contents).unwrap(),
511        )
512    }
513}