rb/routing/pages/
profile.rs

1use std::collections::HashMap;
2
3use authbeam::layout::LayoutComponent;
4use reva_axum::Template;
5use axum::extract::{Path, Query};
6use axum::response::IntoResponse;
7use axum::{extract::State, response::Html};
8use axum_extra::extract::CookieJar;
9
10use authbeam::model::{FinePermission, ItemType, Profile, UserFollow, Warning};
11use serde::Deserialize;
12
13use crate::config::Config;
14use crate::database::Database;
15use crate::model::{DatabaseError, FullResponse, Question, RelationshipStatus};
16use crate::ToHtml;
17
18use super::{clean_metadata, MarkdownTemplate, PaginatedQuery, PasswordQuery, ProfileQuery};
19
20#[derive(Template)]
21#[template(path = "profile/profile.html")]
22struct ProfileTemplate {
23    config: Config,
24    lang: langbeam::LangFile,
25    profile: Option<Box<Profile>>,
26    unread: usize,
27    notifs: usize,
28    other: Box<Profile>,
29    response_count: usize,
30    questions_count: usize,
31    followers_count: usize,
32    following_count: usize,
33    friends_count: usize,
34    is_following: bool,
35    is_following_you: bool,
36    metadata: String,
37    pinned: Option<Vec<FullResponse>>,
38    page: i32,
39    tag: String,
40    query: String,
41    // ...
42    relationship: RelationshipStatus,
43    lock_profile: bool,
44    disallow_anonymous: bool,
45    require_account: bool,
46    hide_social: bool,
47    view_password: String,
48    unlocked: bool,
49    is_powerful: bool, // at least "manager"
50    is_helper: bool,   // at least "helper"
51    is_self: bool,
52}
53
54/// GET /@{username}
55pub async fn profile_request(
56    jar: CookieJar,
57    Path(username): Path<String>,
58    State(database): State<Database>,
59    Query(query): Query<ProfileQuery>,
60) -> impl IntoResponse {
61    let auth_user = match jar.get("__Secure-Token") {
62        Some(c) => match database
63            .auth
64            .get_profile_by_unhashed(c.value_trimmed())
65            .await
66        {
67            Ok(ua) => Some(ua),
68            Err(_) => None,
69        },
70        None => None,
71    };
72
73    let unread = if let Some(ref ua) = auth_user {
74        database.get_inbox_count_by_recipient(&ua.id).await
75    } else {
76        0
77    };
78
79    let notifs = if let Some(ref ua) = auth_user {
80        database
81            .auth
82            .get_notification_count_by_recipient(&ua.id)
83            .await
84    } else {
85        0
86    };
87
88    let other = match database.auth.get_profile(&username).await {
89        Ok(ua) => ua,
90        Err(_) => return Html(DatabaseError::NotFound.to_html(database)),
91    };
92
93    if other.metadata.is_true("rainbeam:authenticated_only") & auth_user.is_none() {
94        // this profile only allows authenticated users to view their profile
95        return Html(DatabaseError::NotAllowed.to_html(database));
96    }
97
98    if other.id == "0" {
99        return Html(
100            MarkdownTemplate {
101                config: database.config.clone(),
102                lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
103                    c.value_trimmed()
104                } else {
105                    ""
106                }),
107                profile: auth_user,
108                title: "System".to_string(),
109                text: "Reserved system profile.".to_string(),
110            }
111            .render()
112            .unwrap(),
113        );
114    } else if other.id == "@" {
115        return Html(
116            MarkdownTemplate {
117                config: database.config.clone(),
118                lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
119                    c.value_trimmed()
120                } else {
121                    ""
122                }),
123                profile: auth_user,
124                title: "Everybody".to_string(),
125                text: "Hello from everyone!".to_string(),
126            }
127            .render()
128            .unwrap(),
129        );
130    }
131
132    let is_following = if let Some(ref ua) = auth_user {
133        match database.auth.get_follow(&ua.id, &other.id).await {
134            Ok(_) => true,
135            Err(_) => false,
136        }
137    } else {
138        false
139    };
140
141    let is_following_you = if let Some(ref ua) = auth_user {
142        match database.auth.get_follow(&other.id, &ua.id).await {
143            Ok(_) => true,
144            Err(_) => false,
145        }
146    } else {
147        false
148    };
149
150    // ...
151    let pinned = if let Some(pinned) = other.metadata.kv.get("sparkler:pinned") {
152        if pinned.is_empty() {
153            None
154        } else {
155            let mut out = Vec::new();
156
157            for id in pinned.split(",") {
158                match database.get_response(id.to_string()).await {
159                    Ok(response) => {
160                        if response.1.author.id != other.id {
161                            // don't allow us to pin responses from other users
162                            continue;
163                        }
164
165                        // push
166                        out.push(response)
167                    }
168                    Err(_) => continue,
169                }
170            }
171
172            Some(out)
173        }
174    } else {
175        None
176    };
177
178    let mut is_helper: bool = false;
179    let is_powerful = if let Some(ref ua) = auth_user {
180        let group = match database.auth.get_group_by_id(ua.group).await {
181            Ok(g) => g,
182            Err(_) => return Html(DatabaseError::Other.to_html(database)),
183        };
184
185        is_helper = group.permissions.check_helper();
186        group.permissions.check_manager()
187    } else {
188        false
189    };
190
191    let is_self = if let Some(ref profile) = auth_user {
192        profile.id == other.id
193    } else {
194        false
195    };
196
197    let relationship = if is_self | is_helper {
198        // we're always friends with ourselves! (and staff)
199        // allows user to bypass their own locked profile
200        RelationshipStatus::Friends
201    } else {
202        if let Some(ref profile) = auth_user {
203            database
204                .auth
205                .get_user_relationship(&other.id, &profile.id)
206                .await
207                .0
208        } else {
209            RelationshipStatus::Unknown
210        }
211    };
212
213    let is_blocked = relationship == RelationshipStatus::Blocked;
214
215    if !is_helper && is_blocked {
216        return Html(DatabaseError::NotFound.to_html(database));
217    }
218
219    // ...
220    Html(
221        ProfileTemplate {
222            config: database.config.clone(),
223            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
224                c.value_trimmed()
225            } else {
226                ""
227            }),
228            profile: auth_user.clone(),
229            unread,
230            notifs,
231            other: other.clone(),
232            response_count: database.get_response_count_by_author(&other.id).await,
233            questions_count: database
234                .get_global_questions_count_by_author(&other.id)
235                .await,
236            followers_count: database.auth.get_followers_count(&other.id).await,
237            following_count: database.auth.get_following_count(&other.id).await,
238            friends_count: database.auth.get_friendship_count_by_user(&other.id).await,
239            is_following,
240            is_following_you,
241            metadata: clean_metadata(&other.metadata),
242            pinned,
243            page: query.page,
244            tag: query.tag.unwrap_or(String::new()),
245            query: query.q.unwrap_or(String::new()),
246            // ...
247            relationship,
248            lock_profile: other
249                .metadata
250                .kv
251                .get("sparkler:lock_profile")
252                .unwrap_or(&"false".to_string())
253                == "true",
254            disallow_anonymous: other
255                .metadata
256                .kv
257                .get("sparkler:disallow_anonymous")
258                .unwrap_or(&"false".to_string())
259                == "true",
260            require_account: other
261                .metadata
262                .kv
263                .get("sparkler:require_account")
264                .unwrap_or(&"false".to_string())
265                == "true",
266            hide_social: (other
267                .metadata
268                .kv
269                .get("sparkler:private_social")
270                .unwrap_or(&"false".to_string())
271                == "true")
272                && !is_self,
273            view_password: query.password.clone(),
274            unlocked: if other.metadata.exists("rainbeam:view_password") {
275                (other.metadata.soft_get("rainbeam:view_password") == query.password)
276                    | is_self
277                    | is_helper
278            } else {
279                true
280            },
281            is_powerful,
282            is_helper,
283            is_self,
284        }
285        .render()
286        .unwrap(),
287    )
288}
289
290#[derive(Template)]
291#[template(path = "partials/profile/feed.html")]
292struct PartialProfileTemplate {
293    config: Config,
294    lang: langbeam::LangFile,
295    profile: Option<Box<Profile>>,
296    other: Box<Profile>,
297    responses: Vec<FullResponse>,
298    relationships: HashMap<String, RelationshipStatus>,
299    // ...
300    is_powerful: bool, // at least "manager"
301    is_helper: bool,   // at least "helper"
302}
303
304/// GET /@{username}/_app/feed.html
305pub async fn partial_profile_request(
306    jar: CookieJar,
307    Path(username): Path<String>,
308    State(database): State<Database>,
309    Query(query): Query<ProfileQuery>,
310) -> impl IntoResponse {
311    let auth_user = match jar.get("__Secure-Token") {
312        Some(c) => match database
313            .auth
314            .get_profile_by_unhashed(c.value_trimmed())
315            .await
316        {
317            Ok(ua) => Some(ua),
318            Err(_) => None,
319        },
320        None => None,
321    };
322
323    let other = match database.auth.get_profile_by_username(&username).await {
324        Ok(ua) => ua,
325        Err(_) => return Html(DatabaseError::NotFound.to_html(database)),
326    };
327
328    let responses = if let Some(ref tag) = query.tag {
329        // tagged
330        match database
331            .get_responses_by_author_tagged_paginated(
332                other.id.to_owned(),
333                tag.to_owned(),
334                query.page,
335            )
336            .await
337        {
338            Ok(responses) => responses,
339            Err(e) => return Html(e.to_html(database)),
340        }
341    } else {
342        if let Some(ref search) = query.q {
343            // search
344            match database
345                .get_responses_by_author_searched_paginated(
346                    other.id.to_owned(),
347                    search.to_owned(),
348                    query.page,
349                )
350                .await
351            {
352                Ok(responses) => responses,
353                Err(e) => return Html(e.to_html(database)),
354            }
355        } else {
356            // normal
357            match database
358                .get_responses_by_author_paginated(other.id.to_owned(), query.page)
359                .await
360            {
361                Ok(responses) => responses,
362                Err(e) => return Html(e.to_html(database)),
363            }
364        }
365    };
366
367    let mut is_helper: bool = false;
368    let is_powerful = if let Some(ref ua) = auth_user {
369        let group = match database.auth.get_group_by_id(ua.group).await {
370            Ok(g) => g,
371            Err(_) => return Html(DatabaseError::Other.to_html(database)),
372        };
373
374        is_helper = group.permissions.check_helper();
375        group.permissions.check_manager()
376    } else {
377        false
378    };
379
380    let is_self = if let Some(ref profile) = auth_user {
381        profile.id == other.id
382    } else {
383        false
384    };
385
386    let relationship = if is_self | is_helper {
387        // we're always friends with ourselves! (and staff)
388        // allows user to bypass their own locked profile
389        RelationshipStatus::Friends
390    } else {
391        if let Some(ref profile) = auth_user {
392            database
393                .auth
394                .get_user_relationship(&other.id, &profile.id)
395                .await
396                .0
397        } else {
398            RelationshipStatus::Unknown
399        }
400    };
401
402    let is_blocked = relationship == RelationshipStatus::Blocked;
403
404    if !is_helper && is_blocked {
405        return Html(DatabaseError::NotFound.to_html(database));
406    }
407
408    // build relationships list
409    let mut relationships: HashMap<String, RelationshipStatus> = HashMap::new();
410
411    if let Some(ref ua) = auth_user {
412        for response in &responses {
413            if relationships.contains_key(&response.1.author.id) {
414                continue;
415            }
416
417            if is_helper {
418                // make sure staff can view your responses
419                relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
420                continue;
421            }
422
423            if response.1.author.id == ua.id {
424                // make sure we can view our own responses
425                relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
426                continue;
427            };
428
429            relationships.insert(
430                response.1.author.id.clone(),
431                database
432                    .auth
433                    .get_user_relationship(&response.1.author.id, &ua.id)
434                    .await
435                    .0,
436            );
437        }
438    } else {
439        for response in &responses {
440            // no user, no relationships
441            if relationships.contains_key(&response.1.author.id) {
442                continue;
443            }
444
445            relationships.insert(response.1.author.id.clone(), RelationshipStatus::Unknown);
446        }
447    }
448
449    // ...
450    Html(
451        PartialProfileTemplate {
452            config: database.config.clone(),
453            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
454                c.value_trimmed()
455            } else {
456                ""
457            }),
458            profile: auth_user.clone(),
459            other: other.clone(),
460            responses,
461            relationships,
462            // ...
463            is_powerful,
464            is_helper,
465        }
466        .render()
467        .unwrap(),
468    )
469}
470
471#[derive(Template)]
472#[template(path = "profile/layout_editor.html")]
473struct ProfileLayoutEditorTemplate {
474    config: Config,
475    lang: langbeam::LangFile,
476    profile: Option<Box<Profile>>,
477    other: Box<Profile>,
478    layout: LayoutComponent,
479    is_self: bool,
480}
481
482#[derive(Deserialize)]
483pub struct ProfileLayoutEditorQuery {
484    #[serde(default)]
485    id: String,
486}
487
488/// GET /@{username}/layout
489pub async fn profile_layout_editor_request(
490    jar: CookieJar,
491    Path(username): Path<String>,
492    State(database): State<Database>,
493    Query(props): Query<ProfileLayoutEditorQuery>,
494) -> impl IntoResponse {
495    let auth_user = match jar.get("__Secure-Token") {
496        Some(c) => match database
497            .auth
498            .get_profile_by_unhashed(c.value_trimmed())
499            .await
500        {
501            Ok(ua) => Some(ua),
502            Err(_) => None,
503        },
504        None => None,
505    };
506
507    let other = match database.auth.get_profile(&username).await {
508        Ok(ua) => ua,
509        Err(_) => return Html(DatabaseError::NotFound.to_html(database)),
510    };
511
512    // ...
513    let is_helper = if let Some(ref ua) = auth_user {
514        let group = match database.auth.get_group_by_id(ua.group).await {
515            Ok(g) => g,
516            Err(_) => return Html(DatabaseError::Other.to_html(database)),
517        };
518
519        group.permissions.check_helper()
520    } else {
521        false
522    };
523
524    let is_self = if let Some(ref profile) = auth_user {
525        profile.id == other.id
526    } else {
527        false
528    };
529
530    if !is_helper && !is_self {
531        return Html(DatabaseError::NotAllowed.to_html(database));
532    }
533
534    // ...
535    Html(
536        ProfileLayoutEditorTemplate {
537            config: database.config.clone(),
538            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
539                c.value_trimmed()
540            } else {
541                ""
542            }),
543            profile: auth_user.clone(),
544            other: other.clone(),
545            layout: if props.id.is_empty() {
546                other.layout.clone()
547            } else {
548                // fetch market item
549                let item = match database.auth.get_item(&props.id).await {
550                    Ok(i) => i,
551                    Err(e) => return Html(e.to_string()),
552                };
553
554                if item.r#type != ItemType::Layout {
555                    return Html(DatabaseError::ValueError.to_string());
556                }
557
558                if !database
559                    .auth
560                    .get_transaction_by_customer_item(&auth_user.unwrap().id, &item.id)
561                    .await
562                    .is_ok()
563                {
564                    // not owned
565                    return Html(DatabaseError::NotAllowed.to_string());
566                }
567
568                let mut layout: LayoutComponent = match serde_json::from_str(&item.content) {
569                    Ok(l) => l,
570                    Err(_) => return Html(DatabaseError::ValueError.to_string()),
571                };
572
573                // mark that this layout came from this item
574                layout
575                    .options
576                    .insert("#rainbeam:market_id".to_string(), props.id.clone());
577
578                // return
579                layout
580            },
581            is_self,
582        }
583        .render()
584        .unwrap(),
585    )
586}
587
588#[derive(Template)]
589#[template(path = "profile/embed.html")]
590struct ProfileEmbedTemplate {
591    config: Config,
592    lang: langbeam::LangFile,
593    profile: Option<Box<Profile>>,
594    other: Box<Profile>,
595    pinned: Option<Vec<FullResponse>>,
596    is_powerful: bool,
597    is_helper: bool,
598    lock_profile: bool,
599    disallow_anonymous: bool,
600    require_account: bool,
601}
602
603/// GET /@{username}/embed
604pub async fn profile_embed_request(
605    jar: CookieJar,
606    Path(username): Path<String>,
607    State(database): State<Database>,
608) -> impl IntoResponse {
609    let auth_user = match jar.get("__Secure-Token") {
610        Some(c) => match database
611            .auth
612            .get_profile_by_unhashed(c.value_trimmed())
613            .await
614        {
615            Ok(ua) => Some(ua),
616            Err(_) => None,
617        },
618        None => None,
619    };
620
621    let other = match database.auth.get_profile_by_username(&username).await {
622        Ok(ua) => ua,
623        Err(_) => return Html(DatabaseError::NotFound.to_html(database)),
624    };
625
626    if other.metadata.is_true("rainbeam:authenticated_only") & auth_user.is_none() {
627        // this profile only allows authenticated users to view their profile
628        return Html(DatabaseError::NotAllowed.to_html(database));
629    }
630
631    let pinned = if let Some(pinned) = other.metadata.kv.get("sparkler:pinned") {
632        if pinned.is_empty() {
633            None
634        } else {
635            let mut out = Vec::new();
636
637            for id in pinned.split(",") {
638                match database.get_response(id.to_string()).await {
639                    Ok(response) => {
640                        if response.1.author.id != other.id {
641                            // don't allow us to pin responses from other users
642                            continue;
643                        }
644
645                        // push
646                        out.push(response)
647                    }
648                    Err(_) => continue,
649                }
650            }
651
652            Some(out)
653        }
654    } else {
655        None
656    };
657
658    // permissions
659    let lock_profile = other
660        .metadata
661        .kv
662        .get("sparkler:lock_profile")
663        .unwrap_or(&"false".to_string())
664        == "true";
665
666    let disallow_anonymous = other
667        .metadata
668        .kv
669        .get("sparkler:disallow_anonymous")
670        .unwrap_or(&"false".to_string())
671        == "true";
672
673    let require_account = other
674        .metadata
675        .kv
676        .get("sparkler:require_account")
677        .unwrap_or(&"false".to_string())
678        == "true";
679
680    let mut is_helper: bool = false;
681    let is_powerful = if let Some(ref ua) = auth_user {
682        let group = match database.auth.get_group_by_id(ua.group).await {
683            Ok(g) => g,
684            Err(_) => return Html(DatabaseError::Other.to_html(database)),
685        };
686
687        is_helper = group.permissions.check_helper();
688        group.permissions.check_manager()
689    } else {
690        false
691    };
692
693    let relationship = if let Some(ref profile) = auth_user {
694        database
695            .auth
696            .get_user_relationship(&other.id, &profile.id)
697            .await
698            .0
699    } else {
700        RelationshipStatus::Unknown
701    };
702
703    let is_blocked = relationship == RelationshipStatus::Blocked;
704
705    if !is_helper && is_blocked {
706        return Html(DatabaseError::NotFound.to_html(database));
707    }
708
709    // ...
710    Html(
711        ProfileEmbedTemplate {
712            config: database.config.clone(),
713            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
714                c.value_trimmed()
715            } else {
716                ""
717            }),
718            profile: auth_user.clone(),
719            other: other.clone(),
720            pinned,
721            is_powerful,
722            is_helper,
723            lock_profile,
724            disallow_anonymous,
725            require_account,
726        }
727        .render()
728        .unwrap(),
729    )
730}
731
732#[derive(Template)]
733#[template(path = "profile/social/followers.html")]
734struct FollowersTemplate {
735    config: Config,
736    lang: langbeam::LangFile,
737    profile: Option<Box<Profile>>,
738    unread: usize,
739    notifs: usize,
740    other: Box<Profile>,
741    followers: Vec<(UserFollow, Box<Profile>, Box<Profile>)>,
742    followers_count: usize,
743    following_count: usize,
744    friends_count: usize,
745    page: i32,
746    // ...
747    is_self: bool,
748    is_helper: bool,
749}
750
751/// GET /@{username}/followers
752pub async fn followers_request(
753    jar: CookieJar,
754    Path(username): Path<String>,
755    State(database): State<Database>,
756    Query(query): Query<PaginatedQuery>,
757) -> impl IntoResponse {
758    let auth_user = match jar.get("__Secure-Token") {
759        Some(c) => match database
760            .auth
761            .get_profile_by_unhashed(c.value_trimmed())
762            .await
763        {
764            Ok(ua) => Some(ua),
765            Err(_) => None,
766        },
767        None => None,
768    };
769
770    let unread = if let Some(ref ua) = auth_user {
771        database.get_inbox_count_by_recipient(&ua.id).await
772    } else {
773        0
774    };
775
776    let notifs = if let Some(ref ua) = auth_user {
777        database
778            .auth
779            .get_notification_count_by_recipient(&ua.id)
780            .await
781    } else {
782        0
783    };
784
785    let other = match database.auth.get_profile_by_username(&username).await {
786        Ok(ua) => ua,
787        Err(e) => return Html(e.to_string()),
788    };
789
790    if other.metadata.is_true("rainbeam:authenticated_only") & auth_user.is_none() {
791        // this profile only allows authenticated users to view their profile
792        return Html(DatabaseError::NotAllowed.to_html(database));
793    }
794
795    let is_helper = if let Some(ref ua) = auth_user {
796        let group = match database.auth.get_group_by_id(ua.group).await {
797            Ok(g) => g,
798            Err(_) => return Html(DatabaseError::Other.to_html(database)),
799        };
800
801        group.permissions.check_helper()
802    } else {
803        false
804    };
805
806    let is_self = if let Some(ref profile) = auth_user {
807        profile.id == other.id
808    } else {
809        false
810    };
811
812    if !is_self
813        && (other
814            .metadata
815            .kv
816            .get("sparkler:private_social")
817            .unwrap_or(&"false".to_string())
818            == "true")
819        && !is_helper
820    {
821        // hide social if not self and private_social is true
822        return Html(DatabaseError::NotAllowed.to_html(database));
823    }
824
825    let relationship = if is_self {
826        // we're always friends with ourselves!
827        // allows user to bypass their own locked profile
828        RelationshipStatus::Friends
829    } else {
830        if let Some(ref profile) = auth_user {
831            database
832                .auth
833                .get_user_relationship(&other.id, &profile.id)
834                .await
835                .0
836        } else {
837            RelationshipStatus::Unknown
838        }
839    };
840
841    let is_blocked = relationship == RelationshipStatus::Blocked;
842
843    if !is_helper && is_blocked {
844        return Html(DatabaseError::NotFound.to_html(database));
845    }
846
847    Html(
848        FollowersTemplate {
849            config: database.config.clone(),
850            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
851                c.value_trimmed()
852            } else {
853                ""
854            }),
855            profile: auth_user.clone(),
856            unread,
857            notifs,
858            other: other.clone(),
859            followers: database
860                .auth
861                .get_followers_paginated(&other.id, query.page)
862                .await
863                .unwrap(),
864            followers_count: database.auth.get_followers_count(&other.id).await,
865            following_count: database.auth.get_following_count(&other.id).await,
866            friends_count: database.auth.get_friendship_count_by_user(&other.id).await,
867            page: query.page,
868            // ...
869            is_self,
870            is_helper,
871        }
872        .render()
873        .unwrap(),
874    )
875}
876
877#[derive(Template)]
878#[template(path = "profile/social/following.html")]
879struct FollowingTemplate {
880    config: Config,
881    lang: langbeam::LangFile,
882    profile: Option<Box<Profile>>,
883    unread: usize,
884    notifs: usize,
885    other: Box<Profile>,
886    followers_count: usize,
887    friends_count: usize,
888    following: Vec<(UserFollow, Box<Profile>, Box<Profile>)>,
889    following_count: usize,
890    page: i32,
891    // ...
892    is_self: bool,
893    is_helper: bool,
894}
895
896/// GET /@{username}/following
897pub async fn following_request(
898    jar: CookieJar,
899    Path(username): Path<String>,
900    State(database): State<Database>,
901    Query(query): Query<PaginatedQuery>,
902) -> impl IntoResponse {
903    let auth_user = match jar.get("__Secure-Token") {
904        Some(c) => match database
905            .auth
906            .get_profile_by_unhashed(c.value_trimmed())
907            .await
908        {
909            Ok(ua) => Some(ua),
910            Err(_) => None,
911        },
912        None => None,
913    };
914
915    let unread = if let Some(ref ua) = auth_user {
916        database.get_inbox_count_by_recipient(&ua.id).await
917    } else {
918        0
919    };
920
921    let notifs = if let Some(ref ua) = auth_user {
922        database
923            .auth
924            .get_notification_count_by_recipient(&ua.id)
925            .await
926    } else {
927        0
928    };
929
930    let other = match database.auth.get_profile_by_username(&username).await {
931        Ok(ua) => ua,
932        Err(e) => return Html(e.to_string()),
933    };
934
935    if other.metadata.is_true("rainbeam:authenticated_only") & auth_user.is_none() {
936        // this profile only allows authenticated users to view their profile
937        return Html(DatabaseError::NotAllowed.to_html(database));
938    }
939
940    let is_helper = if let Some(ref ua) = auth_user {
941        let group = match database.auth.get_group_by_id(ua.group).await {
942            Ok(g) => g,
943            Err(_) => return Html(DatabaseError::Other.to_html(database)),
944        };
945
946        group.permissions.check_helper()
947    } else {
948        false
949    };
950
951    let is_self = if let Some(ref profile) = auth_user {
952        profile.id == other.id
953    } else {
954        false
955    };
956
957    if !is_self
958        && (other
959            .metadata
960            .kv
961            .get("sparkler:private_social")
962            .unwrap_or(&"false".to_string())
963            == "true")
964        && !is_helper
965    {
966        // hide social if not self and private_social is true
967        return Html(DatabaseError::NotAllowed.to_html(database));
968    }
969
970    let relationship = if is_self {
971        // we're always friends with ourselves!
972        // allows user to bypass their own locked profile
973        RelationshipStatus::Friends
974    } else {
975        if let Some(ref profile) = auth_user {
976            database
977                .auth
978                .get_user_relationship(&other.id, &profile.id)
979                .await
980                .0
981        } else {
982            RelationshipStatus::Unknown
983        }
984    };
985
986    let is_blocked = relationship == RelationshipStatus::Blocked;
987
988    if !is_helper && is_blocked {
989        return Html(DatabaseError::NotFound.to_html(database));
990    }
991
992    Html(
993        FollowingTemplate {
994            config: database.config.clone(),
995            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
996                c.value_trimmed()
997            } else {
998                ""
999            }),
1000            profile: auth_user.clone(),
1001            unread,
1002            notifs,
1003            other: other.clone(),
1004            followers_count: database.auth.get_followers_count(&other.id).await,
1005            following_count: database.auth.get_following_count(&other.id).await,
1006            following: database
1007                .auth
1008                .get_following_paginated(&other.id, query.page)
1009                .await
1010                .unwrap(),
1011            friends_count: database.auth.get_friendship_count_by_user(&other.id).await,
1012            page: query.page,
1013            // ...
1014            is_self,
1015            is_helper,
1016        }
1017        .render()
1018        .unwrap(),
1019    )
1020}
1021
1022#[derive(Template)]
1023#[template(path = "profile/social/friends.html")]
1024struct FriendsTemplate {
1025    config: Config,
1026    lang: langbeam::LangFile,
1027    profile: Option<Box<Profile>>,
1028    unread: usize,
1029    notifs: usize,
1030    other: Box<Profile>,
1031    friends: Vec<(Box<Profile>, Box<Profile>)>,
1032    followers_count: usize,
1033    following_count: usize,
1034    friends_count: usize,
1035    page: i32,
1036    // ...
1037    is_self: bool,
1038    is_helper: bool,
1039}
1040
1041/// GET /@{username}/friends
1042pub async fn friends_request(
1043    jar: CookieJar,
1044    Path(username): Path<String>,
1045    State(database): State<Database>,
1046    Query(query): Query<PaginatedQuery>,
1047) -> impl IntoResponse {
1048    let auth_user = match jar.get("__Secure-Token") {
1049        Some(c) => match database
1050            .auth
1051            .get_profile_by_unhashed(c.value_trimmed())
1052            .await
1053        {
1054            Ok(ua) => Some(ua),
1055            Err(_) => None,
1056        },
1057        None => None,
1058    };
1059
1060    let unread = if let Some(ref ua) = auth_user {
1061        database.get_inbox_count_by_recipient(&ua.id).await
1062    } else {
1063        0
1064    };
1065
1066    let notifs = if let Some(ref ua) = auth_user {
1067        database
1068            .auth
1069            .get_notification_count_by_recipient(&ua.id)
1070            .await
1071    } else {
1072        0
1073    };
1074
1075    let other = match database.auth.get_profile_by_username(&username).await {
1076        Ok(ua) => ua,
1077        Err(e) => return Html(e.to_string()),
1078    };
1079
1080    if other.metadata.is_true("rainbeam:authenticated_only") & auth_user.is_none() {
1081        // this profile only allows authenticated users to view their profile
1082        return Html(DatabaseError::NotAllowed.to_html(database));
1083    }
1084
1085    let is_helper = if let Some(ref ua) = auth_user {
1086        let group = match database.auth.get_group_by_id(ua.group).await {
1087            Ok(g) => g,
1088            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1089        };
1090
1091        group.permissions.check_helper()
1092    } else {
1093        false
1094    };
1095
1096    let is_self = if let Some(ref profile) = auth_user {
1097        profile.id == other.id
1098    } else {
1099        false
1100    };
1101
1102    if !is_self
1103        && (other
1104            .metadata
1105            .kv
1106            .get("sparkler:private_social")
1107            .unwrap_or(&"false".to_string())
1108            == "true")
1109        && !is_helper
1110    {
1111        // hide social if not self and private_social is true
1112        return Html(DatabaseError::NotAllowed.to_html(database));
1113    }
1114
1115    let relationship = if is_self {
1116        // we're always friends with ourselves!
1117        // allows user to bypass their own locked profile
1118        RelationshipStatus::Friends
1119    } else {
1120        if let Some(ref profile) = auth_user {
1121            database
1122                .auth
1123                .get_user_relationship(&other.id, &profile.id)
1124                .await
1125                .0
1126        } else {
1127            RelationshipStatus::Unknown
1128        }
1129    };
1130
1131    let is_blocked = relationship == RelationshipStatus::Blocked;
1132
1133    if !is_helper && is_blocked {
1134        return Html(DatabaseError::NotFound.to_html(database));
1135    }
1136
1137    Html(
1138        FriendsTemplate {
1139            config: database.config.clone(),
1140            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1141                c.value_trimmed()
1142            } else {
1143                ""
1144            }),
1145            profile: auth_user.clone(),
1146            unread,
1147            notifs,
1148            other: other.clone(),
1149            friends: database
1150                .auth
1151                .get_user_participating_relationships_of_status_paginated(
1152                    &other.id,
1153                    RelationshipStatus::Friends,
1154                    query.page,
1155                )
1156                .await
1157                .unwrap_or_default(),
1158            followers_count: database.auth.get_followers_count(&other.id).await,
1159            following_count: database.auth.get_following_count(&other.id).await,
1160            friends_count: database.auth.get_friendship_count_by_user(&other.id).await,
1161            page: query.page,
1162            // ...
1163            is_self,
1164            is_helper,
1165        }
1166        .render()
1167        .unwrap(),
1168    )
1169}
1170
1171#[derive(Template)]
1172#[template(path = "profile/social/requests.html")]
1173struct FriendRequestsTemplate {
1174    config: Config,
1175    lang: langbeam::LangFile,
1176    profile: Option<Box<Profile>>,
1177    unread: usize,
1178    notifs: usize,
1179    other: Box<Profile>,
1180    requests: Vec<(Box<Profile>, Box<Profile>)>,
1181    followers_count: usize,
1182    following_count: usize,
1183    friends_count: usize,
1184    page: i32,
1185    // ...
1186    is_self: bool,
1187    is_helper: bool,
1188}
1189
1190/// GET /@{username}/friends/requests
1191pub async fn friend_requests_request(
1192    jar: CookieJar,
1193    Path(username): Path<String>,
1194    State(database): State<Database>,
1195    Query(query): Query<PaginatedQuery>,
1196) -> impl IntoResponse {
1197    let auth_user = match jar.get("__Secure-Token") {
1198        Some(c) => match database
1199            .auth
1200            .get_profile_by_unhashed(c.value_trimmed())
1201            .await
1202        {
1203            Ok(ua) => ua,
1204            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1205        },
1206        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1207    };
1208
1209    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
1210
1211    let notifs = database
1212        .auth
1213        .get_notification_count_by_recipient(&auth_user.id)
1214        .await;
1215
1216    let is_helper = {
1217        let group = match database.auth.get_group_by_id(auth_user.group).await {
1218            Ok(g) => g,
1219            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1220        };
1221
1222        group.permissions.check_helper()
1223    };
1224
1225    let other = match database.auth.get_profile_by_username(&username).await {
1226        Ok(ua) => ua,
1227        Err(e) => return Html(e.to_string()),
1228    };
1229
1230    let is_self = auth_user.id == other.id;
1231
1232    if !is_self && !is_helper {
1233        return Html(DatabaseError::NotAllowed.to_html(database));
1234    }
1235
1236    Html(
1237        FriendRequestsTemplate {
1238            config: database.config.clone(),
1239            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1240                c.value_trimmed()
1241            } else {
1242                ""
1243            }),
1244            profile: Some(auth_user),
1245            unread,
1246            notifs,
1247            other: other.clone(),
1248            requests: database
1249                .auth
1250                .get_user_participating_relationships_of_status_paginated(
1251                    &other.id,
1252                    RelationshipStatus::Pending,
1253                    query.page,
1254                )
1255                .await
1256                .unwrap(),
1257            followers_count: database.auth.get_followers_count(&other.id).await,
1258            following_count: database.auth.get_following_count(&other.id).await,
1259            friends_count: database.auth.get_friendship_count_by_user(&other.id).await,
1260            page: query.page,
1261            // ...
1262            is_self,
1263            is_helper,
1264        }
1265        .render()
1266        .unwrap(),
1267    )
1268}
1269
1270#[derive(Template)]
1271#[template(path = "profile/social/blocks.html")]
1272struct BlocksTemplate {
1273    config: Config,
1274    lang: langbeam::LangFile,
1275    profile: Option<Box<Profile>>,
1276    unread: usize,
1277    notifs: usize,
1278    other: Box<Profile>,
1279    blocks: Vec<(Box<Profile>, Box<Profile>)>,
1280    followers_count: usize,
1281    following_count: usize,
1282    friends_count: usize,
1283    page: i32,
1284    // ...
1285    is_self: bool,
1286    is_helper: bool,
1287}
1288
1289/// GET /@{username}/friends/blocks
1290pub async fn blocks_request(
1291    jar: CookieJar,
1292    Path(username): Path<String>,
1293    State(database): State<Database>,
1294    Query(query): Query<PaginatedQuery>,
1295) -> impl IntoResponse {
1296    let auth_user = match jar.get("__Secure-Token") {
1297        Some(c) => match database
1298            .auth
1299            .get_profile_by_unhashed(c.value_trimmed())
1300            .await
1301        {
1302            Ok(ua) => ua,
1303            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1304        },
1305        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1306    };
1307
1308    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
1309
1310    let notifs = database
1311        .auth
1312        .get_notification_count_by_recipient(&auth_user.id)
1313        .await;
1314
1315    let is_helper = {
1316        let group = match database.auth.get_group_by_id(auth_user.group).await {
1317            Ok(g) => g,
1318            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1319        };
1320
1321        group.permissions.check_helper()
1322    };
1323
1324    if !is_helper {
1325        return Html(DatabaseError::NotAllowed.to_html(database));
1326    }
1327
1328    let other = match database.auth.get_profile_by_username(&username).await {
1329        Ok(ua) => ua,
1330        Err(e) => return Html(e.to_string()),
1331    };
1332
1333    let is_self = auth_user.id == other.id;
1334
1335    if !is_self && !is_helper {
1336        return Html(DatabaseError::NotAllowed.to_html(database));
1337    }
1338
1339    Html(
1340        BlocksTemplate {
1341            config: database.config.clone(),
1342            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1343                c.value_trimmed()
1344            } else {
1345                ""
1346            }),
1347            profile: Some(auth_user),
1348            unread,
1349            notifs,
1350            other: other.clone(),
1351            blocks: database
1352                .auth
1353                .get_user_participating_relationships_of_status_paginated(
1354                    &other.id,
1355                    RelationshipStatus::Blocked,
1356                    query.page,
1357                )
1358                .await
1359                .unwrap(),
1360            followers_count: database.auth.get_followers_count(&other.id).await,
1361            following_count: database.auth.get_following_count(&other.id).await,
1362            friends_count: database.auth.get_friendship_count_by_user(&other.id).await,
1363            page: query.page,
1364            // ...
1365            is_self,
1366            is_helper,
1367        }
1368        .render()
1369        .unwrap(),
1370    )
1371}
1372
1373#[derive(Template)]
1374#[template(path = "profile/questions.html")]
1375struct ProfileQuestionsTemplate {
1376    config: Config,
1377    lang: langbeam::LangFile,
1378    profile: Option<Box<Profile>>,
1379    unread: usize,
1380    notifs: usize,
1381    other: Box<Profile>,
1382    questions: Vec<(Question, usize, usize)>,
1383    questions_count: usize,
1384    response_count: usize,
1385    followers_count: usize,
1386    following_count: usize,
1387    friends_count: usize,
1388    is_following: bool,
1389    is_following_you: bool,
1390    metadata: String,
1391    page: i32,
1392    query: String,
1393    // ...
1394    relationship: RelationshipStatus,
1395    lock_profile: bool,
1396    disallow_anonymous: bool,
1397    require_account: bool,
1398    hide_social: bool,
1399    unlocked: bool,
1400    is_powerful: bool,
1401    is_helper: bool,
1402    is_self: bool,
1403}
1404
1405/// GET /@{username}/questions
1406pub async fn questions_request(
1407    jar: CookieJar,
1408    Path(username): Path<String>,
1409    State(database): State<Database>,
1410    Query(query): Query<ProfileQuery>,
1411) -> impl IntoResponse {
1412    let auth_user = match jar.get("__Secure-Token") {
1413        Some(c) => match database
1414            .auth
1415            .get_profile_by_unhashed(c.value_trimmed())
1416            .await
1417        {
1418            Ok(ua) => Some(ua),
1419            Err(_) => None,
1420        },
1421        None => None,
1422    };
1423
1424    let unread = if let Some(ref ua) = auth_user {
1425        database.get_inbox_count_by_recipient(&ua.id).await
1426    } else {
1427        0
1428    };
1429
1430    let notifs = if let Some(ref ua) = auth_user {
1431        database
1432            .auth
1433            .get_notification_count_by_recipient(&ua.id)
1434            .await
1435    } else {
1436        0
1437    };
1438
1439    let other = match database.auth.get_profile_by_username(&username).await {
1440        Ok(ua) => ua,
1441        Err(e) => return Html(e.to_string()),
1442    };
1443
1444    if other.metadata.is_true("rainbeam:authenticated_only") & auth_user.is_none() {
1445        // this profile only allows authenticated users to view their profile
1446        return Html(DatabaseError::NotAllowed.to_html(database));
1447    }
1448
1449    let is_following = if let Some(ref ua) = auth_user {
1450        match database.auth.get_follow(&ua.id, &other.id).await {
1451            Ok(_) => true,
1452            Err(_) => false,
1453        }
1454    } else {
1455        false
1456    };
1457
1458    let is_following_you = if let Some(ref ua) = auth_user {
1459        match database.auth.get_follow(&other.id, &ua.id).await {
1460            Ok(_) => true,
1461            Err(_) => false,
1462        }
1463    } else {
1464        false
1465    };
1466
1467    let questions = if let Some(ref search) = query.q {
1468        match database
1469            .get_global_questions_by_author_searched_paginated(
1470                other.id.to_owned(),
1471                search.clone(),
1472                query.page,
1473            )
1474            .await
1475        {
1476            Ok(responses) => responses,
1477            Err(e) => return Html(e.to_html(database)),
1478        }
1479    } else {
1480        match database
1481            .get_global_questions_by_author_paginated(other.id.to_owned(), query.page)
1482            .await
1483        {
1484            Ok(responses) => responses,
1485            Err(e) => return Html(e.to_html(database)),
1486        }
1487    };
1488
1489    let mut is_helper: bool = false;
1490    let is_powerful = if let Some(ref ua) = auth_user {
1491        let group = match database.auth.get_group_by_id(ua.group).await {
1492            Ok(g) => g,
1493            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1494        };
1495
1496        is_helper = group.permissions.check_helper();
1497        group.permissions.check_manager()
1498    } else {
1499        false
1500    };
1501
1502    let is_self = if let Some(ref profile) = auth_user {
1503        profile.id == other.id
1504    } else {
1505        false
1506    };
1507
1508    let relationship = if is_self {
1509        // we're always friends with ourselves!
1510        // allows user to bypass their own locked profile
1511        RelationshipStatus::Friends
1512    } else {
1513        if let Some(ref profile) = auth_user {
1514            database
1515                .auth
1516                .get_user_relationship(&other.id, &profile.id)
1517                .await
1518                .0
1519        } else {
1520            RelationshipStatus::Unknown
1521        }
1522    };
1523
1524    let is_blocked = relationship == RelationshipStatus::Blocked;
1525
1526    if !is_helper && is_blocked {
1527        return Html(DatabaseError::NotFound.to_html(database));
1528    }
1529
1530    Html(
1531        ProfileQuestionsTemplate {
1532            config: database.config.clone(),
1533            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1534                c.value_trimmed()
1535            } else {
1536                ""
1537            }),
1538            profile: auth_user.clone(),
1539            unread,
1540            notifs,
1541            other: other.clone(),
1542            questions,
1543            questions_count: database
1544                .get_global_questions_count_by_author(&other.id)
1545                .await,
1546            response_count: database.get_response_count_by_author(&other.id).await,
1547            followers_count: database.auth.get_followers_count(&other.id).await,
1548            following_count: database.auth.get_following_count(&other.id).await,
1549            friends_count: database.auth.get_friendship_count_by_user(&other.id).await,
1550            is_following,
1551            is_following_you,
1552            metadata: clean_metadata(&other.metadata),
1553            page: query.page,
1554            query: query.q.unwrap_or(String::new()),
1555            // ...
1556            relationship,
1557            lock_profile: other
1558                .metadata
1559                .kv
1560                .get("sparkler:lock_profile")
1561                .unwrap_or(&"false".to_string())
1562                == "true",
1563            disallow_anonymous: other
1564                .metadata
1565                .kv
1566                .get("sparkler:disallow_anonymous")
1567                .unwrap_or(&"false".to_string())
1568                == "true",
1569            require_account: other
1570                .metadata
1571                .kv
1572                .get("sparkler:require_account")
1573                .unwrap_or(&"false".to_string())
1574                == "true",
1575            hide_social: (other
1576                .metadata
1577                .kv
1578                .get("sparkler:private_social")
1579                .unwrap_or(&"false".to_string())
1580                == "true")
1581                && !is_self,
1582            unlocked: if other.metadata.exists("rainbeam:view_password") {
1583                (other.metadata.soft_get("rainbeam:view_password") == query.password)
1584                    | is_self
1585                    | is_helper
1586            } else {
1587                true
1588            },
1589            is_powerful,
1590            is_helper,
1591            is_self,
1592        }
1593        .render()
1594        .unwrap(),
1595    )
1596}
1597
1598#[derive(Template)]
1599#[template(path = "profile/mod.html")]
1600struct ModTemplate {
1601    config: Config,
1602    lang: langbeam::LangFile,
1603    profile: Option<Box<Profile>>,
1604    unread: usize,
1605    notifs: usize,
1606    other: Box<Profile>,
1607    warnings: Vec<Warning>,
1608    response_count: usize,
1609    questions_count: usize,
1610    followers_count: usize,
1611    following_count: usize,
1612    friends_count: usize,
1613    is_following: bool,
1614    is_following_you: bool,
1615    metadata: String,
1616    badges: String,
1617    tokens: String,
1618    tokens_src: Vec<String>,
1619    // ...
1620    relationship: RelationshipStatus,
1621    lock_profile: bool,
1622    disallow_anonymous: bool,
1623    require_account: bool,
1624    hide_social: bool,
1625    unlocked: bool,
1626    is_powerful: bool,
1627    is_helper: bool,
1628    is_self: bool,
1629}
1630
1631/// GET /@{username}/mod
1632pub async fn mod_request(
1633    jar: CookieJar,
1634    Path(username): Path<String>,
1635    State(database): State<Database>,
1636    Query(query): Query<PasswordQuery>,
1637) -> impl IntoResponse {
1638    let auth_user = match jar.get("__Secure-Token") {
1639        Some(c) => match database
1640            .auth
1641            .get_profile_by_unhashed(c.value_trimmed())
1642            .await
1643        {
1644            Ok(ua) => ua,
1645            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1646        },
1647        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1648    };
1649
1650    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
1651
1652    let notifs = database
1653        .auth
1654        .get_notification_count_by_recipient(&auth_user.id)
1655        .await;
1656
1657    let mut other = match database.auth.get_profile_by_username(&username).await {
1658        Ok(ua) => ua,
1659        Err(_) => return Html(DatabaseError::NotFound.to_html(database)),
1660    };
1661
1662    let is_following = match database.auth.get_follow(&auth_user.id, &other.id).await {
1663        Ok(_) => true,
1664        Err(_) => false,
1665    };
1666
1667    let is_following_you = match database.auth.get_follow(&other.id, &auth_user.id).await {
1668        Ok(_) => true,
1669        Err(_) => false,
1670    };
1671
1672    let mut is_helper: bool = false;
1673
1674    let group = match database.auth.get_group_by_id(auth_user.group).await {
1675        Ok(g) => g,
1676        Err(_) => return Html(DatabaseError::Other.to_html(database)),
1677    };
1678
1679    let is_powerful = {
1680        if group.permissions.check_helper() {
1681            is_helper = true;
1682        }
1683
1684        group.permissions.check_manager()
1685    };
1686
1687    if !group.permissions.check(FinePermission::VIEW_PROFILE_MANAGE) {
1688        return Html(DatabaseError::NotAllowed.to_html(database));
1689    }
1690
1691    if other.group == -1 {
1692        other.group = -2;
1693    }
1694
1695    let warnings = match database
1696        .auth
1697        .get_warnings_by_recipient(&other.id, auth_user.clone())
1698        .await
1699    {
1700        Ok(r) => r,
1701        Err(_) => return Html(DatabaseError::Other.to_html(database)),
1702    };
1703
1704    let is_self = auth_user.id == other.id;
1705    let relationship = RelationshipStatus::Friends; // moderators should always be your friend! (bypass private profile)
1706
1707    Html(
1708        ModTemplate {
1709            config: database.config.clone(),
1710            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1711                c.value_trimmed()
1712            } else {
1713                ""
1714            }),
1715            profile: Some(auth_user.clone()),
1716            unread,
1717            notifs,
1718            other: other.clone(),
1719            warnings,
1720            response_count: database.get_response_count_by_author(&other.id).await,
1721            questions_count: database
1722                .get_global_questions_count_by_author(&other.id)
1723                .await,
1724            followers_count: database.auth.get_followers_count(&other.id).await,
1725            following_count: database.auth.get_following_count(&other.id).await,
1726            friends_count: database.auth.get_friendship_count_by_user(&other.id).await,
1727            is_following,
1728            is_following_you,
1729            metadata: clean_metadata(&other.metadata),
1730            badges: serde_json::to_string_pretty(&other.badges).unwrap(),
1731            tokens: serde_json::to_string(&other.tokens).unwrap(),
1732            tokens_src: other.tokens.clone(),
1733            // ...
1734            relationship,
1735            lock_profile: other
1736                .metadata
1737                .kv
1738                .get("sparkler:lock_profile")
1739                .unwrap_or(&"false".to_string())
1740                == "true",
1741            disallow_anonymous: other
1742                .metadata
1743                .kv
1744                .get("sparkler:disallow_anonymous")
1745                .unwrap_or(&"false".to_string())
1746                == "true",
1747            require_account: other
1748                .metadata
1749                .kv
1750                .get("sparkler:require_account")
1751                .unwrap_or(&"false".to_string())
1752                == "true",
1753            hide_social: (other
1754                .metadata
1755                .kv
1756                .get("sparkler:private_social")
1757                .unwrap_or(&"false".to_string())
1758                == "true")
1759                && !is_self,
1760            unlocked: if other.metadata.exists("rainbeam:view_password") {
1761                (other.metadata.soft_get("rainbeam:view_password") == query.password)
1762                    | is_self
1763                    | is_helper
1764            } else {
1765                true
1766            },
1767            is_powerful,
1768            is_helper,
1769            is_self,
1770        }
1771        .render()
1772        .unwrap(),
1773    )
1774}
1775
1776#[derive(Template)]
1777#[template(path = "profile/inbox.html")]
1778struct ProfileQuestionsInboxTemplate {
1779    config: Config,
1780    lang: langbeam::LangFile,
1781    profile: Option<Box<Profile>>,
1782    unread: usize,
1783    notifs: usize,
1784    other: Box<Profile>,
1785    questions: Vec<Question>,
1786    questions_count: usize,
1787    response_count: usize,
1788    followers_count: usize,
1789    following_count: usize,
1790    friends_count: usize,
1791    is_following: bool,
1792    is_following_you: bool,
1793    metadata: String,
1794    // ...
1795    relationship: RelationshipStatus,
1796    lock_profile: bool,
1797    disallow_anonymous: bool,
1798    require_account: bool,
1799    hide_social: bool,
1800    unlocked: bool,
1801    is_powerful: bool,
1802    is_helper: bool,
1803    is_self: bool,
1804}
1805
1806/// GET /@{username}/questions/inbox
1807pub async fn inbox_request(
1808    jar: CookieJar,
1809    Path(username): Path<String>,
1810    State(database): State<Database>,
1811    Query(query): Query<PasswordQuery>,
1812) -> impl IntoResponse {
1813    let auth_user = match jar.get("__Secure-Token") {
1814        Some(c) => match database
1815            .auth
1816            .get_profile_by_unhashed(c.value_trimmed())
1817            .await
1818        {
1819            Ok(ua) => ua,
1820            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1821        },
1822        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1823    };
1824
1825    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
1826
1827    let notifs = database
1828        .auth
1829        .get_notification_count_by_recipient(&auth_user.id)
1830        .await;
1831
1832    let other = match database.auth.get_profile_by_username(&username).await {
1833        Ok(ua) => ua,
1834        Err(e) => return Html(e.to_string()),
1835    };
1836
1837    let is_following = match database.auth.get_follow(&auth_user.id, &other.id).await {
1838        Ok(_) => true,
1839        Err(_) => false,
1840    };
1841
1842    let is_following_you = match database.auth.get_follow(&other.id, &auth_user.id).await {
1843        Ok(_) => true,
1844        Err(_) => false,
1845    };
1846
1847    let questions = match database.get_questions_by_recipient(&other.id).await {
1848        Ok(responses) => responses,
1849        Err(e) => return Html(e.to_html(database)),
1850    };
1851
1852    let mut is_helper: bool = false;
1853    let is_powerful = {
1854        let group = match database.auth.get_group_by_id(auth_user.group).await {
1855            Ok(g) => g,
1856            Err(_) => return Html(DatabaseError::Other.to_html(database)),
1857        };
1858
1859        if group.permissions.check_helper() {
1860            is_helper = true;
1861        }
1862
1863        group.permissions.check_manager()
1864    };
1865
1866    if !is_powerful {
1867        return Html(DatabaseError::NotAllowed.to_html(database));
1868    }
1869
1870    let is_self = auth_user.id == other.id;
1871
1872    let relationship = database
1873        .auth
1874        .get_user_relationship(&other.id, &auth_user.id)
1875        .await
1876        .0;
1877
1878    let is_blocked = relationship == RelationshipStatus::Blocked;
1879
1880    if !is_helper && is_blocked {
1881        return Html(DatabaseError::NotFound.to_html(database));
1882    }
1883
1884    Html(
1885        ProfileQuestionsInboxTemplate {
1886            config: database.config.clone(),
1887            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
1888                c.value_trimmed()
1889            } else {
1890                ""
1891            }),
1892            profile: Some(auth_user.clone()),
1893            unread,
1894            notifs,
1895            other: other.clone(),
1896            questions,
1897            questions_count: database
1898                .get_global_questions_count_by_author(&other.id)
1899                .await,
1900            response_count: database.get_response_count_by_author(&other.id).await,
1901            followers_count: database.auth.get_followers_count(&other.id).await,
1902            following_count: database.auth.get_following_count(&other.id).await,
1903            friends_count: database.auth.get_friendship_count_by_user(&other.id).await,
1904            is_following,
1905            is_following_you,
1906            metadata: clean_metadata(&other.metadata),
1907            // ...
1908            relationship,
1909            lock_profile: other
1910                .metadata
1911                .kv
1912                .get("sparkler:lock_profile")
1913                .unwrap_or(&"false".to_string())
1914                == "true",
1915            disallow_anonymous: other
1916                .metadata
1917                .kv
1918                .get("sparkler:disallow_anonymous")
1919                .unwrap_or(&"false".to_string())
1920                == "true",
1921            require_account: other
1922                .metadata
1923                .kv
1924                .get("sparkler:require_account")
1925                .unwrap_or(&"false".to_string())
1926                == "true",
1927            hide_social: (other
1928                .metadata
1929                .kv
1930                .get("sparkler:private_social")
1931                .unwrap_or(&"false".to_string())
1932                == "true")
1933                && !is_self,
1934            unlocked: if other.metadata.exists("rainbeam:view_password") {
1935                (other.metadata.soft_get("rainbeam:view_password") == query.password)
1936                    | is_self
1937                    | is_helper
1938            } else {
1939                true
1940            },
1941            is_powerful,
1942            is_helper,
1943            is_self,
1944        }
1945        .render()
1946        .unwrap(),
1947    )
1948}
1949
1950#[derive(Template)]
1951#[template(path = "profile/outbox.html")]
1952struct ProfileQuestionsOutboxTemplate {
1953    config: Config,
1954    lang: langbeam::LangFile,
1955    profile: Option<Box<Profile>>,
1956    unread: usize,
1957    notifs: usize,
1958    other: Box<Profile>,
1959    questions: Vec<Question>,
1960    questions_count: usize,
1961    response_count: usize,
1962    followers_count: usize,
1963    following_count: usize,
1964    friends_count: usize,
1965    is_following: bool,
1966    is_following_you: bool,
1967    metadata: String,
1968    page: i32,
1969    // ...
1970    relationship: RelationshipStatus,
1971    lock_profile: bool,
1972    disallow_anonymous: bool,
1973    require_account: bool,
1974    hide_social: bool,
1975    unlocked: bool,
1976    is_powerful: bool,
1977    is_helper: bool,
1978    is_self: bool,
1979}
1980
1981/// GET /@{username}/questions/outbox
1982pub async fn outbox_request(
1983    jar: CookieJar,
1984    Path(username): Path<String>,
1985    State(database): State<Database>,
1986    Query(query): Query<ProfileQuery>,
1987) -> impl IntoResponse {
1988    let auth_user = match jar.get("__Secure-Token") {
1989        Some(c) => match database
1990            .auth
1991            .get_profile_by_unhashed(c.value_trimmed())
1992            .await
1993        {
1994            Ok(ua) => ua,
1995            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
1996        },
1997        None => return Html(DatabaseError::NotAllowed.to_html(database)),
1998    };
1999
2000    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
2001
2002    let notifs = database
2003        .auth
2004        .get_notification_count_by_recipient(&auth_user.id)
2005        .await;
2006
2007    let other = match database.auth.get_profile_by_username(&username).await {
2008        Ok(ua) => ua,
2009        Err(e) => return Html(e.to_string()),
2010    };
2011
2012    let is_following = match database.auth.get_follow(&auth_user.id, &other.id).await {
2013        Ok(_) => true,
2014        Err(_) => false,
2015    };
2016
2017    let is_following_you = match database.auth.get_follow(&other.id, &auth_user.id).await {
2018        Ok(_) => true,
2019        Err(_) => false,
2020    };
2021
2022    let questions = match database
2023        .get_questions_by_author_paginated(other.id.to_owned(), query.page)
2024        .await
2025    {
2026        Ok(responses) => responses,
2027        Err(e) => return Html(e.to_html(database)),
2028    };
2029
2030    let mut is_helper: bool = false;
2031    let is_powerful = {
2032        let group = match database.auth.get_group_by_id(auth_user.group).await {
2033            Ok(g) => g,
2034            Err(_) => return Html(DatabaseError::Other.to_html(database)),
2035        };
2036
2037        if group.permissions.check_helper() {
2038            is_helper = true;
2039        }
2040
2041        group.permissions.check_manager()
2042    };
2043
2044    let is_self = auth_user.id == other.id;
2045
2046    if !is_powerful && !is_self {
2047        return Html(DatabaseError::NotAllowed.to_html(database));
2048    }
2049
2050    let relationship = database
2051        .auth
2052        .get_user_relationship(&other.id, &auth_user.id)
2053        .await
2054        .0;
2055
2056    let is_blocked = relationship == RelationshipStatus::Blocked;
2057
2058    if !is_helper && is_blocked {
2059        return Html(DatabaseError::NotFound.to_html(database));
2060    }
2061
2062    Html(
2063        ProfileQuestionsOutboxTemplate {
2064            config: database.config.clone(),
2065            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
2066                c.value_trimmed()
2067            } else {
2068                ""
2069            }),
2070            profile: Some(auth_user.clone()),
2071            unread,
2072            notifs,
2073            other: other.clone(),
2074            questions,
2075            questions_count: database
2076                .get_global_questions_count_by_author(&other.id)
2077                .await,
2078            response_count: database.get_response_count_by_author(&other.id).await,
2079            followers_count: database.auth.get_followers_count(&other.id).await,
2080            following_count: database.auth.get_following_count(&other.id).await,
2081            friends_count: database.auth.get_friendship_count_by_user(&other.id).await,
2082            is_following,
2083            is_following_you,
2084            metadata: clean_metadata(&other.metadata),
2085            page: query.page,
2086            // ...
2087            relationship,
2088            lock_profile: other
2089                .metadata
2090                .kv
2091                .get("sparkler:lock_profile")
2092                .unwrap_or(&"false".to_string())
2093                == "true",
2094            disallow_anonymous: other
2095                .metadata
2096                .kv
2097                .get("sparkler:disallow_anonymous")
2098                .unwrap_or(&"false".to_string())
2099                == "true",
2100            require_account: other
2101                .metadata
2102                .kv
2103                .get("sparkler:require_account")
2104                .unwrap_or(&"false".to_string())
2105                == "true",
2106            hide_social: (other
2107                .metadata
2108                .kv
2109                .get("sparkler:private_social")
2110                .unwrap_or(&"false".to_string())
2111                == "true")
2112                && !is_self,
2113            unlocked: if other.metadata.exists("rainbeam:view_password") {
2114                (other.metadata.soft_get("rainbeam:view_password") == query.password)
2115                    | is_self
2116                    | is_helper
2117            } else {
2118                true
2119            },
2120            is_powerful,
2121            is_helper,
2122            is_self,
2123        }
2124        .render()
2125        .unwrap(),
2126    )
2127}
2128
2129#[derive(Template)]
2130#[template(path = "profile/social/friend_request.html")]
2131struct FriendRequestTemplate {
2132    config: Config,
2133    lang: langbeam::LangFile,
2134    profile: Option<Box<Profile>>,
2135    unread: usize,
2136    notifs: usize,
2137    other: Box<Profile>,
2138}
2139
2140/// GET /@{username}/relationship/friend_accept
2141pub async fn friend_request(
2142    jar: CookieJar,
2143    Path(username): Path<String>,
2144    State(database): State<Database>,
2145) -> impl IntoResponse {
2146    let auth_user = match jar.get("__Secure-Token") {
2147        Some(c) => match database
2148            .auth
2149            .get_profile_by_unhashed(c.value_trimmed())
2150            .await
2151        {
2152            Ok(ua) => ua,
2153            Err(_) => return Html(DatabaseError::NotAllowed.to_html(database)),
2154        },
2155        None => return Html(DatabaseError::NotAllowed.to_html(database)),
2156    };
2157
2158    let unread = database.get_inbox_count_by_recipient(&auth_user.id).await;
2159
2160    let notifs = database
2161        .auth
2162        .get_notification_count_by_recipient(&auth_user.id)
2163        .await;
2164
2165    let other = match database.auth.get_profile_by_username(&username).await {
2166        Ok(ua) => ua,
2167        Err(e) => return Html(e.to_string()),
2168    };
2169
2170    let relationship = database
2171        .auth
2172        .get_user_relationship(&other.id, &auth_user.id)
2173        .await;
2174
2175    // the relationship status must be pending AND we must be user 2 (the user who got sent the request)
2176    if (relationship.0 != RelationshipStatus::Pending) | (relationship.2 != auth_user.id) {
2177        return Html(DatabaseError::NotFound.to_html(database));
2178    }
2179
2180    Html(
2181        FriendRequestTemplate {
2182            config: database.config.clone(),
2183            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
2184                c.value_trimmed()
2185            } else {
2186                ""
2187            }),
2188            profile: Some(auth_user.clone()),
2189            unread,
2190            notifs,
2191            other: other.clone(),
2192        }
2193        .render()
2194        .unwrap(),
2195    )
2196}
2197
2198#[derive(Template)]
2199#[template(path = "fun/styled_profile_card.html")]
2200struct CardTemplate {
2201    lang: langbeam::LangFile,
2202    profile: Option<Box<Profile>>,
2203    user: Box<Profile>,
2204}
2205
2206/// GET /@{username}/_app/card.html
2207pub async fn render_card_request(
2208    jar: CookieJar,
2209    Path(username): Path<String>,
2210    State(database): State<Database>,
2211) -> impl IntoResponse {
2212    let auth_user = match jar.get("__Secure-Token") {
2213        Some(c) => match database
2214            .auth
2215            .get_profile_by_unhashed(c.value_trimmed())
2216            .await
2217        {
2218            Ok(ua) => Some(ua),
2219            Err(_) => None,
2220        },
2221        None => None,
2222    };
2223
2224    Html(
2225        CardTemplate {
2226            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
2227                c.value_trimmed()
2228            } else {
2229                ""
2230            }),
2231            profile: auth_user,
2232            user: match database.get_profile(username).await {
2233                Ok(ua) => ua,
2234                Err(e) => return Html(e.to_html(database)),
2235            },
2236        }
2237        .render()
2238        .unwrap(),
2239    )
2240}
2241
2242#[derive(Template)]
2243#[template(path = "profile/warning.html")]
2244struct WarningTemplate {
2245    config: Config,
2246    lang: langbeam::LangFile,
2247    profile: Option<Box<Profile>>,
2248    other: Box<Profile>,
2249}
2250
2251/// GET /@{username}/_app/warning
2252pub async fn warning_request(
2253    jar: CookieJar,
2254    Path(username): Path<String>,
2255    State(database): State<Database>,
2256) -> impl IntoResponse {
2257    let auth_user = match jar.get("__Secure-Token") {
2258        Some(c) => match database
2259            .auth
2260            .get_profile_by_unhashed(c.value_trimmed())
2261            .await
2262        {
2263            Ok(ua) => Some(ua),
2264            Err(_) => None,
2265        },
2266        None => None,
2267    };
2268
2269    let other = match database.auth.get_profile_by_username(&username).await {
2270        Ok(ua) => ua,
2271        Err(e) => return Html(e.to_string()),
2272    };
2273
2274    Html(
2275        WarningTemplate {
2276            config: database.config.clone(),
2277            lang: database.lang(if let Some(c) = jar.get("net.rainbeam.langs.choice") {
2278                c.value_trimmed()
2279            } else {
2280                ""
2281            }),
2282            profile: auth_user.clone(),
2283            other: other.clone(),
2284        }
2285        .render()
2286        .unwrap(),
2287    )
2288}