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
34pub fn escape_username(name: &String) -> String {
38 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 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
93pub 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 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 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 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
175pub 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 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 relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
226 continue;
227 }
228
229 if response.1.author.id == auth_user.id {
230 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 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
276pub 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 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 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
333pub 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 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 relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
381 continue;
382 }
383
384 if response.1.author.id == auth_user.id {
385 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 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
432pub 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 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 relationships.insert(response.1.author.id.clone(), RelationshipStatus::Friends);
482 continue;
483 }
484
485 if response.1.author.id == ua.id {
486 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 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 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
539pub 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 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
571pub 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 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
606pub 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 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 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
659pub 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
694pub 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
726pub 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
772pub 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
809pub 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
846pub 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 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
945pub fn color_escape(color: &&&String) -> String {
947 remove_tags(
948 &color
949 .replace(";", "")
950 .replace("<", "<")
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
960pub 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("<", "<")
967 .replace(">", ">")
968 .replace("&", "&")
969 .replace("</script>", "</not-script")
970}
971
972pub fn clean_metadata(metadata: &ProfileMetadata) -> String {
974 remove_tags(&serde_json::to_string(&clean_metadata_raw(metadata)).unwrap())
975}
976
977pub fn clean_metadata_raw(metadata: &ProfileMetadata) -> ProfileMetadata {
979 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("<", "<")
988 .replace(">", ">")
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 metadata
998}
999
1000pub 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 .replace("\u{0010}", "")
1006 .replace("\u{0011}", "")
1007 .replace("\u{0012}", "")
1008 .replace("\u{0013}", "")
1009 .replace("\u{0014}", "")
1010}
1011
1012pub fn clean_metadata_short_raw(metadata: &ProfileMetadata) -> ProfileMetadata {
1014 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("<", "<")
1023 .replace(">", ">")
1024 .replace("<style>", "")
1025 .replace("</style>", ""),
1026 );
1027 }
1028
1029 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
1049pub 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 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
1165pub 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
1182pub 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 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 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
1278pub 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 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 relationships.insert(question.0.author.id.clone(), RelationshipStatus::Friends);
1331 continue;
1332 }
1333
1334 if question.0.author.id == auth_user.id {
1335 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 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
1386pub 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 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 relationships.insert(question.0.author.id.clone(), RelationshipStatus::Friends);
1436 continue;
1437 }
1438
1439 if question.0.author.id == auth_user.id {
1440 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 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
1498pub 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 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 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 &props.profile
1540 } else {
1541 &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
1583pub 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 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 return Html(DatabaseError::NotAllowed.to_html(database));
1609 }
1610
1611 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
1647pub 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 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 return Html(DatabaseError::NotAllowed.to_html(database));
1674 }
1675
1676 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
1716pub 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 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 return Html(DatabaseError::NotAllowed.to_html(database));
1739 }
1740
1741 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
1774pub 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
1803pub 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 .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)) .route("/inbox/audit", get(audit_log_request)) .route("/inbox/audit/ipbans", get(ipbans_request)) .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 .route("/@{username}/_app/warning", get(profile::warning_request))
1831 .route("/@{username}/mod", get(profile::mod_request)) .route("/@{username}/questions", get(profile::questions_request))
1833 .route("/@{username}/questions/inbox", get(profile::inbox_request)) .route(
1835 "/@{username}/questions/outbox",
1836 get(profile::outbox_request),
1837 ) .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 .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 .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 .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 .route("/login", get(login_request))
1891 .route("/sign_up", get(sign_up_request))
1892 .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 .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 .with_state(database)
1940}