rb/routing/pages/
mod.rs

1use rainbeam::{
2    database::Database,
3    model::{RelationshipStatus, Question, Reaction, FullResponse, DatabaseError},
4};
5use rainbeam_shared::config::Config;
6use authbeam::{
7    simplify,
8    model::{Profile, ProfileMetadata, Notification, FinePermission, IpBan, ItemType, ItemStatus},
9};
10use langbeam::LangFile;
11
12use axum::{
13    extract::{Path, Query, State},
14    response::{Html, IntoResponse},
15    routing::{get, Router},
16};
17use axum_extra::extract::CookieJar;
18use ammonia::Builder;
19use reqwest::StatusCode;
20use reva_axum::Template;
21
22use crate::ToHtml;
23use serde::{Serialize, Deserialize};
24use std::collections::HashMap;
25
26use super::api;
27
28pub mod market;
29pub mod models;
30pub mod profile;
31pub mod search;
32pub mod settings;
33
34/// Escape a username's characters if we are unable to find a "good" character
35///
36/// A "good" character is any alphanumeric character.
37pub fn escape_username(name: &String) -> String {
38    // comb through chars, if we never find anything that is actually a letter,
39    // go ahead and escape
40    let mut found_good: bool = false;
41
42    for char in name.chars() {
43        if char.is_alphanumeric() {
44            found_good = true;
45            break;
46        }
47    }
48
49    if !found_good {
50        return "bad username".to_string();
51    }
52
53    // return given data
54    name.to_owned()
55}
56
57#[derive(Template)]
58#[template(path = "error.html")]
59pub struct ErrorTemplate {
60    pub config: Config,
61    pub lang: LangFile,
62    pub profile: Option<Box<Profile>>,
63    pub message: String,
64}
65
66pub async fn not_found(State(database): State<Database>) -> impl IntoResponse {
67    (
68        StatusCode::NOT_FOUND,
69        Html(DatabaseError::NotFound.to_html(database)),
70    )
71}
72
73#[derive(Template)]
74#[template(path = "homepage.html")]
75struct HomepageTemplate {
76    config: Config,
77    lang: langbeam::LangFile,
78    profile: Option<Box<Profile>>,
79}
80
81#[derive(Template)]
82#[template(path = "timelines/timeline.html")]
83struct TimelineTemplate {
84    config: Config,
85    lang: langbeam::LangFile,
86    profile: Option<Box<Profile>>,
87    unread: usize,
88    notifs: usize,
89    friends: Vec<(Box<Profile>, Box<Profile>)>,
90    page: i32,
91}
92
93/// GET /
94pub async fn homepage_request(
95    jar: CookieJar,
96    State(database): State<Database>,
97    Query(props): Query<PaginatedQuery>,
98) -> impl IntoResponse {
99    let auth_user = match jar.get("__Secure-Token") {
100        Some(c) => match database
101            .auth
102            .get_profile_by_unhashed(c.value_trimmed())
103            .await
104        {
105            Ok(ua) => Some(ua),
106            Err(_) => None,
107        },
108        None => None,
109    };
110
111    // timeline
112    if let Some(ref ua) = auth_user {
113        let unread = database.get_inbox_count_by_recipient(&ua.id).await;
114
115        let notifs = database
116            .auth
117            .get_notification_count_by_recipient(&ua.id)
118            .await;
119
120        // ...
121        return Html(
122            TimelineTemplate {
123                config: database.config.clone(),
124                lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
125                    c.value_trimmed()
126                } else {
127                    ""
128                }),
129                profile: auth_user.clone(),
130                unread,
131                notifs,
132                friends: database
133                    .auth
134                    .get_user_participating_relationships_of_status(
135                        &ua.id,
136                        RelationshipStatus::Friends,
137                    )
138                    .await
139                    .unwrap(),
140                page: props.page,
141            }
142            .render()
143            .unwrap(),
144        );
145    }
146
147    // homepage
148    Html(
149        HomepageTemplate {
150            config: database.config.clone(),
151            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
152                c.value_trimmed()
153            } else {
154                ""
155            }),
156            profile: auth_user,
157        }
158        .render()
159        .unwrap(),
160    )
161}
162
163#[derive(Template)]
164#[template(path = "partials/timelines/timeline.html")]
165struct PartialTimelineTemplate {
166    config: Config,
167    lang: langbeam::LangFile,
168    profile: Option<Box<Profile>>,
169    responses: Vec<FullResponse>,
170    relationships: HashMap<String, RelationshipStatus>,
171    is_powerful: bool,
172    is_helper: bool,
173}
174
175/// GET /_app/timelines/timeline.html
176pub async fn partial_timeline_request(
177    jar: CookieJar,
178    State(database): State<Database>,
179    Query(props): Query<PaginatedQuery>,
180) -> impl IntoResponse {
181    let auth_user = match jar.get("__Secure-Token") {
182        Some(c) => match database
183            .auth
184            .get_profile_by_unhashed(c.value_trimmed())
185            .await
186        {
187            Ok(ua) => ua,
188            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
189        },
190        None => return Html(DatabaseError::NotAllowed.to_html(database)),
191    };
192
193    let responses = match database
194        .get_responses_by_following_paginated(&auth_user.id, props.page)
195        .await
196    {
197        Ok(responses) => responses,
198        Err(e) => return Html(e.to_html(database)),
199    };
200
201    let mut is_helper: bool = false;
202    let is_powerful = {
203        let group = match database.auth.get_group_by_id(auth_user.group).await {
204            Ok(g) => g,
205            Err(_) => return Html(DatabaseError::Other.to_html(database)),
206        };
207
208        if group.permissions.check_helper() {
209            is_helper = true
210        }
211
212        group.permissions.check_manager()
213    };
214
215    // build relationships list
216    let mut relationships: HashMap<String, RelationshipStatus> = HashMap::new();
217
218    for response in &responses {
219        if relationships.contains_key(&response.1.author.id) {
220            continue;
221        }
222
223        if is_helper {
224            // make sure staff can view your responses
225            relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
226            continue;
227        }
228
229        if response.1.author.id == auth_user.id {
230            // make sure we can view our own responses
231            relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
232            continue;
233        };
234
235        relationships.insert(
236            response.1.author.id.clone(),
237            database
238                .auth
239                .get_user_relationship(&response.1.author.id, &auth_user.id)
240                .await
241                .0,
242        );
243    }
244
245    // ...
246    return Html(
247        PartialTimelineTemplate {
248            config: database.config.clone(),
249            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
250                c.value_trimmed()
251            } else {
252                ""
253            }),
254            profile: Some(auth_user.clone()),
255            responses,
256            relationships,
257            is_powerful,
258            is_helper,
259        }
260        .render()
261        .unwrap(),
262    );
263}
264
265#[derive(Template)]
266#[template(path = "timelines/public_timeline.html")]
267struct PublicTimelineTemplate {
268    config: Config,
269    lang: langbeam::LangFile,
270    profile: Option<Box<Profile>>,
271    unread: usize,
272    notifs: usize,
273    page: i32,
274}
275
276/// GET /public
277pub async fn public_timeline_request(
278    jar: CookieJar,
279    State(database): State<Database>,
280    Query(props): Query<PaginatedQuery>,
281) -> impl IntoResponse {
282    let auth_user = match jar.get("__Secure-Token") {
283        Some(c) => match database
284            .auth
285            .get_profile_by_unhashed(c.value_trimmed())
286            .await
287        {
288            Ok(ua) => ua,
289            Err(_) => return Html(DatabaseError::NotAllowed.to_string()),
290        },
291        None => return Html(DatabaseError::NotAllowed.to_string()),
292    };
293
294    // timeline
295    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
296
297    let notifs = database
298        .auth
299        .get_notification_count_by_recipient(&auth_user.id)
300        .await;
301
302    // ...
303    return Html(
304        PublicTimelineTemplate {
305            config: database.config.clone(),
306            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
307                c.value_trimmed()
308            } else {
309                ""
310            }),
311            profile: Some(auth_user),
312            unread,
313            notifs,
314            page: props.page,
315        }
316        .render()
317        .unwrap(),
318    );
319}
320
321#[derive(Template)]
322#[template(path = "partials/timelines/timeline.html")]
323struct PartialPublicTimelineTemplate {
324    config: Config,
325    lang: langbeam::LangFile,
326    profile: Option<Box<Profile>>,
327    responses: Vec<FullResponse>,
328    relationships: HashMap<String, RelationshipStatus>,
329    is_powerful: bool,
330    is_helper: bool,
331}
332
333/// GET /_app/timelines/public_timeline.html
334pub async fn partial_public_timeline_request(
335    jar: CookieJar,
336    State(database): State<Database>,
337    Query(props): Query<PaginatedQuery>,
338) -> impl IntoResponse {
339    let auth_user = match jar.get("__Secure-Token") {
340        Some(c) => match database
341            .auth
342            .get_profile_by_unhashed(c.value_trimmed())
343            .await
344        {
345            Ok(ua) => ua,
346            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
347        },
348        None => return Html(DatabaseError::NotAllowed.to_html(database)),
349    };
350
351    let responses = match database.get_responses_paginated(props.page).await {
352        Ok(responses) => responses,
353        Err(e) => return Html(e.to_html(database)),
354    };
355
356    let mut is_helper: bool = false;
357    let is_powerful = {
358        let group = match database.auth.get_group_by_id(auth_user.group).await {
359            Ok(g) => g,
360            Err(_) => return Html(DatabaseError::Other.to_html(database)),
361        };
362
363        if group.permissions.check_helper() {
364            is_helper = true
365        }
366
367        group.permissions.check_manager()
368    };
369
370    // build relationships list
371    let mut relationships: HashMap<String, RelationshipStatus> = HashMap::new();
372
373    for response in &responses {
374        if relationships.contains_key(&response.1.author.id) {
375            continue;
376        }
377
378        if is_helper {
379            // make sure staff can view your responses
380            relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
381            continue;
382        }
383
384        if response.1.author.id == auth_user.id {
385            // make sure we can view our own responses
386            relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
387            continue;
388        };
389
390        relationships.insert(
391            response.1.author.id.clone(),
392            database
393                .auth
394                .get_user_relationship(&response.1.author.id, &auth_user.id)
395                .await
396                .0,
397        );
398    }
399
400    // ...
401    return Html(
402        PartialPublicTimelineTemplate {
403            config: database.config.clone(),
404            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
405                c.value_trimmed()
406            } else {
407                ""
408            }),
409            profile: Some(auth_user.clone()),
410            responses,
411            relationships,
412            is_powerful,
413            is_helper,
414        }
415        .render()
416        .unwrap(),
417    );
418}
419
420#[derive(Template)]
421#[template(path = "partials/timelines/discover/responses_top.html")]
422struct PartialTopResponsesTemplate {
423    config: Config,
424    lang: langbeam::LangFile,
425    profile: Option<Box<Profile>>,
426    responses: Vec<FullResponse>,
427    relationships: HashMap<String, RelationshipStatus>,
428    is_powerful: bool,
429    is_helper: bool,
430}
431
432/// GET /_app/timelines/discover/responses_top.html
433pub async fn partial_top_responses_request(
434    jar: CookieJar,
435    State(database): State<Database>,
436) -> impl IntoResponse {
437    let auth_user = match jar.get("__Secure-Token") {
438        Some(c) => match database
439            .auth
440            .get_profile_by_unhashed(c.value_trimmed())
441            .await
442        {
443            Ok(ua) => Some(ua),
444            Err(_) => None,
445        },
446        None => None,
447    };
448
449    let responses = match database.get_top_reacted_responses(604_800_000).await {
450        Ok(r) => r,
451        Err(e) => return Html(e.to_html(database)),
452    };
453
454    let mut is_helper: bool = false;
455    let is_powerful = if let Some(ref ua) = auth_user {
456        let group = match database.auth.get_group_by_id(ua.group).await {
457            Ok(g) => g,
458            Err(_) => return Html(DatabaseError::Other.to_html(database)),
459        };
460
461        if group.permissions.check_helper() {
462            is_helper = true
463        }
464
465        group.permissions.check_manager()
466    } else {
467        false
468    };
469
470    // build relationships list
471    let mut relationships: HashMap<String, RelationshipStatus> = HashMap::new();
472
473    if let Some(ref ua) = auth_user {
474        for response in &responses {
475            if relationships.contains_key(&response.1.author.id) {
476                continue;
477            }
478
479            if is_helper {
480                // make sure staff can view your responses
481                relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
482                continue;
483            }
484
485            if response.1.author.id == ua.id {
486                // make sure we can view our own responses
487                relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
488                continue;
489            };
490
491            relationships.insert(
492                response.1.author.id.clone(),
493                database
494                    .auth
495                    .get_user_relationship(&response.1.author.id, &ua.id)
496                    .await
497                    .0,
498            );
499        }
500    } else {
501        // the posts timeline requires that we have an entry for every relationship,
502        // since we don't have an account every single relationship should be unknown
503        for response in &responses {
504            if relationships.contains_key(&response.1.author.id) {
505                continue;
506            }
507
508            relationships.insert(response.1.author.id.clone(), RelationshipStatus::Unknown);
509        }
510    }
511
512    // ...
513    return Html(
514        PartialTopResponsesTemplate {
515            config: database.config.clone(),
516            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
517                c.value_trimmed()
518            } else {
519                ""
520            }),
521            profile: auth_user,
522            responses,
523            relationships,
524            is_powerful,
525            is_helper,
526        }
527        .render()
528        .unwrap(),
529    );
530}
531
532#[derive(Template)]
533#[template(path = "partials/timelines/discover/questions_most.html")]
534struct PartialTopAskersTemplate {
535    lang: langbeam::LangFile,
536    users: Vec<(usize, Box<Profile>)>,
537}
538
539/// GET /_app/timelines/discover/questions_most.html
540pub async fn partial_top_askers_request(
541    jar: CookieJar,
542    State(database): State<Database>,
543) -> impl IntoResponse {
544    let users = match database.get_top_askers().await {
545        Ok(r) => r,
546        Err(e) => return Html(e.to_html(database)),
547    };
548
549    // ...
550    return Html(
551        PartialTopAskersTemplate {
552            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
553                c.value_trimmed()
554            } else {
555                ""
556            }),
557            users,
558        }
559        .render()
560        .unwrap(),
561    );
562}
563
564#[derive(Template)]
565#[template(path = "partials/timelines/discover/responses_most.html")]
566struct PartialTopRespondersTemplate {
567    lang: langbeam::LangFile,
568    users: Vec<(usize, Box<Profile>)>,
569}
570
571/// GET /_app/timelines/discover/responses_most.html
572pub async fn partial_top_responders_request(
573    jar: CookieJar,
574    State(database): State<Database>,
575) -> impl IntoResponse {
576    let users = match database.get_top_responders().await {
577        Ok(r) => r,
578        Err(e) => return Html(e.to_html(database)),
579    };
580
581    // ...
582    return Html(
583        PartialTopRespondersTemplate {
584            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
585                c.value_trimmed()
586            } else {
587                ""
588            }),
589            users,
590        }
591        .render()
592        .unwrap(),
593    );
594}
595
596#[derive(Template)]
597#[template(path = "timelines/discover.html")]
598struct DiscoverTemplate {
599    config: Config,
600    lang: langbeam::LangFile,
601    profile: Option<Box<Profile>>,
602    unread: usize,
603    notifs: usize,
604}
605
606/// GET /public
607pub async fn discover_request(
608    jar: CookieJar,
609    State(database): State<Database>,
610) -> impl IntoResponse {
611    let auth_user = match jar.get("__Secure-Token") {
612        Some(c) => match database
613            .auth
614            .get_profile_by_unhashed(c.value_trimmed())
615            .await
616        {
617            Ok(ua) => ua,
618            Err(_) => return Html(DatabaseError::NotAllowed.to_string()),
619        },
620        None => return Html(DatabaseError::NotAllowed.to_string()),
621    };
622
623    // timeline
624    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
625
626    let notifs = database
627        .auth
628        .get_notification_count_by_recipient(&auth_user.id)
629        .await;
630
631    // ...
632    return Html(
633        DiscoverTemplate {
634            config: database.config.clone(),
635            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
636                c.value_trimmed()
637            } else {
638                ""
639            }),
640            profile: Some(auth_user),
641            unread,
642            notifs,
643        }
644        .render()
645        .unwrap(),
646    );
647}
648
649#[derive(Template)]
650#[template(path = "general_markdown_text.html")]
651pub struct MarkdownTemplate {
652    config: Config,
653    lang: langbeam::LangFile,
654    profile: Option<Box<Profile>>,
655    title: String,
656    text: String,
657}
658
659/// GET /site/about
660pub async fn about_request(jar: CookieJar, State(database): State<Database>) -> impl IntoResponse {
661    let auth_user = match jar.get("__Secure-Token") {
662        Some(c) => match database
663            .auth
664            .get_profile_by_unhashed(c.value_trimmed())
665            .await
666        {
667            Ok(ua) => Some(ua),
668            Err(_) => None,
669        },
670        None => None,
671    };
672
673    Html(
674        MarkdownTemplate {
675            config: database.config.clone(),
676            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
677                c.value_trimmed()
678            } else {
679                ""
680            }),
681            profile: auth_user,
682            title: "About".to_string(),
683            text: rainbeam_shared::fs::read(format!(
684                "{}/site/about.md",
685                database.config.static_dir
686            ))
687            .unwrap_or(database.config.description),
688        }
689        .render()
690        .unwrap(),
691    )
692}
693
694/// GET /site/terms-of-service
695pub async fn tos_request(jar: CookieJar, State(database): State<Database>) -> impl IntoResponse {
696    let auth_user = match jar.get("__Secure-Token") {
697        Some(c) => match database
698            .auth
699            .get_profile_by_unhashed(c.value_trimmed())
700            .await
701        {
702            Ok(ua) => Some(ua),
703            Err(_) => None,
704        },
705        None => None,
706    };
707
708    Html(
709        MarkdownTemplate {
710            config: database.config.clone(),
711            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
712                c.value_trimmed()
713            } else {
714                ""
715            }),
716            profile: auth_user,
717            title: "Terms of Service".to_string(),
718            text: rainbeam_shared::fs::read(format!("{}/site/tos.md", database.config.static_dir))
719                .unwrap_or(String::new()),
720        }
721        .render()
722        .unwrap(),
723    )
724}
725
726/// GET /site/privacy
727pub async fn privacy_request(
728    jar: CookieJar,
729    State(database): State<Database>,
730) -> impl IntoResponse {
731    let auth_user = match jar.get("__Secure-Token") {
732        Some(c) => match database
733            .auth
734            .get_profile_by_unhashed(c.value_trimmed())
735            .await
736        {
737            Ok(ua) => Some(ua),
738            Err(_) => None,
739        },
740        None => None,
741    };
742
743    Html(
744        MarkdownTemplate {
745            config: database.config.clone(),
746            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
747                c.value_trimmed()
748            } else {
749                ""
750            }),
751            profile: auth_user,
752            title: "Privacy Policy".to_string(),
753            text: rainbeam_shared::fs::read(format!(
754                "{}/site/privacy.md",
755                database.config.static_dir
756            ))
757            .unwrap_or(String::new()),
758        }
759        .render()
760        .unwrap(),
761    )
762}
763
764#[derive(Template)]
765#[template(path = "fun/carp.html")]
766struct CarpTemplate {
767    config: Config,
768    lang: langbeam::LangFile,
769    profile: Option<Box<Profile>>,
770}
771
772/// GET /site/fun/carp
773pub async fn carp_request(jar: CookieJar, State(database): State<Database>) -> impl IntoResponse {
774    let auth_user = match jar.get("__Secure-Token") {
775        Some(c) => match database
776            .auth
777            .get_profile_by_unhashed(c.value_trimmed())
778            .await
779        {
780            Ok(ua) => Some(ua),
781            Err(_) => None,
782        },
783        None => None,
784    };
785
786    Html(
787        CarpTemplate {
788            config: database.config.clone(),
789            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
790                c.value_trimmed()
791            } else {
792                ""
793            }),
794            profile: auth_user,
795        }
796        .render()
797        .unwrap(),
798    )
799}
800
801#[derive(Template)]
802#[template(path = "auth/login.html")]
803struct LoginTemplate {
804    config: Config,
805    lang: langbeam::LangFile,
806    profile: Option<Box<Profile>>,
807}
808
809/// GET /login
810pub async fn login_request(jar: CookieJar, State(database): State<Database>) -> impl IntoResponse {
811    let auth_user = match jar.get("__Secure-Token") {
812        Some(c) => match database
813            .auth
814            .get_profile_by_unhashed(c.value_trimmed())
815            .await
816        {
817            Ok(ua) => Some(ua),
818            Err(_) => None,
819        },
820        None => None,
821    };
822
823    Html(
824        LoginTemplate {
825            config: database.config.clone(),
826            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
827                c.value_trimmed()
828            } else {
829                ""
830            }),
831            profile: auth_user,
832        }
833        .render()
834        .unwrap(),
835    )
836}
837
838#[derive(Template)]
839#[template(path = "auth/sign_up.html")]
840struct SignUpTemplate {
841    config: Config,
842    lang: langbeam::LangFile,
843    profile: Option<Box<Profile>>,
844}
845
846/// GET /sign_up
847pub async fn sign_up_request(
848    jar: CookieJar,
849    State(database): State<Database>,
850) -> impl IntoResponse {
851    if database.config.registration_enabled == false {
852        return Html(DatabaseError::NotAllowed.to_html(database));
853    }
854
855    // ...
856    let auth_user = match jar.get("__Secure-Token") {
857        Some(c) => match database
858            .auth
859            .get_profile_by_unhashed(c.value_trimmed())
860            .await
861        {
862            Ok(ua) => Some(ua),
863            Err(_) => None,
864        },
865        None => None,
866    };
867
868    Html(
869        SignUpTemplate {
870            config: database.config.clone(),
871            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
872                c.value_trimmed()
873            } else {
874                ""
875            }),
876            profile: auth_user,
877        }
878        .render()
879        .unwrap(),
880    )
881}
882
883#[derive(Serialize, Deserialize)]
884pub struct PaginatedQuery {
885    #[serde(default)]
886    pub page: i32,
887}
888
889#[derive(Serialize, Deserialize)]
890pub struct NotificationsQuery {
891    #[serde(default)]
892    page: i32,
893    #[serde(default)]
894    profile: String,
895}
896
897#[derive(Serialize, Deserialize)]
898pub struct SearchQuery {
899    #[serde(default)]
900    page: i32,
901    #[serde(default)]
902    q: String,
903    #[serde(default)]
904    tag: String,
905}
906
907#[derive(Serialize, Deserialize)]
908pub struct MarketQuery {
909    #[serde(default)]
910    page: i32,
911    #[serde(default)]
912    q: String,
913    #[serde(default)]
914    status: ItemStatus,
915    #[serde(default)]
916    creator: String,
917    #[serde(default)]
918    customer: String,
919    #[serde(default)]
920    r#type: Option<ItemType>,
921}
922
923#[derive(Serialize, Deserialize)]
924pub struct SearchHomeQuery {
925    #[serde(default)]
926    driver: i8,
927}
928
929#[derive(Serialize, Deserialize)]
930pub struct ProfileQuery {
931    #[serde(default)]
932    pub page: i32,
933    pub tag: Option<String>,
934    pub q: Option<String>,
935    #[serde(default)]
936    pub password: String,
937}
938
939#[derive(Deserialize)]
940pub struct PasswordQuery {
941    #[serde(default)]
942    pub password: String,
943}
944
945/// Escape profile colors
946pub fn color_escape(color: &&&String) -> String {
947    remove_tags(
948        &color
949            .replace(";", "")
950            .replace("<", "&lt;")
951            .replace(">", "%gt;")
952            .replace("}", "")
953            .replace("{", "")
954            .replace("url(\"", "url(\"/api/v0/util/ext/image?img=")
955            .replace("url('", "url('/api/v0/util/ext/image?img=")
956            .replace("url(https://", "url(/api/v0/util/ext/image?img=https://"),
957    )
958}
959
960/// Clean profile metadata
961pub fn remove_tags(input: &str) -> String {
962    Builder::default()
963        .rm_tags(&["img", "a", "span", "p", "h1", "h2", "h3", "h4", "h5", "h6"])
964        .clean(input)
965        .to_string()
966        .replace("&lt;", "<")
967        .replace("&gt;", ">")
968        .replace("&amp;", "&")
969        .replace("</script>", "</not-script")
970}
971
972/// Clean profile metadata
973pub fn clean_metadata(metadata: &ProfileMetadata) -> String {
974    remove_tags(&serde_json::to_string(&clean_metadata_raw(metadata)).unwrap())
975}
976
977/// Clean profile metadata
978pub fn clean_metadata_raw(metadata: &ProfileMetadata) -> ProfileMetadata {
979    // remove stupid characters
980    let mut metadata = metadata.to_owned();
981
982    for field in metadata.kv.clone() {
983        metadata.kv.insert(
984            field.0.to_string(),
985            field
986                .1
987                .replace("<", "&lt;")
988                .replace(">", "&gt;")
989                .replace("url(\"", "url(\"/api/v0/util/ext/image?img=")
990                .replace("url(https://", "url(/api/v0/util/ext/image?img=https://")
991                .replace("<style>", "")
992                .replace("</style>", ""),
993        );
994    }
995
996    // ...
997    metadata
998}
999
1000/// Clean profile metadata short
1001pub fn clean_metadata_short(metadata: &ProfileMetadata) -> String {
1002    remove_tags(&serde_json::to_string(&clean_metadata_short_raw(metadata)).unwrap())
1003        .replace("\u{200d}", "")
1004        // how do you end up with these in your settings?
1005        .replace("\u{0010}", "")
1006        .replace("\u{0011}", "")
1007        .replace("\u{0012}", "")
1008        .replace("\u{0013}", "")
1009        .replace("\u{0014}", "")
1010}
1011
1012/// Clean profile metadata short row
1013pub fn clean_metadata_short_raw(metadata: &ProfileMetadata) -> ProfileMetadata {
1014    // remove stupid characters
1015    let mut metadata = metadata.to_owned();
1016
1017    for field in metadata.kv.clone() {
1018        metadata.kv.insert(
1019            field.0.to_string(),
1020            field
1021                .1
1022                .replace("<", "&lt;")
1023                .replace(">", "&gt;")
1024                .replace("<style>", "")
1025                .replace("</style>", ""),
1026        );
1027    }
1028
1029    // ...
1030    metadata
1031}
1032
1033#[derive(Template)]
1034#[template(path = "views/question.html")]
1035struct QuestionTemplate {
1036    config: Config,
1037    lang: langbeam::LangFile,
1038    profile: Option<Box<Profile>>,
1039    unread: usize,
1040    notifs: usize,
1041    question: Question,
1042    responses: Vec<FullResponse>,
1043    reactions: Vec<Reaction>,
1044    already_responded: bool,
1045    is_powerful: bool,
1046    is_helper: bool,
1047}
1048
1049/// GET /@{}/q/{id}
1050pub async fn question_request(
1051    jar: CookieJar,
1052    Path((_, id)): Path<(String, String)>,
1053    State(database): State<Database>,
1054) -> impl IntoResponse {
1055    let auth_user = match jar.get("__Secure-Token") {
1056        Some(c) => match database
1057            .auth
1058            .get_profile_by_unhashed(c.value_trimmed())
1059            .await
1060        {
1061            Ok(ua) => Some(ua),
1062            Err(_) => None,
1063        },
1064        None => None,
1065    };
1066
1067    let unread = if let Some(ref ua) = auth_user {
1068        database.get_inbox_count_by_recipient(&ua.id).await
1069    } else {
1070        0
1071    };
1072
1073    let notifs = if let Some(ref ua) = auth_user {
1074        database
1075            .auth
1076            .get_notification_count_by_recipient(&ua.id)
1077            .await
1078    } else {
1079        0
1080    };
1081
1082    let question = match database.get_question(id.clone()).await {
1083        Ok(ua) => ua,
1084        Err(e) => return Html(e.to_html(database)),
1085    };
1086
1087    let responses = match database.get_responses_by_question(id.to_owned()).await {
1088        Ok(responses) => responses,
1089        Err(_) => return Html(DatabaseError::Other.to_html(database)),
1090    };
1091
1092    let mut is_helper: bool = false;
1093    let is_powerful = if let Some(ref ua) = auth_user {
1094        let group = match database.auth.get_group_by_id(ua.group).await {
1095            Ok(g) => g,
1096            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1097        };
1098
1099        is_helper = group.permissions.check_helper();
1100        group.permissions.check_manager()
1101    } else {
1102        false
1103    };
1104
1105    let reactions = match database.get_reactions_by_asset(id.clone()).await {
1106        Ok(r) => r,
1107        Err(e) => return Html(e.to_html(database)),
1108    };
1109
1110    // ...
1111    Html(
1112        QuestionTemplate {
1113            config: database.config.clone(),
1114            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1115                c.value_trimmed()
1116            } else {
1117                ""
1118            }),
1119            already_responded: if let Some(ref ua) = auth_user {
1120                database
1121                    .get_response_by_question_and_author(&id, &ua.id)
1122                    .await
1123                    .is_ok()
1124            } else {
1125                false
1126            },
1127            profile: auth_user,
1128            unread,
1129            notifs,
1130            question,
1131            responses,
1132            is_powerful,
1133            is_helper,
1134            reactions,
1135        }
1136        .render()
1137        .unwrap(),
1138    )
1139}
1140
1141#[derive(Template)]
1142#[template(path = "inbox.html")]
1143struct InboxTemplate {
1144    config: Config,
1145    lang: langbeam::LangFile,
1146    profile: Option<Box<Profile>>,
1147    unread: Vec<Question>,
1148    notifs: usize,
1149    anonymous_username: Option<String>,
1150    anonymous_avatar: Option<String>,
1151    is_helper: bool,
1152}
1153
1154#[derive(Template)]
1155#[template(path = "partials/views/reactions.html")]
1156struct PartialReactionsTemplate {
1157    reactions: Vec<Reaction>,
1158}
1159
1160#[derive(Deserialize)]
1161pub struct PartialReactionsProps {
1162    pub id: String,
1163}
1164
1165/// GET /_app/components/short_reactions.html
1166pub async fn partial_reactions_request(
1167    State(database): State<Database>,
1168    Query(props): Query<PartialReactionsProps>,
1169) -> impl IntoResponse {
1170    Html(
1171        PartialReactionsTemplate {
1172            reactions: match database.get_reactions_by_asset(props.id).await {
1173                Ok(r) => r,
1174                Err(e) => return Html(e.to_string()),
1175            },
1176        }
1177        .render()
1178        .unwrap(),
1179    )
1180}
1181
1182/// GET /inbox
1183pub async fn inbox_request(jar: CookieJar, State(database): State<Database>) -> impl IntoResponse {
1184    let auth_user = match jar.get("__Secure-Token") {
1185        Some(c) => match database
1186            .auth
1187            .get_profile_by_unhashed(c.value_trimmed())
1188            .await
1189        {
1190            Ok(ua) => ua,
1191            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1192        },
1193        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1194    };
1195
1196    // mark all as read
1197    if !auth_user
1198        .metadata
1199        .is_true("rainbeam:do_not_clear_inbox_count_on_view")
1200    {
1201        simplify!(
1202            database
1203                .auth
1204                .update_profile_inbox_count(&auth_user.id, 0)
1205                .await;
1206            Err; Html(DatabaseError::Other.to_html(database))
1207        );
1208    }
1209
1210    // ...
1211    let unread = match database.get_questions_by_recipient(&auth_user.id).await {
1212        Ok(unread) => unread,
1213        Err(_) => return Html(DatabaseError::Other.to_html(database)),
1214    };
1215
1216    let notifs = database
1217        .auth
1218        .get_notification_count_by_recipient(&auth_user.id)
1219        .await;
1220
1221    let is_helper: bool = {
1222        let group = match database.auth.get_group_by_id(auth_user.group).await {
1223            Ok(g) => g,
1224            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1225        };
1226
1227        group.permissions.check_helper()
1228    };
1229
1230    Html(
1231        InboxTemplate {
1232            config: database.config.clone(),
1233            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1234                c.value_trimmed()
1235            } else {
1236                ""
1237            }),
1238            unread,
1239            notifs,
1240            anonymous_username: Some(
1241                auth_user
1242                    .metadata
1243                    .kv
1244                    .get("sparkler:anonymous_username")
1245                    .unwrap_or(&"anonymous".to_string())
1246                    .to_string(),
1247            ),
1248            anonymous_avatar: Some(
1249                auth_user
1250                    .metadata
1251                    .kv
1252                    .get("sparkler:anonymous_avatar")
1253                    .unwrap_or(&"/static/images/default-avatar.svg".to_string())
1254                    .to_string(),
1255            ),
1256            profile: Some(auth_user),
1257            is_helper,
1258        }
1259        .render()
1260        .unwrap(),
1261    )
1262}
1263
1264#[derive(Template)]
1265#[template(path = "timelines/global_question_timeline.html")]
1266struct GlobalTimelineTemplate {
1267    config: Config,
1268    lang: langbeam::LangFile,
1269    profile: Option<Box<Profile>>,
1270    unread: usize,
1271    notifs: usize,
1272    questions: Vec<(Question, usize, usize)>,
1273    relationships: HashMap<String, RelationshipStatus>,
1274    is_helper: bool,
1275    page: i32,
1276}
1277
1278/// GET /inbox/global/following
1279pub async fn global_timeline_request(
1280    jar: CookieJar,
1281    State(database): State<Database>,
1282    Query(query): Query<PaginatedQuery>,
1283) -> impl IntoResponse {
1284    let auth_user = match jar.get("__Secure-Token") {
1285        Some(c) => match database
1286            .auth
1287            .get_profile_by_unhashed(c.value_trimmed())
1288            .await
1289        {
1290            Ok(ua) => ua,
1291            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1292        },
1293        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1294    };
1295
1296    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
1297
1298    let notifs = database
1299        .auth
1300        .get_notification_count_by_recipient(&auth_user.id)
1301        .await;
1302
1303    let questions = match database
1304        .get_global_questions_by_following_paginated(&auth_user.id, query.page)
1305        .await
1306    {
1307        Ok(r) => r,
1308        Err(e) => return Html(e.to_html(database)),
1309    };
1310
1311    let is_helper = {
1312        let group = match database.auth.get_group_by_id(auth_user.group).await {
1313            Ok(g) => g,
1314            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1315        };
1316
1317        group.permissions.check_helper()
1318    };
1319
1320    // build relationships list
1321    let mut relationships: HashMap<String, RelationshipStatus> = HashMap::new();
1322
1323    for question in &questions {
1324        if relationships.contains_key(&question.0.author.id) {
1325            continue;
1326        }
1327
1328        if is_helper {
1329            // make sure staff can view your questions
1330            relationships.insert(question.0.author.id.clone(), RelationshipStatus::Friends);
1331            continue;
1332        }
1333
1334        if question.0.author.id == auth_user.id {
1335            // make sure we can view our own responses
1336            relationships.insert(question.0.author.id.clone(), RelationshipStatus::Friends);
1337            continue;
1338        };
1339
1340        relationships.insert(
1341            question.0.author.id.clone(),
1342            database
1343                .auth
1344                .get_user_relationship(&question.0.author.id, &auth_user.id)
1345                .await
1346                .0,
1347        );
1348    }
1349
1350    // ...
1351    Html(
1352        GlobalTimelineTemplate {
1353            config: database.config.clone(),
1354            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1355                c.value_trimmed()
1356            } else {
1357                ""
1358            }),
1359            profile: Some(auth_user),
1360            unread,
1361            notifs,
1362            questions,
1363            relationships,
1364            is_helper,
1365            page: query.page,
1366        }
1367        .render()
1368        .unwrap(),
1369    )
1370}
1371
1372#[derive(Template)]
1373#[template(path = "timelines/public_global_question_timeline.html")]
1374struct PublicGlobalTimelineTemplate {
1375    config: Config,
1376    lang: langbeam::LangFile,
1377    profile: Option<Box<Profile>>,
1378    unread: usize,
1379    notifs: usize,
1380    questions: Vec<(Question, usize, usize)>,
1381    relationships: HashMap<String, RelationshipStatus>,
1382    is_helper: bool,
1383    page: i32,
1384}
1385
1386/// GET /inbox/global
1387pub async fn public_global_timeline_request(
1388    jar: CookieJar,
1389    State(database): State<Database>,
1390    Query(query): Query<PaginatedQuery>,
1391) -> impl IntoResponse {
1392    let auth_user = match jar.get("__Secure-Token") {
1393        Some(c) => match database
1394            .auth
1395            .get_profile_by_unhashed(c.value_trimmed())
1396            .await
1397        {
1398            Ok(ua) => ua,
1399            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1400        },
1401        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1402    };
1403
1404    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
1405
1406    let notifs = database
1407        .auth
1408        .get_notification_count_by_recipient(&auth_user.id)
1409        .await;
1410
1411    let is_helper = {
1412        let group = match database.auth.get_group_by_id(auth_user.group).await {
1413            Ok(g) => g,
1414            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1415        };
1416
1417        group.permissions.check_helper()
1418    };
1419
1420    let questions = match database.get_global_questions_paginated(query.page).await {
1421        Ok(r) => r,
1422        Err(e) => return Html(e.to_html(database)),
1423    };
1424
1425    // build relationships list
1426    let mut relationships: HashMap<String, RelationshipStatus> = HashMap::new();
1427
1428    for question in &questions {
1429        if relationships.contains_key(&question.0.author.id) {
1430            continue;
1431        }
1432
1433        if is_helper {
1434            // make sure staff can view your questions
1435            relationships.insert(question.0.author.id.clone(), RelationshipStatus::Friends);
1436            continue;
1437        }
1438
1439        if question.0.author.id == auth_user.id {
1440            // make sure we can view our own responses
1441            relationships.insert(question.0.author.id.clone(), RelationshipStatus::Friends);
1442            continue;
1443        };
1444
1445        relationships.insert(
1446            question.0.author.id.clone(),
1447            database
1448                .auth
1449                .get_user_relationship(&question.0.author.id, &auth_user.id)
1450                .await
1451                .0,
1452        );
1453    }
1454
1455    // ...
1456    let is_helper = {
1457        let group = match database.auth.get_group_by_id(auth_user.group).await {
1458            Ok(g) => g,
1459            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1460        };
1461
1462        group.permissions.check_helper()
1463    };
1464
1465    Html(
1466        PublicGlobalTimelineTemplate {
1467            config: database.config.clone(),
1468            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1469                c.value_trimmed()
1470            } else {
1471                ""
1472            }),
1473            profile: Some(auth_user),
1474            unread,
1475            notifs,
1476            questions,
1477            relationships,
1478            is_helper,
1479            page: query.page,
1480        }
1481        .render()
1482        .unwrap(),
1483    )
1484}
1485
1486#[derive(Template)]
1487#[template(path = "notifications.html")]
1488struct NotificationsTemplate {
1489    config: Config,
1490    lang: langbeam::LangFile,
1491    profile: Option<Box<Profile>>,
1492    unread: usize,
1493    notifs: Vec<Notification>,
1494    page: i32,
1495    pid: String,
1496}
1497
1498/// GET /inbox/notifications
1499pub async fn notifications_request(
1500    jar: CookieJar,
1501    State(database): State<Database>,
1502    Query(props): Query<NotificationsQuery>,
1503) -> impl IntoResponse {
1504    let auth_user = match jar.get("__Secure-Token") {
1505        Some(c) => match database
1506            .auth
1507            .get_profile_by_unhashed(c.value_trimmed())
1508            .await
1509        {
1510            Ok(ua) => ua,
1511            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1512        },
1513        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1514    };
1515
1516    // mark all as read
1517    simplify!(
1518        database
1519            .auth
1520            .update_profile_notification_count(&auth_user.id, 0)
1521            .await;
1522        Err; Html(DatabaseError::Other.to_html(database))
1523    );
1524
1525    // ...
1526    let is_helper = {
1527        let group = match database.auth.get_group_by_id(auth_user.group).await {
1528            Ok(g) => g,
1529            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1530        };
1531
1532        group.permissions.check_helper()
1533    };
1534
1535    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
1536
1537    let pid = if is_helper && !props.profile.is_empty() {
1538        // use the given profile value if we gave one and we are a helper
1539        &props.profile
1540    } else {
1541        // otherwise, use the current user
1542        &auth_user.id
1543    };
1544
1545    let notifs = match database
1546        .auth
1547        .get_notifications_by_recipient_paginated(&pid, props.page)
1548        .await
1549    {
1550        Ok(r) => r,
1551        Err(_) => return Html(DatabaseError::Other.to_html(database)),
1552    };
1553
1554    Html(
1555        NotificationsTemplate {
1556            config: database.config.clone(),
1557            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1558                c.value_trimmed()
1559            } else {
1560                ""
1561            }),
1562            pid: pid.to_string(),
1563            profile: Some(auth_user),
1564            unread,
1565            notifs,
1566            page: props.page,
1567        }
1568        .render()
1569        .unwrap(),
1570    )
1571}
1572
1573#[derive(Template)]
1574#[template(path = "reports.html")]
1575struct ReportsTemplate {
1576    config: Config,
1577    lang: langbeam::LangFile,
1578    profile: Option<Box<Profile>>,
1579    unread: usize,
1580    reports: Vec<Notification>,
1581}
1582
1583/// GET /inbox/reports
1584pub async fn reports_request(
1585    jar: CookieJar,
1586    State(database): State<Database>,
1587) -> impl IntoResponse {
1588    let auth_user = match jar.get("__Secure-Token") {
1589        Some(c) => match database
1590            .auth
1591            .get_profile_by_unhashed(c.value_trimmed())
1592            .await
1593        {
1594            Ok(ua) => ua,
1595            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1596        },
1597        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1598    };
1599
1600    // check permission
1601    let group = match database.auth.get_group_by_id(auth_user.group).await {
1602        Ok(g) => g,
1603        Err(_) => return Html(DatabaseError::NotFound.to_html(database)),
1604    };
1605
1606    if !group.permissions.check(FinePermission::VIEW_REPORTS) {
1607        // we must be a manager to do this
1608        return Html(DatabaseError::NotAllowed.to_html(database));
1609    }
1610
1611    // ...
1612    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
1613
1614    let reports = match database.auth.get_notifications_by_recipient("*").await {
1615        Ok(r) => r,
1616        Err(_) => return Html(DatabaseError::Other.to_html(database)),
1617    };
1618
1619    Html(
1620        ReportsTemplate {
1621            config: database.config.clone(),
1622            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1623                c.value_trimmed()
1624            } else {
1625                ""
1626            }),
1627            profile: Some(auth_user),
1628            unread,
1629            reports,
1630        }
1631        .render()
1632        .unwrap(),
1633    )
1634}
1635
1636#[derive(Template)]
1637#[template(path = "audit.html")]
1638struct AuditTemplate {
1639    config: Config,
1640    lang: langbeam::LangFile,
1641    profile: Option<Box<Profile>>,
1642    unread: usize,
1643    logs: Vec<Notification>,
1644    page: i32,
1645}
1646
1647/// GET /inbox/audit
1648pub async fn audit_log_request(
1649    jar: CookieJar,
1650    State(database): State<Database>,
1651    Query(props): Query<PaginatedQuery>,
1652) -> impl IntoResponse {
1653    let auth_user = match jar.get("__Secure-Token") {
1654        Some(c) => match database
1655            .auth
1656            .get_profile_by_unhashed(c.value_trimmed())
1657            .await
1658        {
1659            Ok(ua) => ua,
1660            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1661        },
1662        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1663    };
1664
1665    // check permission
1666    let group = match database.auth.get_group_by_id(auth_user.group).await {
1667        Ok(g) => g,
1668        Err(_) => return Html(DatabaseError::NotFound.to_html(database)),
1669    };
1670
1671    if !group.permissions.check(FinePermission::VIEW_AUDIT_LOG) {
1672        // we must be a manager to do this
1673        return Html(DatabaseError::NotAllowed.to_html(database));
1674    }
1675
1676    // ...
1677    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
1678
1679    let logs = match database
1680        .auth
1681        .get_notifications_by_recipient_paginated("*(audit)", props.page)
1682        .await
1683    {
1684        Ok(r) => r,
1685        Err(_) => return Html(DatabaseError::Other.to_html(database)),
1686    };
1687
1688    Html(
1689        AuditTemplate {
1690            config: database.config.clone(),
1691            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1692                c.value_trimmed()
1693            } else {
1694                ""
1695            }),
1696            profile: Some(auth_user),
1697            unread,
1698            logs,
1699            page: props.page,
1700        }
1701        .render()
1702        .unwrap(),
1703    )
1704}
1705
1706#[derive(Template)]
1707#[template(path = "ipbans.html")]
1708struct IpbansTemplate {
1709    config: Config,
1710    lang: langbeam::LangFile,
1711    profile: Option<Box<Profile>>,
1712    unread: usize,
1713    bans: Vec<IpBan>,
1714}
1715
1716/// GET /inbox/audit/ipbans
1717pub async fn ipbans_request(jar: CookieJar, State(database): State<Database>) -> impl IntoResponse {
1718    let auth_user = match jar.get("__Secure-Token") {
1719        Some(c) => match database
1720            .auth
1721            .get_profile_by_unhashed(c.value_trimmed())
1722            .await
1723        {
1724            Ok(ua) => ua,
1725            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1726        },
1727        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1728    };
1729
1730    // check permission
1731    let group = match database.auth.get_group_by_id(auth_user.group).await {
1732        Ok(g) => g,
1733        Err(_) => return Html(DatabaseError::NotFound.to_html(database)),
1734    };
1735
1736    if !group.permissions.check(FinePermission::BAN_IP) {
1737        // we must be a manager to do this
1738        return Html(DatabaseError::NotAllowed.to_html(database));
1739    }
1740
1741    // ...
1742    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
1743
1744    let bans = match database.auth.get_ipbans(auth_user.clone()).await {
1745        Ok(r) => r,
1746        Err(_) => return Html(DatabaseError::Other.to_html(database)),
1747    };
1748
1749    Html(
1750        IpbansTemplate {
1751            config: database.config.clone(),
1752            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1753                c.value_trimmed()
1754            } else {
1755                ""
1756            }),
1757            profile: Some(auth_user),
1758            unread,
1759            bans,
1760        }
1761        .render()
1762        .unwrap(),
1763    )
1764}
1765
1766#[derive(Template)]
1767#[template(path = "intents/report.html")]
1768struct ReportTemplate {
1769    config: Config,
1770    lang: langbeam::LangFile,
1771    profile: Option<Box<Profile>>,
1772}
1773
1774/// GET /intents/report
1775pub async fn report_request(jar: CookieJar, State(database): State<Database>) -> impl IntoResponse {
1776    let auth_user = match jar.get("__Secure-Token") {
1777        Some(c) => match database
1778            .auth
1779            .get_profile_by_unhashed(c.value_trimmed())
1780            .await
1781        {
1782            Ok(ua) => Some(ua),
1783            Err(_) => None,
1784        },
1785        None => None,
1786    };
1787
1788    Html(
1789        ReportTemplate {
1790            config: database.config.clone(),
1791            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1792                c.value_trimmed()
1793            } else {
1794                ""
1795            }),
1796            profile: auth_user,
1797        }
1798        .render()
1799        .unwrap(),
1800    )
1801}
1802
1803// ...
1804pub async fn routes(database: Database) -> Router {
1805    Router::new()
1806        .route("/", get(homepage_request))
1807        .route("/public", get(public_timeline_request))
1808        .route("/discover", get(discover_request))
1809        .route("/site/about", get(about_request))
1810        .route("/site/terms-of-service", get(tos_request))
1811        .route("/site/privacy", get(privacy_request))
1812        .route("/intents/report", get(report_request))
1813        .route("/site/fun/carp", get(carp_request))
1814        // inbox
1815        .route("/inbox", get(inbox_request))
1816        .route("/inbox/global", get(public_global_timeline_request))
1817        .route("/inbox/global/following", get(global_timeline_request))
1818        .route("/inbox/notifications", get(notifications_request))
1819        .route("/inbox/reports", get(reports_request)) // staff
1820        .route("/inbox/audit", get(audit_log_request)) // staff
1821        .route("/inbox/audit/ipbans", get(ipbans_request)) // staff
1822        // assets
1823        .route("/@{username}/q/{id}", get(question_request))
1824        .route(
1825            "/@{username}/r/{id}",
1826            get(models::response::response_request),
1827        )
1828        .route("/@{username}/c/{id}", get(models::comment::comment_request))
1829        // profiles
1830        .route("/@{username}/_app/warning", get(profile::warning_request))
1831        .route("/@{username}/mod", get(profile::mod_request)) // staff
1832        .route("/@{username}/questions", get(profile::questions_request))
1833        .route("/@{username}/questions/inbox", get(profile::inbox_request)) // staff
1834        .route(
1835            "/@{username}/questions/outbox",
1836            get(profile::outbox_request),
1837        ) // staff
1838        .route("/@{username}/following", get(profile::following_request))
1839        .route("/@{username}/followers", get(profile::followers_request))
1840        .route("/@{username}/friends", get(profile::friends_request))
1841        .route(
1842            "/@{username}/friends/requests",
1843            get(profile::friend_requests_request),
1844        )
1845        .route("/@{username}/friends/blocks", get(profile::blocks_request))
1846        .route("/@{username}/embed", get(profile::profile_embed_request))
1847        .route(
1848            "/@{username}/relationship/friend_accept",
1849            get(profile::friend_request),
1850        )
1851        .route(
1852            "/@{username}/_app/card.html",
1853            get(profile::render_card_request),
1854        )
1855        .route(
1856            "/@{username}/_app/feed.html",
1857            get(profile::partial_profile_request),
1858        )
1859        .route(
1860            "/@{username}/layout",
1861            get(profile::profile_layout_editor_request),
1862        )
1863        .route("/@{username}", get(profile::profile_request))
1864        .route("/{id}", get(api::profiles::expand_request))
1865        // settings
1866        .route("/settings", get(settings::account_settings))
1867        .route("/settings/sessions", get(settings::sessions_settings))
1868        .route("/settings/profile", get(settings::profile_settings))
1869        .route("/settings/theme", get(settings::theme_settings))
1870        .route("/settings/privacy", get(settings::privacy_settings))
1871        .route("/settings/coins", get(settings::coins_settings))
1872        // search
1873        .route("/search", get(search::search_homepage_request))
1874        .route("/search/responses", get(search::search_responses_request))
1875        .route("/search/questions", get(search::search_questions_request))
1876        .route("/search/users", get(search::search_users_request))
1877        // market
1878        .route("/market", get(market::homepage_request))
1879        .route("/market/new", get(market::create_request))
1880        .route("/market/item/{id}", get(market::item_request))
1881        .route(
1882            "/market/_app/theme_playground.html/{id}",
1883            get(market::theme_playground_request),
1884        )
1885        .route(
1886            "/market/_app/layout_playground.html/{id}",
1887            get(market::layout_playground_request),
1888        )
1889        // auth
1890        .route("/login", get(login_request))
1891        .route("/sign_up", get(sign_up_request))
1892        // expanders
1893        .route("/+q/{id}", get(api::questions::expand_request))
1894        .route("/question/{id}", get(api::questions::expand_request))
1895        .route("/+r/{id}", get(api::responses::expand_request))
1896        .route("/response/{id}", get(api::responses::expand_request))
1897        .route("/+c/{id}", get(api::comments::expand_request))
1898        .route("/comment/{id}", get(api::comments::expand_request))
1899        .route("/+u/{id}", get(api::profiles::expand_request))
1900        .route("/+i/{ip}", get(api::profiles::expand_ip_request))
1901        // partials
1902        .route(
1903            "/_app/components/comments.html",
1904            get(models::comment::partial_comments_request),
1905        )
1906        .route(
1907            "/_app/components/response_comments.html",
1908            get(models::comment::partial_response_comments_request),
1909        )
1910        .route(
1911            "/_app/components/response.html",
1912            get(models::response::partial_response_request),
1913        )
1914        .route(
1915            "/_app/components/short_reactions.html",
1916            get(partial_reactions_request),
1917        )
1918        .route(
1919            "/_app/timelines/timeline.html",
1920            get(partial_timeline_request),
1921        )
1922        .route(
1923            "/_app/timelines/public_timeline.html",
1924            get(partial_public_timeline_request),
1925        )
1926        .route(
1927            "/_app/timelines/discover/responses_top.html",
1928            get(partial_top_responses_request),
1929        )
1930        .route(
1931            "/_app/timelines/discover/questions_most.html",
1932            get(partial_top_askers_request),
1933        )
1934        .route(
1935            "/_app/timelines/discover/responses_most.html",
1936            get(partial_top_responders_request),
1937        )
1938        // ...
1939        .with_state(database)
1940}