Browse Source

Major Raito-FE overhaul

pull/50/head
Toromino 2 weeks ago
parent
commit
df8e703f5e

+ 4
- 2
README.md View File

@@ -6,13 +6,15 @@ written in Rust, utilizes Rocket as it's web framework and Diesel as it's databa
6 6
 
7 7
 The project's objective is to provide a highly customizable multi-protocol social networking server. Currently supported is the commonly used [ActivityPub](https://activitypub.rocks) protocol.
8 8
 
9
-Furthermore, Kibou implements [Mastodon's REST API](https://docs.joinmastodon.org/api). This means that all applications for [Mastodon](https://joinmastodon.org) should also work with Kibou.
9
+Furthermore Kibou implements [Mastodon's REST API](https://docs.joinmastodon.org/api). This means that all applications for [Mastodon](https://joinmastodon.org) should also work with Kibou.
10
+
11
+Kibou ships with it's own user interface called Raito-FE. In it's standard configuration it's completely based on static components and does not use any JavaScript. Although dynamic components (such as automatically refreshing timelines and dynamic routing) can optionally be enabled. A `minimal mode` can also be enabled in it's settings which reduces network traffic and only shows Raito-FE's core components.
10 12
 
11 13
 **Notice**: Kibou is *not* considered stable yet. This project's development is still
12 14
 work-in-progress. Using the development branch is not recommended for production usage.
13 15
 
14 16
 ## Federation with other software
15
-Federation is known to work with [Pleroma](https://pleroma.social), [Misskey](https://joinmisskey.github.io) and [Mastodon](https://joinmastodon.org) which are also the main projects being tested against. But other implementations should work as well.
17
+Federation is known to work with [Pleroma](https://pleroma.social), [Misskey](https://joinmisskey.github.io) and [Mastodon](https://joinmastodon.org) which are also the main projects being tested against. But federation with other software should work as well.
16 18
 
17 19
 ## Get in touch
18 20
 Join the IRC channel `#kibou` on freenode.net

+ 6
- 6
src/activitypub/activity.rs View File

@@ -54,7 +54,7 @@ pub struct Tag {
54 54
     pub name: String,
55 55
 }
56 56
 
57
-pub fn get_activity_json_by_id(id: String) -> serde_json::Value {
57
+pub fn get_activity_json_by_id(id: &str) -> serde_json::Value {
58 58
     let database = database::establish_connection();
59 59
     let activity_id = format!(
60 60
         "{}://{}/activities/{}",
@@ -69,7 +69,7 @@ pub fn get_activity_json_by_id(id: String) -> serde_json::Value {
69 69
     }
70 70
 }
71 71
 
72
-pub fn get_object_json_by_id(id: String) -> serde_json::Value {
72
+pub fn get_object_json_by_id(id: &str) -> serde_json::Value {
73 73
     let database = database::establish_connection();
74 74
     let object_id = format!(
75 75
         "{}://{}/objects/{}",
@@ -89,12 +89,12 @@ pub fn serialize_from_internal_activity(activity: activity::Activity) -> Activit
89 89
 }
90 90
 
91 91
 pub fn create_internal_activity(
92
-    json_activity: serde_json::Value,
93
-    actor_uri: String,
92
+    json_activity: &serde_json::Value,
93
+    actor_uri: &str,
94 94
 ) -> activity::Activity {
95 95
     activity::Activity {
96 96
         id: 0,
97
-        data: json_activity,
98
-        actor: actor_uri,
97
+        data: json_activity.clone().to_owned(),
98
+        actor: actor_uri.to_string(),
99 99
     }
100 100
 }

+ 1
- 1
src/activitypub/actor.rs View File

@@ -88,7 +88,7 @@ pub fn remove_follow(account: &str, source: &str) {
88 88
 // [TODO]
89 89
 pub fn refresh() {}
90 90
 
91
-pub fn get_json_by_preferred_username(preferred_username: String) -> serde_json::Value {
91
+pub fn get_json_by_preferred_username(preferred_username: &str) -> serde_json::Value {
92 92
     let database = database::establish_connection();
93 93
 
94 94
     match actor::get_local_actor_by_preferred_username(&database, preferred_username) {

+ 8
- 17
src/activitypub/controller.rs View File

@@ -268,7 +268,7 @@ fn activity_build(
268 268
 
269 269
     insert_activity(
270 270
         &database,
271
-        create_internal_activity(serde_json::json!(&new_activity), new_activity.actor.clone()),
271
+        create_internal_activity(&serde_json::json!(&new_activity), &new_activity.actor),
272 272
     );
273 273
     new_activity
274 274
 }
@@ -361,27 +361,21 @@ fn handle_activity(activity: serde_json::Value) {
361 361
                             Err(e) => eprintln!("{}", e),
362 362
                         }
363 363
 
364
-                        insert_activity(
365
-                            &database,
366
-                            create_internal_activity(activity.clone(), actor.clone()),
367
-                        );
364
+                        insert_activity(&database, create_internal_activity(&activity, &actor));
368 365
                     }
369 366
                     &_ => (),
370 367
                 },
371 368
                 Err(e) => eprintln!("Unknown object mentioned in `Accept` activity {}", e),
372 369
             }
373 370
 
374
-            insert_activity(
375
-                &database,
376
-                create_internal_activity(activity.to_owned(), actor),
377
-            );
371
+            insert_activity(&database, create_internal_activity(&activity, &actor));
378 372
         }
379 373
         Some("Announce") => {
380 374
             let object_id = activity["object"].as_str().unwrap().to_string();
381 375
             thread::spawn(move || {
382 376
                 fetch_object_by_id(object_id);
383 377
             });
384
-            insert_activity(&database, create_internal_activity(activity, actor));
378
+            insert_activity(&database, create_internal_activity(&activity, &actor));
385 379
         }
386 380
         Some("Create") => {
387 381
             if activity["object"].get("inReplyTo").is_some() {
@@ -396,7 +390,7 @@ fn handle_activity(activity: serde_json::Value) {
396 390
                 }
397 391
             }
398 392
 
399
-            insert_activity(&database, create_internal_activity(activity, actor));
393
+            insert_activity(&database, create_internal_activity(&activity, &actor));
400 394
         }
401 395
         Some("Follow") => {
402 396
             let remote_account = get_actor_by_uri(&database, &actor).unwrap();
@@ -446,14 +440,14 @@ fn handle_activity(activity: serde_json::Value) {
446 440
                 Err(_) => (),
447 441
             }
448 442
 
449
-            insert_activity(&database, create_internal_activity(activity, actor));
443
+            insert_activity(&database, create_internal_activity(&activity, &actor));
450 444
         }
451 445
         Some("Like") => {
452 446
             let object_id = activity["object"].as_str().unwrap().to_string();
453 447
             thread::spawn(move || {
454 448
                 fetch_object_by_id(object_id);
455 449
             });
456
-            insert_activity(&database, create_internal_activity(activity, actor));
450
+            insert_activity(&database, create_internal_activity(&activity, &actor));
457 451
         }
458 452
         Some("Undo") => {
459 453
             let remote_account = get_actor_by_uri(&database, &actor).unwrap();
@@ -473,10 +467,7 @@ fn handle_activity(activity: serde_json::Value) {
473 467
                         Err(_) => (),
474 468
                     }
475 469
 
476
-                    insert_activity(
477
-                        &database,
478
-                        create_internal_activity(activity.clone(), actor.clone()),
479
-                    );
470
+                    insert_activity(&database, create_internal_activity(&activity, &actor));
480 471
                 }
481 472
                 &_ => (),
482 473
             }

+ 3
- 3
src/activitypub/routes.rs View File

@@ -8,12 +8,12 @@ use serde_json;
8 8
 
9 9
 #[get("/activities/<id>")]
10 10
 pub fn activity(media_type: ActivitypubMediatype, id: String) -> ActivitystreamsResponse {
11
-    ActivitystreamsResponse(ap_activity::get_activity_json_by_id(id).to_string())
11
+    ActivitystreamsResponse(ap_activity::get_activity_json_by_id(&id).to_string())
12 12
 }
13 13
 
14 14
 #[get("/actors/<handle>")]
15 15
 pub fn actor(media_type: ActivitypubMediatype, handle: String) -> ActivitystreamsResponse {
16
-    ActivitystreamsResponse(ap_actor::get_json_by_preferred_username(handle).to_string())
16
+    ActivitystreamsResponse(ap_actor::get_json_by_preferred_username(&handle).to_string())
17 17
 }
18 18
 
19 19
 #[post("/actors/<id>/inbox", data = "<activity>")]
@@ -34,5 +34,5 @@ pub fn inbox(activity: String, _signature: HTTPSignature) {
34 34
 
35 35
 #[get("/objects/<id>")]
36 36
 pub fn object(media_type: ActivitypubMediatype, id: String) -> ActivitystreamsResponse {
37
-    ActivitystreamsResponse(ap_activity::get_object_json_by_id(id).to_string())
37
+    ActivitystreamsResponse(ap_activity::get_object_json_by_id(&id).to_string())
38 38
 }

+ 2
- 2
src/actor.rs View File

@@ -250,7 +250,7 @@ pub fn update_followers(db_connection: &PgConnection, actor: &mut Actor) {
250 250
 
251 251
 pub fn get_actor_by_acct(
252 252
     db_connection: &PgConnection,
253
-    acct: String,
253
+    acct: &str,
254 254
 ) -> Result<Actor, diesel::result::Error> {
255 255
     if acct.contains("@") {
256 256
         let acct_split = acct.split('@');
@@ -335,7 +335,7 @@ pub fn get_actor_followees(
335 335
 /// - get_local_actor_by_preferred_username()
336 336
 pub fn get_local_actor_by_preferred_username(
337 337
     db_connection: &PgConnection,
338
-    _preferred_username: String,
338
+    _preferred_username: &str,
339 339
 ) -> Result<Actor, diesel::result::Error> {
340 340
     match actors
341 341
         .filter(preferred_username.eq(_preferred_username))

+ 3
- 1
src/env.rs View File

@@ -6,7 +6,9 @@ pub fn get_value(key: String) -> String {
6 6
     set_default_config_values(&mut config);
7 7
 
8 8
     // TODO: Find config file based on ROCKET_ENV
9
-    config.merge(config::File::with_name("env.development.toml"));
9
+    config
10
+        .merge(config::File::with_name("env.development.toml"))
11
+        .expect("Environment config not found!");
10 12
 
11 13
     if config.get_str(&key).is_ok() {
12 14
         config.get_str(&key).ok().unwrap()

+ 5
- 3
src/kibou_api/mod.rs View File

@@ -44,7 +44,7 @@ pub fn status_build(
44 44
     mut content: String,
45 45
     visibility: &str,
46 46
     in_reply_to: Option<String>,
47
-) -> String {
47
+) -> i64 {
48 48
     let database = database::establish_connection();
49 49
     let serialized_actor: Actor = get_actor_by_uri(&database, &actor).unwrap();
50 50
 
@@ -111,7 +111,9 @@ pub fn status_build(
111 111
         inboxes,
112 112
     );
113 113
 
114
-    return activitypub_activity_create.id;
114
+    return get_ap_activity_by_id(&database, &activitypub_activity_create.id)
115
+        .unwrap()
116
+        .id;
115 117
 }
116 118
 
117 119
 pub fn unfollow(actor: String, object: String) {
@@ -202,7 +204,7 @@ fn parse_mentions(content: String) -> (Vec<String>, Vec<String>, Vec<serde_json:
202 204
     for mention in acct_regex.captures_iter(&content) {
203 205
         match get_actor_by_acct(
204 206
             &database,
205
-            mention.get(0).unwrap().as_str().to_string().split_off(1),
207
+            &mention.get(0).unwrap().as_str().to_string().split_off(1),
206 208
         ) {
207 209
             Ok(actor) => {
208 210
                 let tag: Tag = Tag {

+ 0
- 2
src/lib.rs View File

@@ -8,8 +8,6 @@ extern crate cached;
8 8
 extern crate chrono;
9 9
 #[macro_use]
10 10
 extern crate diesel;
11
-#[macro_use]
12
-extern crate lazy_static;
13 11
 extern crate openssl;
14 12
 extern crate pem;
15 13
 extern crate regex;

+ 58
- 10
src/mastodon_api/controller.rs View File

@@ -4,16 +4,19 @@ use activitypub;
4 4
 use actor;
5 5
 use actor::get_actor_by_id;
6 6
 use actor::get_actor_by_uri;
7
+use chrono::Utc;
7 8
 use database;
8 9
 use diesel::PgConnection;
9 10
 use env;
10 11
 use kibou_api;
11 12
 use mastodon_api::{
12
-    Account, Attachment, HomeTimeline, PublicTimeline, Relationship, Source, Status, StatusForm,
13
+    Account, Attachment, HomeTimeline, PublicTimeline, RegistrationForm, Relationship, Source,
14
+    Status, StatusForm,
13 15
 };
14 16
 use oauth;
15 17
 use oauth::application::Application as OAuthApplication;
16
-use oauth::token::verify_token;
18
+use oauth::token::{verify_token, Token};
19
+use regex::Regex;
17 20
 use rocket_contrib::json;
18 21
 use rocket_contrib::json::JsonValue;
19 22
 use timeline;
@@ -32,7 +35,7 @@ pub fn account_by_oauth_token(token: String) -> Result<Account, diesel::result::
32 35
     let database = database::establish_connection();
33 36
 
34 37
     match verify_token(&database, token) {
35
-        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, token.actor) {
38
+        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, &token.actor) {
36 39
             Ok(actor) => Ok(serialize_account(actor, true)),
37 40
             Err(e) => Err(e),
38 41
         },
@@ -44,7 +47,7 @@ pub fn account_json_by_oauth_token(token: String) -> JsonValue {
44 47
     let database = database::establish_connection();
45 48
 
46 49
     match verify_token(&database, token) {
47
-        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, token.actor) {
50
+        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, &token.actor) {
48 51
             Ok(actor) => json!(serialize_account(actor, true)),
49 52
             Err(_) => json!({"error": "No user is associated to this token!"}),
50 53
         },
@@ -52,6 +55,51 @@ pub fn account_json_by_oauth_token(token: String) -> JsonValue {
52 55
     }
53 56
 }
54 57
 
58
+pub fn account_create_json(form: &RegistrationForm) -> JsonValue {
59
+    match account_create(form) {
60
+        Some(token) => serde_json::to_value(token).unwrap().into(),
61
+        None => json!({"error": "Account could not be created!"}),
62
+    }
63
+}
64
+
65
+pub fn account_create(form: &RegistrationForm) -> Option<Token> {
66
+    let email_regex = Regex::new(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$").unwrap();
67
+    let username_regex = Regex::new(r"^[A-Za-z0-9_]{1,32}$").unwrap();
68
+    
69
+    if username_regex.is_match(&form.username) && email_regex.is_match(&form.email) {
70
+        let database = database::establish_connection();
71
+        let mut new_actor = actor::Actor {
72
+            id: 0,
73
+            email: Some(form.email.to_string()),
74
+            password: Some(form.password.to_string()),
75
+            actor_uri: format!(
76
+                "{base_scheme}://{base_domain}/actors/{username}",
77
+                base_scheme = env::get_value(String::from("endpoint.base_scheme")),
78
+                base_domain = env::get_value(String::from("endpoint.base_domain")),
79
+                username = form.username
80
+            ),
81
+            username: Some(form.username.to_string()),
82
+            preferred_username: form.username.to_string(),
83
+            summary: None,
84
+            followers: serde_json::json!({"activitypub": []}),
85
+            inbox: None,
86
+            icon: None,
87
+            local: true,
88
+            keys: serde_json::json!({}),
89
+            created: Utc::now().naive_utc(),
90
+        };
91
+
92
+        actor::create_actor(&database, &mut new_actor);
93
+
94
+        match actor::get_local_actor_by_preferred_username(&database, &form.username) {
95
+            Ok(actor) => Some(oauth::token::create(&form.username)),
96
+            Err(_) => None,
97
+        }
98
+    } else {
99
+        return None;
100
+    }
101
+}
102
+
55 103
 pub fn account_statuses_json_by_id(
56 104
     id: i64,
57 105
     max_id: Option<i64>,
@@ -109,7 +157,7 @@ pub fn follow(token: String, target_id: i64) -> JsonValue {
109 157
     let database = database::establish_connection();
110 158
 
111 159
     match verify_token(&database, token) {
112
-        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, token.actor) {
160
+        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, &token.actor) {
113 161
             Ok(actor) => {
114 162
                 let followee = actor::get_actor_by_id(&database, target_id).unwrap();
115 163
 
@@ -148,7 +196,7 @@ pub fn relationships_by_token(token: String, ids: Vec<i64>) -> JsonValue {
148 196
     let database = database::establish_connection();
149 197
 
150 198
     match verify_token(&database, token) {
151
-        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, token.actor) {
199
+        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, &token.actor) {
152 200
             Ok(actor) => {
153 201
                 let mut relationships: Vec<Relationship> = Vec::new();
154 202
                 let activitypub_followers: Vec<serde_json::Value> =
@@ -285,7 +333,7 @@ pub fn status_post(form: StatusForm, token: String) -> JsonValue {
285 333
     let database = database::establish_connection();
286 334
 
287 335
     match verify_token(&database, token) {
288
-        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, token.actor) {
336
+        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, &token.actor) {
289 337
             Ok(actor) => {
290 338
                 let status_id = kibou_api::status_build(
291 339
                     actor.actor_uri,
@@ -294,7 +342,7 @@ pub fn status_post(form: StatusForm, token: String) -> JsonValue {
294 342
                     form.in_reply_to_id,
295 343
                 );
296 344
 
297
-                return json!(status_cached_by_id(status_id.parse::<i64>().unwrap()));
345
+                return json!(status_cached_by_id(status_id));
298 346
             }
299 347
             Err(_) => json!({"error": "Account not found"}),
300 348
         },
@@ -306,7 +354,7 @@ pub fn unfollow(token: String, target_id: i64) -> JsonValue {
306 354
     let database = database::establish_connection();
307 355
 
308 356
     match verify_token(&database, token) {
309
-        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, token.actor) {
357
+        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, &token.actor) {
310 358
             Ok(actor) => {
311 359
                 let followee = actor::get_actor_by_id(&database, target_id).unwrap();
312 360
 
@@ -381,7 +429,7 @@ fn home_timeline(
381 429
     let database = database::establish_connection();
382 430
 
383 431
     match verify_token(&database, token) {
384
-        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, token.actor) {
432
+        Ok(token) => match actor::get_local_actor_by_preferred_username(&database, &token.actor) {
385 433
             Ok(actor) => {
386 434
                 match get_home_timeline(
387 435
                     &database,

+ 13
- 3
src/mastodon_api/mod.rs View File

@@ -9,8 +9,6 @@ use rocket::Outcome;
9 9
 use rocket_contrib::json;
10 10
 use rocket_contrib::json::JsonValue;
11 11
 use serde::{Deserialize, Serialize};
12
-use std::collections::hash_map::HashMap;
13
-use std::sync::Mutex;
14 12
 
15 13
 #[derive(Serialize, Deserialize)]
16 14
 pub struct Account {
@@ -60,7 +58,7 @@ pub struct Attachment {
60 58
 }
61 59
 
62 60
 #[derive(Debug)]
63
-pub struct AuthorizationHeader(String);
61
+pub struct AuthorizationHeader(pub String);
64 62
 
65 63
 #[derive(Serialize, Deserialize)]
66 64
 pub struct Emoji {
@@ -112,6 +110,18 @@ pub struct PublicTimeline {
112 110
     pub limit: Option<i64>,
113 111
 }
114 112
 
113
+#[derive(FromForm)]
114
+pub struct RegistrationForm {
115
+    // Properties acctording to
116
+    // - https://docs.joinmastodon.org/api/rest/accounts/#post-api-v1-accounts
117
+    pub username: String,
118
+    pub email: String,
119
+    pub password: String,
120
+    // Optional values in Kibou, as they're not used by the backend (yet?)
121
+    pub agreement: Option<String>,
122
+    pub locale: Option<String>,
123
+}
124
+
115 125
 #[derive(Serialize, Deserialize)]
116 126
 pub struct Relationship {
117 127
     // Properties according to

+ 3
- 1
src/oauth/application.rs View File

@@ -53,7 +53,9 @@ pub fn get_application_by_client_id(
53 53
 
54 54
 pub fn create(db_connection: &PgConnection, mut app: Application) -> Application {
55 55
     let mut big_num: BigNum = BigNum::new().unwrap();
56
-    big_num.rand(256, MsbOption::MAYBE_ZERO, true);
56
+    big_num
57
+        .rand(256, MsbOption::MAYBE_ZERO, true)
58
+        .expect("Error generating client secret");
57 59
     app.client_id = Uuid::new_v4().to_string();
58 60
     app.client_secret = big_num.to_hex_str().unwrap().to_string();
59 61
 

+ 6
- 9
src/oauth/authorization.rs View File

@@ -1,5 +1,6 @@
1 1
 use actor;
2 2
 use chrono::prelude::*;
3
+use chrono::Duration;
3 4
 use chrono::NaiveDateTime;
4 5
 use database;
5 6
 use database::models::QueryOAuthAuthorization;
@@ -106,20 +107,16 @@ pub fn authorize_application(_actor: String, application_id: i64) -> String {
106 107
     let db_connection = database::establish_connection();
107 108
     let mut hex_num: BigNum = BigNum::new().unwrap();
108 109
     let utc_time: chrono::DateTime<Utc> = Utc::now();
109
-    hex_num.rand(256, MsbOption::MAYBE_ZERO, true);
110
+    let expiration_date: chrono::DateTime<Utc> = utc_time + Duration::days(30);
111
+    hex_num
112
+        .rand(256, MsbOption::MAYBE_ZERO, true)
113
+        .expect("Error generating authorization code");
110 114
 
111 115
     let new_authorization = Authorization {
112 116
         application: application_id,
113 117
         actor: _actor,
114 118
         code: hex_num.to_string(),
115
-        valid_until: chrono::NaiveDate::from_ymd(
116
-            utc_time.year(),
117
-            utc_time.month() + 1,
118
-            utc_time.day(),
119
-        )
120
-        .and_hms(utc_time.hour(), utc_time.minute(), utc_time.second())
121
-        .timestamp()
122
-        .to_string(),
119
+        valid_until: expiration_date.timestamp().to_string(),
123 120
     };
124 121
 
125 122
     insert(&db_connection, &new_authorization);

+ 12
- 13
src/oauth/token.rs View File

@@ -1,4 +1,5 @@
1 1
 use chrono::prelude::*;
2
+use chrono::Duration;
2 3
 use chrono::NaiveDateTime;
3 4
 use database;
4 5
 use database::models::QueryOauthToken;
@@ -65,7 +66,7 @@ pub fn get_token(form: TokenForm) -> JsonValue {
65 66
 
66 67
     if verify_credentials(&db_connection, form.client_id, form.client_secret) {
67 68
         match get_authorization_by_code(&db_connection, form.code) {
68
-            Ok(authorization) => json!(create(authorization.actor)),
69
+            Ok(authorization) => json!(create(&authorization.actor)),
69 70
             Err(_) => json!({"Error": "OAuth authorization code is invalid."}),
70 71
         }
71 72
     } else {
@@ -73,28 +74,26 @@ pub fn get_token(form: TokenForm) -> JsonValue {
73 74
     }
74 75
 }
75 76
 
76
-pub fn create(actor_username: String) -> Token {
77
+pub fn create(actor_username: &str) -> Token {
77 78
     let db_connection = database::establish_connection();
78 79
     let mut access_token_num: BigNum = BigNum::new().unwrap();
79 80
     let mut refresh_token_num: BigNum = BigNum::new().unwrap();
80 81
     let utc_time: chrono::DateTime<Utc> = Utc::now();
82
+    let expiration_date: chrono::DateTime<Utc> = utc_time + Duration::days(30);
81 83
 
82
-    access_token_num.rand(256, MsbOption::MAYBE_ZERO, true);
83
-    refresh_token_num.rand(256, MsbOption::MAYBE_ZERO, true);
84
+    access_token_num
85
+        .rand(256, MsbOption::MAYBE_ZERO, true)
86
+        .expect("Error generating access token");
87
+    refresh_token_num
88
+        .rand(256, MsbOption::MAYBE_ZERO, true)
89
+        .expect("Error generating refresh token");
84 90
 
85 91
     let new_token: Token = Token {
86 92
         access_token: access_token_num.to_hex_str().unwrap().to_string(),
87 93
         refresh_token: refresh_token_num.to_hex_str().unwrap().to_string(),
88
-        actor: actor_username,
94
+        actor: actor_username.to_string(),
89 95
         token_type: String::from("Bearer"),
90
-        valid_until: chrono::NaiveDate::from_ymd(
91
-            utc_time.year(),
92
-            utc_time.month() + 1,
93
-            utc_time.day(),
94
-        )
95
-        .and_hms(utc_time.hour(), utc_time.minute(), utc_time.second())
96
-        .timestamp()
97
-        .to_string(),
96
+        valid_until: expiration_date.timestamp().to_string(),
98 97
         scope: String::from("read write follow"),
99 98
     };
100 99
 

+ 0
- 52
src/raito_fe/account.rs View File

@@ -1,52 +0,0 @@
1
-use actor;
2
-use database;
3
-use raito_fe;
4
-use raito_fe::timeline::get_user_timeline;
5
-use rocket_contrib::templates::Template;
6
-use std::collections::HashMap;
7
-
8
-pub fn get_account_by_local_id(id: String) -> Template {
9
-    let mut template_parameters = HashMap::<String, String>::new();
10
-    template_parameters.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
11
-
12
-    match raito_fe::api_controller::get_account(id) {
13
-        Ok(account) => {
14
-            template_parameters.insert(String::from("account_acct"), account.acct);
15
-            template_parameters.insert(String::from("account_display_name"), account.display_name);
16
-            template_parameters.insert(String::from("account_avatar"), account.avatar);
17
-            template_parameters.insert(
18
-                String::from("account_followers_count"),
19
-                account.followers_count.to_string(),
20
-            );
21
-            template_parameters.insert(
22
-                String::from("account_following_count"),
23
-                account.following_count.to_string(),
24
-            );
25
-            template_parameters.insert(String::from("account_header"), account.header);
26
-            template_parameters.insert(String::from("account_note"), account.note);
27
-            template_parameters.insert(
28
-                String::from("account_statuses_count"),
29
-                account.statuses_count.to_string(),
30
-            );
31
-
32
-            template_parameters.insert(
33
-                String::from("account_timeline"),
34
-                get_user_timeline(account.id),
35
-            );
36
-
37
-            return Template::render("raito_fe/account", template_parameters);
38
-        }
39
-        Err(_) => Template::render("raito_fe/index", template_parameters),
40
-    }
41
-}
42
-
43
-pub fn get_account_by_username(username: String) -> Template {
44
-    let database = database::establish_connection();
45
-
46
-    let mut template_parameters = HashMap::<String, String>::new();
47
-    template_parameters.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
48
-    match actor::get_local_actor_by_preferred_username(&database, username) {
49
-        Ok(actor) => get_account_by_local_id(actor.id.to_string()),
50
-        Err(_) => Template::render("raito_fe/index", template_parameters),
51
-    }
52
-}

+ 69
- 7
src/raito_fe/api_controller.rs View File

@@ -1,10 +1,12 @@
1
-use mastodon_api::routes;
2
-use mastodon_api::Account;
3
-use mastodon_api::Status;
4
-use raito_fe::BYPASS_API;
5
-use raito_fe::MASTODON_API_BASE_URI;
6
-use reqwest::header::HeaderValue;
7
-use reqwest::header::ACCEPT;
1
+use actor;
2
+use database;
3
+use mastodon_api::{
4
+    controller, routes, Account, AuthorizationHeader, RegistrationForm, Status, StatusForm,
5
+};
6
+use oauth;
7
+use raito_fe::{LoginForm, BYPASS_API, MASTODON_API_BASE_URI};
8
+use reqwest::header::{HeaderValue, ACCEPT};
9
+use rocket::request::LenientForm;
8 10
 
9 11
 pub fn get_account(id: String) -> Result<Account, ()> {
10 12
     if unsafe { BYPASS_API } == &true {
@@ -70,6 +72,26 @@ pub fn get_status_context(id: String) -> Result<serde_json::Value, ()> {
70 72
     }
71 73
 }
72 74
 
75
+pub fn home_timeline(token: &str) -> Result<Vec<Status>, ()> {
76
+    if unsafe { BYPASS_API } == &true {
77
+        match serde_json::from_str(
78
+            &routes::home_timeline(
79
+                None,
80
+                None,
81
+                None,
82
+                None,
83
+                AuthorizationHeader(token.to_string()),
84
+            )
85
+            .to_string(),
86
+        ) {
87
+            Ok(timeline) => Ok(timeline),
88
+            Err(_) => Err(()),
89
+        }
90
+    } else {
91
+        Err(())
92
+    }
93
+}
94
+
73 95
 pub fn get_public_timeline(local: bool) -> Result<Vec<Status>, ()> {
74 96
     if unsafe { BYPASS_API } == &true {
75 97
         match serde_json::from_str(
@@ -127,6 +149,46 @@ pub fn get_user_timeline(id: String) -> Result<Vec<Status>, ()> {
127 149
     }
128 150
 }
129 151
 
152
+// This approach is not optimal, as it skips the internal OAuth flow and should definitely be
153
+// reworked. From a security perspective, this approach is safe, as the backend has no reason not
154
+// to trust the internal front-end. On the other hand, this approach does not work if Raito-FE
155
+// is run in standalone.
156
+//
157
+// TODO: Rework
158
+pub fn login(form: LenientForm<LoginForm>) -> Option<String> {
159
+    let db_connection = database::establish_connection();
160
+
161
+    if unsafe { BYPASS_API } == &true {
162
+        let form = form.into_inner();
163
+        match actor::authorize(&db_connection, &form.username, form.password) {
164
+            Ok(true) => Some(oauth::token::create(&form.username).access_token),
165
+            Ok(false) => None,
166
+            Err(_) => None,
167
+        }
168
+    } else {
169
+        None
170
+    }
171
+}
172
+
173
+pub fn post_status(form: LenientForm<StatusForm>, token: &str) {
174
+    if unsafe { BYPASS_API } == &true {
175
+        routes::status_post(form, AuthorizationHeader(format!("Bearer: {}", token)));
176
+    }
177
+}
178
+
179
+// TODO: Rework
180
+// (same as in line 129)
181
+pub fn register(form: LenientForm<RegistrationForm>) -> Option<String> {
182
+    if unsafe { BYPASS_API } == &true {
183
+        match controller::account_create(&form.into_inner()) {
184
+            Some(token) => Some(token.access_token),
185
+            None => None,
186
+        }
187
+    } else {
188
+        None
189
+    }
190
+}
191
+
130 192
 fn fetch_object(url: &str) -> Result<String, reqwest::Error> {
131 193
     let client = reqwest::Client::new();
132 194
     let request = client

+ 85
- 3
src/raito_fe/mod.rs View File

@@ -1,28 +1,110 @@
1
-pub mod account;
2 1
 pub mod api_controller;
2
+pub mod renderer;
3 3
 pub mod routes;
4
-pub mod status;
5
-pub mod timeline;
6 4
 
5
+use env;
6
+use mastodon_api;
7
+use rocket::outcome::Outcome;
8
+use rocket::request::{self, FromRequest, Request};
9
+use std::collections::hash_map::IntoIter;
10
+use std::collections::HashMap;
7 11
 use std::fs::File;
8 12
 use std::io::prelude::*;
9 13
 
10 14
 pub static mut BYPASS_API: &'static bool = &false;
11 15
 pub static mut MASTODON_API_BASE_URI: &'static str = "127.0.0.1";
12 16
 
17
+pub struct Authentication {
18
+    pub account: Option<mastodon_api::Account>,
19
+    pub token: Option<String>,
20
+}
21
+
22
+#[derive(FromForm)]
23
+pub struct LoginForm {
24
+    pub username: String,
25
+    pub password: String,
26
+}
27
+
28
+#[derive(Debug, Clone)]
29
+pub struct LocalConfiguration(HashMap<String, String>);
30
+
31
+impl<'a, 'r> FromRequest<'a, 'r> for Authentication {
32
+    type Error = ();
33
+
34
+    fn from_request(request: &'a Request<'r>) -> request::Outcome<Authentication, ()> {
35
+        match request.cookies().get_private("oauth_token") {
36
+            Some(token) => {
37
+                match mastodon_api::controller::account_by_oauth_token(token.value().to_string()) {
38
+                    Ok(mastodon_api_account) => Outcome::Success(Authentication {
39
+                        account: Some(mastodon_api_account),
40
+                        token: Some(token.value().to_string()),
41
+                    }),
42
+                    Err(_) => Outcome::Success(Authentication {
43
+                        account: None,
44
+                        token: None,
45
+                    }),
46
+                }
47
+            }
48
+            None => Outcome::Success(Authentication {
49
+                account: None,
50
+                token: None,
51
+            }),
52
+        }
53
+    }
54
+}
55
+
56
+impl<'a, 'r> FromRequest<'a, 'r> for LocalConfiguration {
57
+    type Error = ();
58
+
59
+    fn from_request(request: &'a Request<'r>) -> request::Outcome<LocalConfiguration, ()> {
60
+        let mut new_config = HashMap::<String, String>::new();
61
+        new_config.insert("javascript_enabled".to_string(), "false".to_string());
62
+        new_config.insert("mastodon_api_base_uri".to_string(), unsafe {
63
+            if BYPASS_API == &true {
64
+                format!(
65
+                    "{base_scheme}://{base_domain}",
66
+                    base_scheme = env::get_value(String::from("endpoint.base_scheme")),
67
+                    base_domain = env::get_value(String::from("endpoint.base_domain"))
68
+                )
69
+            } else {
70
+                MASTODON_API_BASE_URI.to_string()
71
+            }
72
+        });
73
+        new_config.insert("minimalmode_enabled".to_string(), "false".to_string());
74
+        Outcome::Success(LocalConfiguration(new_config))
75
+    }
76
+}
77
+
78
+impl IntoIterator for LocalConfiguration {
79
+    type Item = (String, String);
80
+    type IntoIter = IntoIter<String, String>;
81
+
82
+    fn into_iter(self) -> Self::IntoIter {
83
+        self.0.into_iter()
84
+    }
85
+}
86
+
13 87
 pub fn get_routes() -> Vec<rocket::Route> {
14 88
     routes![
15 89
         routes::about,
16 90
         routes::account,
17 91
         routes::actor,
18 92
         routes::global_timeline,
93
+        routes::home_timeline,
19 94
         routes::index,
95
+        routes::login,
96
+        routes::login_post,
20 97
         routes::object,
21 98
         routes::public_timeline,
99
+        routes::register,
100
+        routes::settings,
101
+        routes::status_compose,
102
+        routes::status_draft,
22 103
         routes::view_status
23 104
     ]
24 105
 }
25 106
 
107
+// This should only be a temporary solution, as it ought to be replace by actual theming
26 108
 pub fn get_stylesheet() -> String {
27 109
     let mut style = File::open("static/raito_fe/themes/raito_light.css").expect("theme not found");
28 110
 

+ 543
- 0
src/raito_fe/renderer.rs View File

@@ -0,0 +1,543 @@
1
+use activity;
2
+use actor;
3
+use chrono::prelude::*;
4
+use database;
5
+use env;
6
+use mastodon_api::{RegistrationForm, Status, StatusForm};
7
+use raito_fe::{self, Authentication, LocalConfiguration, LoginForm};
8
+use rocket::http::{Cookie, Cookies};
9
+use rocket::request::LenientForm;
10
+use rocket::Rocket;
11
+use rocket_contrib::templates::Template;
12
+use std::collections::HashMap;
13
+
14
+pub fn about(configuration: LocalConfiguration, authentication: Authentication) -> Template {
15
+    let mut context = HashMap::<String, String>::new();
16
+    context.extend(configuration);
17
+    context.extend(prepare_authentication_context(&authentication));
18
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
19
+    return Template::render("raito_fe/about", context);
20
+}
21
+
22
+pub fn account_by_local_id(
23
+    configuration: LocalConfiguration,
24
+    authentication: Authentication,
25
+    id: String,
26
+) -> Template {
27
+    let mut context = HashMap::<String, String>::new();
28
+    context.extend(configuration.clone());
29
+    context.extend(prepare_authentication_context(&authentication));
30
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
31
+
32
+    match raito_fe::api_controller::get_account(id) {
33
+        Ok(account) => {
34
+            context.insert(String::from("account_acct"), account.acct);
35
+            context.insert(String::from("account_display_name"), account.display_name);
36
+            context.insert(String::from("account_avatar"), account.avatar);
37
+            context.insert(
38
+                String::from("account_followers_count"),
39
+                account.followers_count.to_string(),
40
+            );
41
+            context.insert(
42
+                String::from("account_following_count"),
43
+                account.following_count.to_string(),
44
+            );
45
+            context.insert(String::from("account_header"), account.header);
46
+            context.insert(String::from("account_note"), account.note);
47
+            context.insert(
48
+                String::from("account_statuses_count"),
49
+                account.statuses_count.to_string(),
50
+            );
51
+
52
+            context.insert(
53
+                String::from("account_timeline"),
54
+                user_timeline(configuration, authentication, account.id),
55
+            );
56
+
57
+            return Template::render("raito_fe/account", context);
58
+        }
59
+        Err(_) => Template::render("raito_fe/index", context),
60
+    }
61
+}
62
+
63
+pub fn account_by_username(
64
+    configuration: LocalConfiguration,
65
+    authentication: Authentication,
66
+    username: String,
67
+) -> Template {
68
+    let database = database::establish_connection();
69
+
70
+    let mut context = HashMap::<String, String>::new();
71
+    context.extend(configuration.clone());
72
+    context.extend(prepare_authentication_context(&authentication));
73
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
74
+    match actor::get_local_actor_by_preferred_username(&database, &username) {
75
+        Ok(actor) => account_by_local_id(configuration, authentication, actor.id.to_string()),
76
+        Err(_) => Template::render("raito_fe/index", context),
77
+    }
78
+}
79
+
80
+pub fn compose(
81
+    configuration: LocalConfiguration,
82
+    authentication: Authentication,
83
+    in_reply_to: Option<i64>,
84
+) -> Template {
85
+    let mut context = HashMap::<String, String>::new();
86
+    context.extend(configuration);
87
+    context.extend(prepare_authentication_context(&authentication));
88
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
89
+
90
+    if authentication.account.is_none() {
91
+        return Template::render("raito_fe/infoscreen", context);
92
+    } else {
93
+        return Template::render("raito_fe/status_post", context);
94
+    }
95
+}
96
+
97
+pub fn compose_post(
98
+    configuration: LocalConfiguration,
99
+    authentication: Authentication,
100
+    form: LenientForm<StatusForm>,
101
+) -> Template {
102
+    let mut context = HashMap::<String, String>::new();
103
+    context.extend(configuration.clone());
104
+    context.extend(prepare_authentication_context(&authentication));
105
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
106
+
107
+    match &authentication.token {
108
+        Some(token) => {
109
+            raito_fe::api_controller::post_status(form, &token);
110
+            return home_timeline(configuration, authentication);
111
+        }
112
+        None => return Template::render("raito_fe/infoscreen", context),
113
+    }
114
+}
115
+
116
+pub fn conversation(
117
+    configuration: LocalConfiguration,
118
+    authentication: Authentication,
119
+    id: String,
120
+) -> Template {
121
+    let mut context = HashMap::<String, String>::new();
122
+    let rocket_renderer = rocket::ignite().attach(Template::fairing());
123
+
124
+    context.extend(configuration.clone());
125
+    context.extend(prepare_authentication_context(&authentication));
126
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
127
+
128
+    match raito_fe::api_controller::get_status(id.clone()) {
129
+        Ok(status) => {
130
+            let mut renderered_statuses: Vec<String> = vec![];
131
+            let mut parent_statuses: Vec<Status> = vec![];
132
+            let mut child_statuses: Vec<Status> = vec![];
133
+            let mut timeline_parameters = HashMap::<String, String>::new();
134
+
135
+            match raito_fe::api_controller::get_status_context(id) {
136
+                Ok(context) => {
137
+                    parent_statuses =
138
+                        serde_json::from_value(context["ancestors"].to_owned()).unwrap();
139
+                    child_statuses =
140
+                        serde_json::from_value(context["descendants"].to_owned()).unwrap();
141
+                }
142
+                Err(()) => (),
143
+            }
144
+
145
+            for parent in parent_statuses {
146
+                renderered_statuses.push(raw_status(
147
+                    configuration.clone(),
148
+                    &authentication,
149
+                    parent,
150
+                    &rocket_renderer,
151
+                ));
152
+            }
153
+            renderered_statuses.push(raw_status(
154
+                configuration.clone(),
155
+                &authentication,
156
+                status,
157
+                &rocket_renderer,
158
+            ));
159
+            for child in child_statuses {
160
+                renderered_statuses.push(raw_status(
161
+                    configuration.clone(),
162
+                    &authentication,
163
+                    child,
164
+                    &rocket_renderer,
165
+                ));
166
+            }
167
+            context.insert(String::from("timeline_name"), String::from("Conversation"));
168
+            timeline_parameters.insert(String::from("statuses"), renderered_statuses.join(""));
169
+            timeline_parameters.extend(configuration.clone());
170
+            timeline_parameters.extend(prepare_authentication_context(&authentication));
171
+            context.insert(
172
+                String::from("timeline"),
173
+                Template::show(
174
+                    &rocket_renderer,
175
+                    "raito_fe/components/timeline",
176
+                    timeline_parameters,
177
+                )
178
+                .unwrap(),
179
+            );
180
+
181
+            return Template::render("raito_fe/timeline_view", context);
182
+        }
183
+        Err(_) => Template::render("raito_fe/index", context),
184
+    }
185
+}
186
+// Note: This case only occurs if the Raito-FE is set as the main UI of Kibou
187
+pub fn conversation_by_uri(
188
+    configuration: LocalConfiguration,
189
+    authentication: Authentication,
190
+    id: String,
191
+) -> Template {
192
+    let database = database::establish_connection();
193
+    let mut context = HashMap::<String, String>::new();
194
+    let object_id = format!(
195
+        "{}://{}/objects/{}",
196
+        env::get_value(String::from("endpoint.base_scheme")),
197
+        env::get_value(String::from("endpoint.base_domain")),
198
+        id
199
+    );
200
+
201
+    context.extend(configuration.clone());
202
+    context.extend(prepare_authentication_context(&authentication));
203
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
204
+
205
+    match activity::get_ap_object_by_id(&database, &object_id) {
206
+        Ok(activity) => conversation(configuration, authentication, activity.id.to_string()),
207
+        Err(_) => Template::render("raito_fe/index", context),
208
+    }
209
+}
210
+
211
+pub fn home_timeline(
212
+    configuration: LocalConfiguration,
213
+    authentication: Authentication,
214
+) -> Template {
215
+    let mut context = HashMap::<String, String>::new();
216
+    let rocket_renderer = rocket::ignite().attach(Template::fairing());
217
+    let mut timeline_parameters = HashMap::<String, String>::new();
218
+
219
+    context.extend(configuration.clone());
220
+    context.extend(prepare_authentication_context(&authentication));
221
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
222
+
223
+    match &authentication.token {
224
+        Some(token) => {
225
+            if configuration.0.get("javascript_enabled").unwrap() == "true" {
226
+                context.insert(
227
+                    String::from("timeline"),
228
+                    Template::show(
229
+                        &rocket_renderer,
230
+                        "raito_fe/components/timeline",
231
+                        timeline_parameters,
232
+                    )
233
+                    .unwrap(),
234
+                );
235
+                return Template::render("raito_fe/timeline_view", context);
236
+            } else {
237
+                match raito_fe::api_controller::home_timeline(&format!("Bearer: {}", token)) {
238
+                    Ok(statuses) => {
239
+                        let mut renderered_statuses: Vec<String> = vec![];
240
+                        for status in statuses {
241
+                            renderered_statuses.push(raw_status(
242
+                                configuration.clone(),
243
+                                &authentication,
244
+                                status,
245
+                                &rocket_renderer,
246
+                            ));
247
+                        }
248
+
249
+                        context
250
+                            .insert(String::from("timeline_name"), String::from("Home Timeline"));
251
+                        timeline_parameters.extend(configuration.clone());
252
+
253
+                        timeline_parameters
254
+                            .insert(String::from("statuses"), renderered_statuses.join(""));
255
+                        context.insert(
256
+                            String::from("timeline"),
257
+                            Template::show(
258
+                                &rocket_renderer,
259
+                                "raito_fe/components/timeline",
260
+                                timeline_parameters,
261
+                            )
262
+                            .unwrap(),
263
+                        );
264
+
265
+                        return Template::render("raito_fe/timeline_view", context);
266
+                    }
267
+                    Err(_) => Template::render("raito_fe/index", context),
268
+                }
269
+            }
270
+        }
271
+        None => return public_timeline(configuration, authentication, false),
272
+    }
273
+}
274
+
275
+pub fn index(configuration: LocalConfiguration, authentication: Authentication) -> Template {
276
+    let mut context = HashMap::<String, String>::new();
277
+
278
+    match &authentication.account {
279
+        Some(account) => return home_timeline(configuration, authentication),
280
+        None => {
281
+            context.extend(configuration);
282
+            context.extend(prepare_authentication_context(&authentication));
283
+            context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
284
+            return Template::render("raito_fe/infoscreen", context);
285
+        }
286
+    }
287
+}
288
+
289
+pub fn login(configuration: LocalConfiguration, authentication: Authentication) -> Template {
290
+    let mut context = HashMap::<String, String>::new();
291
+    context.extend(configuration.clone());
292
+    context.extend(prepare_authentication_context(&authentication));
293
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
294
+
295
+    if authentication.account.is_none() {
296
+        return Template::render("raito_fe/login", context);
297
+    } else {
298
+        return public_timeline(configuration, authentication, false);
299
+    }
300
+}
301
+
302
+pub fn login_post(
303
+    configuration: LocalConfiguration,
304
+    authentication: Authentication,
305
+    mut cookies: Cookies,
306
+    form: LenientForm<LoginForm>,
307
+) -> Template {
308
+    let mut context = HashMap::<String, String>::new();
309
+    context.extend(configuration.clone());
310
+    context.extend(prepare_authentication_context(&authentication));
311
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
312
+
313
+    if authentication.account.is_none() {
314
+        match raito_fe::api_controller::login(form) {
315
+            Some(token) => {
316
+                cookies.add_private(Cookie::new("oauth_token", token));
317
+                return public_timeline(configuration, authentication, false);
318
+            }
319
+            None => Template::render("raito_fe/login", context),
320
+        }
321
+    } else {
322
+        return public_timeline(configuration, authentication, false);
323
+    }
324
+}
325
+
326
+pub fn public_timeline(
327
+    configuration: LocalConfiguration,
328
+    authentication: Authentication,
329
+    local: bool,
330
+) -> Template {
331
+    let rocket_renderer = rocket::ignite().attach(Template::fairing());
332
+    let mut context = HashMap::<String, String>::new();
333
+    let mut timeline_parameters = HashMap::<String, String>::new();
334
+
335
+    context.extend(configuration.clone());
336
+    context.extend(prepare_authentication_context(&authentication));
337
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
338
+    if local {
339
+        context.insert(
340
+            String::from("timeline_name"),
341
+            String::from("Public Timeline"),
342
+        );
343
+    } else {
344
+        context.insert(
345
+            String::from("timeline_name"),
346
+            String::from("Global Timeline"),
347
+        );
348
+    }
349
+
350
+    timeline_parameters.extend(configuration.clone());
351
+
352
+    if configuration.0.get("javascript_enabled").unwrap() == "true" {
353
+        context.insert(
354
+            String::from("timeline"),
355
+            Template::show(
356
+                &rocket_renderer,
357
+                "raito_fe/components/timeline",
358
+                timeline_parameters,
359
+            )
360
+            .unwrap(),
361
+        );
362
+        return Template::render("raito_fe/timeline_view", context);
363
+    } else {
364
+        match raito_fe::api_controller::get_public_timeline(local) {
365
+            Ok(statuses) => {
366
+                let mut renderered_statuses: Vec<String> = vec![];
367
+                for status in statuses {
368
+                    renderered_statuses.push(raw_status(
369
+                        configuration.clone(),
370
+                        &authentication,
371
+                        status,
372
+                        &rocket_renderer,
373
+                    ));
374
+                }
375
+
376
+                timeline_parameters.insert(String::from("statuses"), renderered_statuses.join(""));
377
+                context.insert(
378
+                    String::from("timeline"),
379
+                    Template::show(
380
+                        &rocket_renderer,
381
+                        "raito_fe/components/timeline",
382
+                        timeline_parameters,
383
+                    )
384
+                    .unwrap(),
385
+                );
386
+                return Template::render("raito_fe/timeline_view", context);
387
+            }
388
+            Err(_) => Template::render("raito_fe/index", context),
389
+        }
390
+    }
391
+}
392
+
393
+pub fn raw_status(
394
+    configuration: LocalConfiguration,
395
+    authentication: &Authentication,
396
+    status: Status,
397
+    rocket: &Rocket,
398
+) -> String {
399
+    let mut context = prepare_status_context(status);
400
+    context.extend(configuration);
401
+    context.extend(prepare_authentication_context(authentication));
402
+    return Template::show(rocket, "raito_fe/components/status", context).unwrap();
403
+}
404
+
405
+pub fn register_post(
406
+    configuration: LocalConfiguration,
407
+    authentication: Authentication,
408
+    mut cookies: Cookies,
409
+    form: LenientForm<RegistrationForm>,
410
+) -> Template {
411
+    let mut context = HashMap::<String, String>::new();
412
+    context.extend(configuration.clone());
413
+    context.extend(prepare_authentication_context(&authentication));
414
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
415
+
416
+    if authentication.account.is_none() {
417
+        match raito_fe::api_controller::register(form) {
418
+            Some(token) => {
419
+                cookies.add_private(Cookie::new("oauth_token", token));
420
+                public_timeline(configuration, authentication, false)
421
+            }
422
+            None => Template::render("raito_fe/infoscreen", context),
423
+        }
424
+    } else {
425
+        return home_timeline(configuration, authentication);
426
+    }
427
+}
428
+
429
+pub fn settings(configuration: LocalConfiguration, authentication: Authentication) -> Template {
430
+    let mut context = HashMap::<String, String>::new();
431
+    context.extend(configuration);
432
+    context.extend(prepare_authentication_context(&authentication));
433
+    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
434
+    return Template::render("raito_fe/settings", context);
435
+}
436
+
437
+pub fn user_timeline(
438
+    configuration: LocalConfiguration,
439
+    authentication: Authentication,
440
+    id: String,
441
+) -> String {
442
+    let mut context = HashMap::<String, String>::new();
443
+
444
+    match raito_fe::api_controller::get_user_timeline(id) {
445
+        Ok(statuses) => {
446
+            let mut renderered_statuses: Vec<String> = vec![];
447
+            let rocket_renderer = rocket::ignite().attach(Template::fairing());
448
+            for status in statuses {
449
+                renderered_statuses.push(raw_status(
450
+                    configuration.clone(),
451
+                    &authentication,
452
+                    status,
453
+                    &rocket_renderer,
454
+                ));
455
+            }
456
+
457
+            context.extend(configuration);
458
+            context.extend(prepare_authentication_context(&authentication));
459
+            context.insert(String::from("statuses"), renderered_statuses.join(""));
460
+            context.insert(String::from("timeline_name"), String::from("User Timeline"));
461
+
462
+            return Template::show(&rocket_renderer, "raito_fe/components/timeline", context)
463
+                .unwrap();
464
+        }
465
+        Err(_) => String::from(""),
466
+    }
467
+}
468
+
469
+fn prepare_authentication_context(authentication: &Authentication) -> HashMap<String, String> {
470
+    let mut context = HashMap::<String, String>::new();
471
+
472
+    match &authentication.account {
473
+        Some(account) => {
474
+            context.insert(String::from("authenticated_account"), true.to_string());
475
+            context.insert(
476
+                String::from("authenticated_account_display_name"),
477
+                (*account.display_name).to_string(),
478
+            );
479
+        }
480
+        None => {
481
+            context.insert(String::from("authenticated_account"), false.to_string());
482
+            context.insert(
483
+                String::from("authenticated_account_display_name"),
484
+                String::from("Guest"),
485
+            );
486
+        }
487
+    }
488
+
489
+    return context;
490
+}
491
+
492
+fn prepare_status_context(status: Status) -> HashMap<String, String> {
493
+    let mut context = HashMap::<String, String>::new();
494
+
495
+    let date: String;
496
+    let favourites_count: String;
497
+    let replies_count: String;
498
+    let shares_count: String;
499
+
500
+    match DateTime::parse_from_rfc3339(&status.created_at) {
501
+        Ok(parsed_date) => date = parsed_date.format("%B %d, %Y, %H:%M:%S").to_string(),
502
+        Err(_) => date = String::from("Unknown date"),
503
+    }
504
+
505
+    if status.favourites_count > 0 {
506
+        favourites_count = status.favourites_count.to_string()
507
+    } else {
508
+        favourites_count = String::from("")
509
+    }
510
+
511
+    if status.replies_count > 0 {
512
+        replies_count = status.replies_count.to_string()
513
+    } else {
514
+        replies_count = String::from("")
515
+    }
516
+
517
+    if status.reblogs_count > 0 {
518
+        shares_count = status.reblogs_count.to_string()
519
+    } else {
520
+        shares_count = String::from("")
521
+    }
522
+
523
+    context.insert(String::from("status_account_acct"), status.account.acct);
524
+    context.insert(String::from("status_account_avatar"), status.account.avatar);
525
+    context.insert(
526
+        String::from("status_account_displayname"),
527
+        status.account.display_name,
528
+    );
529
+    context.insert(
530
+        String::from("status_account_url"),
531
+        format!("/account/{}", status.account.id),
532
+    );
533
+    context.insert(String::from("status_content"), status.content);
534
+    context.insert(String::from("status_created_at"), date);
535
+    context.insert(String::from("status_favourites_count"), favourites_count);
536
+    context.insert(String::from("status_id"), status.id.to_string());
537
+    context.insert(String::from("status_reblogs_count"), shares_count);
538
+    context.insert(String::from("status_replies_count"), replies_count);
539
+    context.insert(String::from("status_uri"), status.uri);
540
+    context.insert(String::from("status_url"), format!("/status/{}", status.id));
541
+
542
+    return context;
543
+}

+ 100
- 24
src/raito_fe/routes.rs View File

@@ -1,47 +1,123 @@
1
-use raito_fe;
2
-use raito_fe::account;
3
-use raito_fe::timeline;
1
+use mastodon_api::{RegistrationForm, StatusForm};
2
+use raito_fe::{renderer, Authentication, LocalConfiguration, LoginForm};
3
+use rocket::http::Cookies;
4
+use rocket::request::LenientForm;
4 5
 use rocket_contrib::templates::Template;
5
-use std::collections::HashMap;
6 6
 
7 7
 #[get("/")]
8
-pub fn index() -> Template {
9
-    timeline::render_public_timeline(false)
8
+pub fn index(configuration: LocalConfiguration, authentication: Authentication) -> Template {
9
+    return renderer::index(configuration, authentication);
10 10
 }
11 11
 
12 12
 #[get("/about")]
13
-pub fn about() -> Template {
14
-    let mut context = HashMap::<String, String>::new();
15
-    context.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
16
-    return Template::render("raito_fe/about", context);
13
+pub fn about(configuration: LocalConfiguration, authentication: Authentication) -> Template {
14
+    return renderer::about(configuration, authentication);
17 15
 }
18 16
 
19 17
 #[get("/account/<id>", rank = 2)]
20
-pub fn account(id: String) -> Template {
21
-    account::get_account_by_local_id(id)
18
+pub fn account(
19
+    configuration: LocalConfiguration,
20
+    authentication: Authentication,
21
+    id: String,
22
+) -> Template {
23
+    return renderer::account_by_local_id(configuration, authentication, id);
22 24
 }
23 25
 
24 26
 #[get("/actors/<handle>", rank = 2)]
25
-pub fn actor(handle: String) -> Template {
26
-    account::get_account_by_username(handle)
27
+pub fn actor(
28
+    configuration: LocalConfiguration,
29
+    authentication: Authentication,
30
+    handle: String,
31
+) -> Template {
32
+    return renderer::account_by_username(configuration, authentication, handle);
33
+}
34
+
35
+#[get("/compose?<in_reply_to>", rank = 2)]
36
+pub fn status_draft(
37
+    configuration: LocalConfiguration,
38
+    authentication: Authentication,
39
+    in_reply_to: Option<i64>,
40
+) -> Template {
41
+    return renderer::compose(configuration, authentication, in_reply_to);
42
+}
43
+
44
+#[post("/compose", rank = 2, data = "<form>")]
45
+pub fn status_compose(
46
+    configuration: LocalConfiguration,
47
+    authentication: Authentication,
48
+    form: LenientForm<StatusForm>,
49
+) -> Template {
50
+    return renderer::compose_post(configuration, authentication, form);
51
+}
52
+
53
+#[get("/login")]
54
+pub fn login(configuration: LocalConfiguration, authentication: Authentication) -> Template {
55
+    return renderer::login(configuration, authentication);
56
+}
57
+
58
+#[post("/login", data = "<form>")]
59
+pub fn login_post(
60
+    configuration: LocalConfiguration,
61
+    authentication: Authentication,
62
+    cookies: Cookies,
63
+    form: LenientForm<LoginForm>,
64
+) -> Template {
65
+    return renderer::login_post(configuration, authentication, cookies, form);
66
+}
67
+
68
+#[get("/timeline/home")]
69
+pub fn home_timeline(
70
+    configuration: LocalConfiguration,
71
+    authentication: Authentication,
72
+) -> Template {
73
+    return renderer::home_timeline(configuration, authentication);
27 74
 }
28 75
 
29 76
 #[get("/objects/<id>", rank = 2)]
30
-pub fn object(id: String) -> Template {
31
-    timeline::get_conversation_by_uri(id)
77
+pub fn object(
78
+    configuration: LocalConfiguration,
79
+    authentication: Authentication,
80
+    id: String,
81
+) -> Template {
82
+    return renderer::conversation_by_uri(configuration, authentication, id);
83
+}
84
+
85
+#[post("/register", data = "<form>")]
86
+pub fn register(
87
+    configuration: LocalConfiguration,
88
+    authentication: Authentication,
89
+    cookies: Cookies,
90
+    form: LenientForm<RegistrationForm>,
91
+) -> Template {
92
+    return renderer::register_post(configuration, authentication, cookies, form);
93
+}
94
+
95
+#[get("/settings")]
96
+pub fn settings(configuration: LocalConfiguration, authentication: Authentication) -> Template {
97
+    return renderer::settings(configuration, authentication);
32 98
 }
33 99
 
34 100
 #[get("/status/<id>", rank = 2)]
35
-pub fn view_status(id: String) -> Template {
36
-    timeline::render_conversation(id)
101
+pub fn view_status(
102
+    configuration: LocalConfiguration,
103
+    authentication: Authentication,
104
+    id: String,
105
+) -> Template {
106
+    return renderer::conversation(configuration, authentication, id);
37 107
 }
38 108
 
39
-#[get("/timeline/global", rank = 2)]
40
-pub fn global_timeline() -> Template {
41
-    timeline::render_public_timeline(false)
109
+#[get("/timeline/global")]
110
+pub fn global_timeline(
111
+    configuration: LocalConfiguration,
112
+    authentication: Authentication,
113
+) -> Template {
114
+    return renderer::public_timeline(configuration, authentication, false);
42 115
 }
43 116
 
44
-#[get("/timeline/public", rank = 2)]
45
-pub fn public_timeline() -> Template {
46
-    timeline::render_public_timeline(true)
117
+#[get("/timeline/public")]
118
+pub fn public_timeline(
119
+    configuration: LocalConfiguration,
120
+    authentication: Authentication,
121
+) -> Template {
122
+    return renderer::public_timeline(configuration, authentication, true);
47 123
 }

+ 0
- 76
src/raito_fe/status.rs View File

@@ -1,76 +0,0 @@
1
-use chrono::prelude::*;
2
-use mastodon_api::Status;
3
-use raito_fe;
4
-use rocket::Rocket;
5
-use rocket_contrib::templates::Template;
6
-use std::collections::HashMap;
7
-
8
-fn prepare_status(status: Status) -> HashMap<String, String> {
9
-    let mut template_parameters = HashMap::<String, String>::new();
10
-
11
-    let date: String;
12
-    let favourites_count: String;
13
-    let replies_count: String;
14
-    let shares_count: String;
15
-
16
-    match DateTime::parse_from_rfc3339(&status.created_at) {
17
-        Ok(parsed_date) => date = parsed_date.format("%B %d, %Y, %H:%M:%S").to_string(),
18
-        Err(_) => date = String::from("Unknown date"),
19
-    }
20
-
21
-    if status.favourites_count > 0 {
22
-        favourites_count = status.favourites_count.to_string()
23
-    } else {
24
-        favourites_count = String::from("")
25
-    }
26
-
27
-    if status.replies_count > 0 {
28
-        replies_count = status.replies_count.to_string()
29
-    } else {
30
-        replies_count = String::from("")
31
-    }
32
-
33
-    if status.reblogs_count > 0 {
34
-        shares_count = status.reblogs_count.to_string()
35
-    } else {
36
-        shares_count = String::from("")
37
-    }
38
-
39
-    template_parameters.insert(String::from("status_account_acct"), status.account.acct);
40
-    template_parameters.insert(String::from("status_account_avatar"), status.account.avatar);
41
-    template_parameters.insert(
42
-        String::from("status_account_displayname"),
43
-        status.account.display_name,
44
-    );
45
-    template_parameters.insert(
46
-        String::from("status_account_url"),
47
-        format!("/account/{}", status.account.id),
48
-    );
49
-    template_parameters.insert(String::from("status_content"), status.content);
50
-    template_parameters.insert(String::from("status_created_at"), date);
51
-    template_parameters.insert(String::from("status_favourites_count"), favourites_count);
52
-    template_parameters.insert(String::from("status_reblogs_count"), shares_count);
53
-    template_parameters.insert(String::from("status_replies_count"), replies_count);
54
-    template_parameters.insert(String::from("status_uri"), status.uri);
55
-    template_parameters.insert(String::from("status_url"), format!("/status/{}", status.id));
56
-
57
-    return template_parameters;
58
-}
59
-
60
-pub fn render_status_by_local_id(id: String) -> Template {
61
-    let mut template_parameters = HashMap::<String, String>::new();
62
-
63
-    match raito_fe::api_controller::get_status(id) {
64
-        Ok(status) => {
65
-            template_parameters = prepare_status(status);
66
-            template_parameters.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
67
-            return Template::render("raito_fe/status_view", template_parameters);
68
-        }
69
-        Err(_) => Template::render("raito_fe/index", template_parameters),
70
-    }
71
-}
72
-
73
-pub fn render_raw_status(status: Status, rocket: &Rocket) -> String {
74
-    let template_parameters = prepare_status(status);
75
-    return Template::show(rocket, "raito_fe/components/status", template_parameters).unwrap();
76
-}

+ 0
- 168
src/raito_fe/timeline.rs View File

@@ -1,168 +0,0 @@
1
-use activity;
2
-use database;
3
-use env;
4
-use mastodon_api::Status;
5
-use raito_fe;
6
-use raito_fe::status::render_raw_status;
7
-use rocket_contrib::templates::Template;
8
-use std::collections::HashMap;
9
-
10
-pub fn get_conversation_by_uri(id: String) -> Template {
11
-    let database = database::establish_connection();
12
-    let mut template_parameters = HashMap::<String, String>::new();
13
-    let object_id = format!(
14
-        "{}://{}/objects/{}",
15
-        env::get_value(String::from("endpoint.base_scheme")),
16
-        env::get_value(String::from("endpoint.base_domain")),
17
-        id
18
-    );
19
-
20
-    template_parameters.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
21
-
22
-    match activity::get_ap_object_by_id(&database, &object_id) {
23
-        Ok(activity) => render_conversation(activity.id.to_string()),
24
-        Err(_) => Template::render("raito_fe/index", template_parameters),
25
-    }
26
-}
27
-
28
-pub fn render_conversation(id: String) -> Template {
29
-    let mut template_parameters = HashMap::<String, String>::new();
30
-    let rocket_renderer = rocket::ignite().attach(Template::fairing());
31
-
32
-    template_parameters.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
33
-
34
-    match raito_fe::api_controller::get_status(id.clone()) {
35
-        Ok(status) => {
36
-            let mut renderered_statuses: Vec<String> = vec![];
37
-            let mut parent_statuses: Vec<Status> = vec![];
38
-            let mut child_statuses: Vec<Status> = vec![];
39
-            let mut timeline_parameters = HashMap::<String, String>::new();
40
-
41
-            match raito_fe::api_controller::get_status_context(id) {
42
-                Ok(context) => {
43
-                    parent_statuses =
44
-                        serde_json::from_value(context["ancestors"].to_owned()).unwrap();
45
-                    child_statuses =
46
-                        serde_json::from_value(context["descendants"].to_owned()).unwrap();
47
-                }
48
-                Err(()) => (),
49
-            }
50
-
51
-            for parent in parent_statuses {
52
-                renderered_statuses.push(render_raw_status(parent, &rocket_renderer));
53
-            }
54
-            renderered_statuses.push(render_raw_status(status, &rocket_renderer));
55
-            for child in child_statuses {
56
-                renderered_statuses.push(render_raw_status(child, &rocket_renderer));
57
-            }
58
-            template_parameters.insert(String::from("timeline_name"), String::from("Conversation"));
59
-            timeline_parameters.insert(String::from("statuses"), renderered_statuses.join(""));
60
-            template_parameters.insert(
61
-                String::from("timeline"),
62
-                Template::show(
63
-                    &rocket_renderer,
64
-                    "raito_fe/components/timeline",
65
-                    timeline_parameters,
66
-                )
67
-                .unwrap(),
68
-            );
69
-
70
-            return Template::render("raito_fe/timeline_view", template_parameters);
71
-        }
72
-        Err(_) => Template::render("raito_fe/index", template_parameters),
73
-    }
74
-}
75
-
76
-pub fn render_home_timeline() -> Template {
77
-    let mut template_parameters = HashMap::<String, String>::new();
78
-    template_parameters.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
79
-
80
-    match raito_fe::api_controller::get_public_timeline(false) {
81
-        Ok(statuses) => {
82
-            let mut renderered_statuses: Vec<String> = vec![];
83
-            let rocket_renderer = rocket::ignite().attach(Template::fairing());
84
-            let mut timeline_parameters = HashMap::<String, String>::new();
85
-            for status in statuses {
86
-                renderered_statuses.push(render_raw_status(status, &rocket_renderer));
87
-            }
88
-
89
-            template_parameters
90
-                .insert(String::from("timeline_name"), String::from("Home Timeline"));
91
-
92
-            timeline_parameters.insert(String::from("statuses"), renderered_statuses.join(""));
93
-            template_parameters.insert(
94
-                String::from("timeline"),
95
-                Template::show(
96
-                    &rocket_renderer,
97
-                    "raito_fe/components/timeline",
98
-                    timeline_parameters,
99
-                )
100
-                .unwrap(),
101
-            );
102
-
103
-            return Template::render("raito_fe/timeline_view", template_parameters);
104
-        }
105
-        Err(_) => Template::render("raito_fe/index", template_parameters),
106
-    }
107
-}
108
-
109
-pub fn render_public_timeline(local: bool) -> Template {
110
-    let mut template_parameters = HashMap::<String, String>::new();
111
-    template_parameters.insert("stylesheet".to_string(), raito_fe::get_stylesheet());
112
-
113
-    match raito_fe::api_controller::get_public_timeline(local) {
114
-        Ok(statuses) => {
115
-            let mut renderered_statuses: Vec<String> = vec![];
116
-            let rocket_renderer = rocket::ignite().attach(Template::fairing());
117
-            let mut timeline_parameters = HashMap::<String, String>::new();
118
-            for status in statuses {
119
-                renderered_statuses.push(render_raw_status(status, &rocket_renderer));
120
-            }
121
-
122
-            if local {
123
-                template_parameters.insert(String::from("timeline_name"), String::from("Public"));
124
-            } else {
125
-                template_parameters.insert(String::from("timeline_name"), String::from("Global"));
126
-            }
127
-            timeline_parameters.insert(String::from("statuses"), renderered_statuses.join(""));
128
-            template_parameters.insert(
129
-                String::from("timeline"),
130
-                Template::show(
131
-                    &rocket_renderer,
132
-                    "raito_fe/components/timeline",
133
-                    timeline_parameters,
134
-                )
135
-                .unwrap(),
136
-            );
137
-
138
-            return Template::render("raito_fe/timeline_view", template_parameters);
139
-        }
140
-        Err(_) => Template::render("raito_fe/index", template_parameters),
141
-    }
142
-}
143
-
144
-pub fn get_user_timeline(id: String) -> String {
145
-    let mut template_parameters = HashMap::<String, String>::new();
146
-
147
-    match raito_fe::api_controller::get_user_timeline(id) {
148
-        Ok(statuses) => {
149
-            let mut renderered_statuses: Vec<String> = vec![];
150
-            let rocket_renderer = rocket::ignite().attach(Template::fairing());
151
-            for status in statuses {
152
-                renderered_statuses.push(render_raw_status(status, &rocket_renderer));
153
-            }
154
-
155
-            template_parameters.insert(String::from("statuses"), renderered_statuses.join(""));
156
-            template_parameters
157
-                .insert(String::from("timeline_name"), String::from("User Timeline"));
158
-
159
-            return Template::show(
160
-                &rocket_renderer,
161
-                "raito_fe/components/timeline",
162
-                template_parameters,
163
-            )
164
-            .unwrap();
165
-        }
166
-        Err(_) => String::from(""),
167
-    }
168
-}

+ 17
- 14
src/tests/activity.rs View File

@@ -29,10 +29,7 @@ fn get_ap_activity_by_id() {
29 29
 
30 30
     activity::insert_activity(
31 31
         &database,
32
-        ap_activity::create_internal_activity(
33
-            test_activity,
34
-            String::from("https://example.tld/alyssa"),
35
-        ),
32
+        ap_activity::create_internal_activity(&test_activity, "https://example.tld/alyssa"),
36 33
     );
37 34
     let result = activity::get_ap_activity_by_id(
38 35
         &database,
@@ -59,8 +56,8 @@ fn get_ap_object_by_id() {
59 56
     activity::insert_activity(
60 57
         &database,
61 58
         ap_activity::create_internal_activity(
62
-            valid_remote_dummy_create_activity(test_object_id.clone(), None),
63
-            String::from("https://remote.tld/ben"),
59
+            &valid_remote_dummy_create_activity(test_object_id.clone(), None),
60
+            "https://remote.tld/ben",
64 61
         ),
65 62
     );
66 63
     let result = activity::get_ap_object_by_id(&database, &test_object_id);
@@ -83,15 +80,18 @@ fn get_ap_object_replies_by_id() {
83 80
     activity::insert_activity(
84 81
         &database,
85 82
         ap_activity::create_internal_activity(
86
-            valid_local_dummy_create_activity(test_object_id.clone(), None),
87
-            String::from("https://example.tld/alyssa"),
83
+            &valid_local_dummy_create_activity(test_object_id.clone(), None),
84
+            "https://example.tld/alyssa",
88 85
         ),
89 86
     );
90 87
     activity::insert_activity(
91 88
         &database,
92 89
         ap_activity::create_internal_activity(
93
-            valid_remote_dummy_create_activity(test_reply_id.clone(), Some(test_object_id.clone())),
94
-            String::from("https://remote.tld/ben"),
90
+            &valid_remote_dummy_create_activity(
91
+                test_reply_id.clone(),
92
+                Some(test_object_id.clone()),
93
+            ),
94
+            "https://remote.tld/ben",
95 95
         ),
96 96
     );
97 97
     match activity::get_ap_object_replies_by_id(&database, &test_object_id.clone()) {
@@ -125,15 +125,18 @@ fn count_ap_object_replies_by_id() {
125 125
     activity::insert_activity(
126 126
         &database,
127 127
         ap_activity::create_internal_activity(
128
-            valid_local_dummy_create_activity(test_object_id.clone(), None),
129
-            String::from("https://example.tld/alyssa"),
128
+            &valid_local_dummy_create_activity(test_object_id.clone(), None),
129
+            "https://example.tld/alyssa",
130 130
         ),
131 131
     );
132 132
     activity::insert_activity(
133 133
         &database,
134 134
         ap_activity::create_internal_activity(
135
-            valid_remote_dummy_create_activity(test_reply_id.clone(), Some(test_object_id.clone())),
136
-            String::from("https://remote.tld/ben"),
135
+            &valid_remote_dummy_create_activity(
136
+                test_reply_id.clone(),
137
+                Some(test_object_id.clone()),
138
+            ),
139
+            "https://remote.tld/ben",
137 140
         ),
138 141
     );
139 142
 

+ 2
- 2
src/tests/activitypub_actor.rs View File

@@ -7,7 +7,7 @@ use tests::utils::delete_test_actor;
7 7
 
8 8
 #[test]
9 9
 fn get_err_by_preferred_username() {
10
-    let json_object = actor::get_json_by_preferred_username(String::from("希"));
10
+    let json_object = actor::get_json_by_preferred_username("希");
11 11
     assert_eq!(
12 12
         json_object["error"].to_string(),
13 13
         format!("\"{}\"", "User not found.")
@@ -32,7 +32,7 @@ fn get_json_by_preferred_username() {
32 32
         .unwrap_or_else(|| String::from(""));
33 33
     let local = test_actor.local.clone();
34 34
 
35
-    let json_object = actor::get_json_by_preferred_username(preferred_username.clone());
35
+    let json_object = actor::get_json_by_preferred_username(&preferred_username);
36 36
 
37 37
     delete_test_actor(test_actor);
38 38
     assert_eq!(

+ 2
- 2
src/tests/activitypub_controller.rs View File

@@ -43,8 +43,8 @@ fn object_exists() {
43 43
     activity::insert_activity(
44 44
         &database,
45 45
         create_internal_activity(
46
-            valid_remote_dummy_create_activity(test_object_id.clone(), None),
47
-            test_actor,
46
+            &valid_remote_dummy_create_activity(test_object_id.clone(), None),
47
+            &test_actor,
48 48
         ),
49 49
     );
50 50
     let test_object_exists = controller::object_exists(&test_object_id.clone());

+ 4
- 10
src/tests/actor.rs View File

@@ -96,7 +96,7 @@ fn get_actor_by_acct() {
96 96
     let test_actor_id = test_actor.id;
97 97
     let acct = test_actor.get_acct();
98 98
 
99
-    match actor::get_actor_by_acct(&database, acct) {
99
+    match actor::get_actor_by_acct(&database, &acct) {
100 100
         Ok(actor) => {
101 101
             delete_test_actor(test_actor);
102 102
             assert_eq!(test_actor_id, actor.id);
@@ -131,10 +131,7 @@ fn get_local_actor_by_preferred_username() {
131 131
     let database = database::establish_connection();
132 132
     let mut test_actor = create_local_test_actor("dd4f078d-d5d8-4f84-a99f-7c13841fa962");
133 133
 
134
-    match actor::get_local_actor_by_preferred_username(
135
-        &database,
136
-        test_actor.preferred_username.clone(),
137
-    ) {
134
+    match actor::get_local_actor_by_preferred_username(&database, &test_actor.preferred_username) {
138 135
         Ok(_) => {
139 136
             delete_test_actor(test_actor);
140 137
             assert!(true);
@@ -262,10 +259,7 @@ fn create_local_actor() {
262 259
     let summary = test_actor.summary.clone();
263 260
     let local = test_actor.local.clone();
264 261
 
265
-    match actor::get_local_actor_by_preferred_username(
266
-        &database,
267
-        test_actor.preferred_username.clone(),
268
-    ) {
262
+    match actor::get_local_actor_by_preferred_username(&database, &test_actor.preferred_username) {
269 263
         Ok(db_actor) => {
270 264
             delete_test_actor(test_actor);
271 265
             assert_eq!(db_actor.email, email);
@@ -297,7 +291,7 @@ fn delete_local_actor() {
297 291
     let preferred_username = test_actor.preferred_username.clone();
298 292
     delete_test_actor(test_actor);
299 293
 
300
-    match actor::get_local_actor_by_preferred_username(&database, preferred_username) {
294
+    match actor::get_local_actor_by_preferred_username(&database, &preferred_username) {
301 295
         Ok(_) => assert!(false, "Actor was not deleted!"),
302 296
         Err(_) => assert!(true),
303 297
     }

+ 1
- 5
src/well_known/nodeinfo.rs View File

@@ -109,11 +109,7 @@ pub fn nodeinfo_v2_1() -> JsonValue {
109 109
 
110 110
 // Lists nodeinfo routes.
111 111
 pub fn get_routes() -> Vec<rocket::Route> {
112
-    routes![
113
-        nodeinfo,
114
-        nodeinfo_v2,
115
-        nodeinfo_v2_1,
116
-    ]
112
+    routes![nodeinfo, nodeinfo_v2, nodeinfo_v2_1,]
117 113
 }
118 114
 
119 115
 fn get_local_posts() -> usize {

+ 1
- 1
src/well_known/webfinger.rs View File

@@ -10,7 +10,7 @@ pub fn webfinger(resource: &RawStr) -> JsonValue {
10 10
 
11 11
     let mut parsed_resource: &str = &resource.as_str().replace("%3A", ":").replace("%40", "@");
12 12
 
13
-    match get_actor_by_acct(&database, str::replace(parsed_resource, "acct:", "")) {
13
+    match get_actor_by_acct(&database, &str::replace(parsed_resource, "acct:", "")) {
14 14
         Ok(actor) => {
15 15
             if actor.local {
16 16
                 json!({

+ 77
- 0
static/raito_fe/js/timeline.js View File

@@ -0,0 +1,77 @@
1
+var timeline = [];
2
+
3
+function timeline_error() {
4
+  var error = document.createElement('div');
5
+
6
+  error.className = 'status';
7
+
8
+  error.innerHTML = `Error: Could not fetch statuses`;
9
+
10
+  document.getElementById('inner-timeline').insertBefore(error, document.getElementById('inner-timeline').firstChild);
11
+}
12
+
13
+function prepare_status(status) {
14
+  var new_status = document.createElement('div');
15
+
16
+  new_status.className = 'status';
17
+
18
+  new_status.innerHTML =
19
+      `<div class="status-header">
20
+              <img src="` + status.account.avatar + `" class="status-user-avatar">
21
+
22
+              <div class="status-user-info">
23
+                  <div class="status-user-info-displayname"><a href="` + status.account.url + `">` + status.account.display_name + `</a></div>
24
+                  <div class="status-user-info-username"><a href="` + status.account.url + `">` + status.account.acct + `</a></div>
25
+              </div>
26
+          </div>
27
+
28
+          <div class="status-content">` + status.content + `</div>
29
+
30
+          <div class="status-info">
31
+              <ul>
32
+                  <li>
33
+                      <div class="status-reply"><a class="status-reply-button">⤷</a><a class="status-info-count">` + status.replies_count + `</a>
34
+                      </div>
35
+                  </li>
36
+                  <li>
37
+                      <div class="status-favourite"><a class="status-favourite-button">♡</a><a class="status-info-count">` + status.favourites_count + `</a>
38
+                      </div>
39
+                  </li>
40
+                  <li>
41
+                      <div class="status-share"><a class="status-share-button">⟳</a><a class="status-info-count">` + status.reblogs_count + `</a>
42
+                      </div>
43
+                  </li>
44
+              </ul>
45
+              <div class="status-info-meta">
46
+              <a href="` + status.url + `">View Thread</a>
47
+              <a href="` + status.uri + `">` + status.created_at + `</a>
48
+              </div>
49
+          </div>`;
50
+
51
+          document.getElementById('inner-timeline').insertBefore(new_status, document.getElementById('inner-timeline').firstChild);
52
+}
53
+
54
+function poll_timeline() {
55
+  var api_endpoint = mastodon_api_base_uri + '/api/v1/timelines/public';
56
+  fetch(api_endpoint)
57
+  .then(response => {
58
+    return response.json()
59
+  })
60
+  .then(data => {
61
+    console.log(data);
62
+    data.reverse();
63
+    for (id in data)
64
+    {
65
+      if (!timeline.filter(function(e) { return e.id === data[id].id; }).length > 0)
66
+      {
67
+        timeline.push(data[id]);
68
+        prepare_status(data[id]);
69
+      }
70
+    }
71
+  })
72
+  .catch(err => {
73
+    console.log(err)
74
+  })
75
+
76
+    setTimeout(poll_timeline, 2000);
77
+}

+ 64
- 13
static/raito_fe/themes/raito_light.css View File

@@ -207,8 +207,7 @@ textarea {
207 207
 }
208 208
 
209 209
 #post-form {
210
-	display: none;
211
-	background: var(--focus-color);
210
+	display: block;
212 211
 	padding: 5px 5px 5px 5px;
213 212
 	border-color: var(--background-color);
214 213
 	border-bottom-style: solid;
@@ -222,7 +221,8 @@ textarea {
222 221
 
223 222
 #post-form textarea {
224 223
 	min-height: 50px;
225
-	width: calc(100% - 5px);
224
+	resize: vertical;
225
+	width: calc(100%);
226 226
 	margin: 0;
227 227
 }
228 228
 
@@ -241,6 +241,18 @@ textarea {
241 241
 	padding: 0;
242 242
 }
243 243
 
244
+#user-menu ul ul {
245
+	background: var(--background-color);
246
+	display: none;
247
+	position: absolute;
248
+	top: 100%;
249
+	left: 0;
250
+}
251
+
252
+#user-menu ul li:hover > ul {
253
+	display: block;
254
+}
255
+
244 256
 #user-menu li {
245 257
 	float: left;
246 258
 	list-style: none;
@@ -265,18 +277,30 @@ textarea {
265 277
 	margin-right: 5px;
266 278
 }
267 279
 
268
-#user-menu li li {
269
-	float: none;
270
-}
271
-
272 280
 #user-menu a {
273 281
 	color: var(--top-bar-text-color);
274 282
 	text-decoration: none;
275 283
 }
276 284
 
277
-#settings {
278
-	font-size: 16px;
279
-	padding: 4px 4px 4px 4px;
285
+#settings button {
286
+	background: var(--focus-color);
287
+	color: #FFFFFF;
288
+	font-size: 32px;
289
+	height: 70px;
290
+	width: 170px;
291
+}
292
+
293
+#settings table {
294
+	width: 100%;
295
+}
296
+
297
+#settings td {
298
+	width: 100px;
299
+	text-align: center;
300
+}
301
+
302
+#settings ul {
303
+	list-style: none;
280 304
 }
281 305
 
282 306
 #side-menu {
@@ -337,17 +361,17 @@ textarea {
337 361
 	text-decoration: none;
338 362
 }
339 363
 
340
-#text-container {
364
+.generic-container {
341 365
 	background: var(--background-color);
342 366
 	padding: 1px 20px 4px 20px;
343 367
 }
344 368
 
345
-#text-container h2 {
369
+.generic-container h2 {
346 370
 	border-bottom-color: var(--focus-color);
347 371
 	border-bottom-style: solid;
348 372
 }
349 373
 
350
-#text-container a {
374
+.generic-container a {
351 375
 	color: var(--link-color);
352 376
 	text-decoration: none;
353 377
 }
@@ -397,6 +421,23 @@ textarea {
397 421
 	-webkit-transform: rotate(45deg);
398 422
 }
399 423
 
424
+.mention {
425
+	background: var(--button-background-color) !important;
426
+	border-radius: 16px;
427
+	color: var(--button-text-color) !important;
428
+	padding: 2px 4px 2px 4px;
429
+}
430
+
431
+.panel {
432
+	background: var(--background-color);
433
+	padding: 1px 20px 4px 20px;
434
+}
435
+
436
+.panel h2 {
437
+	border-bottom-color: var(--focus-color);
438
+	border-bottom-style: solid;
439
+}
440
+
400 441
 .status {
401 442
 	background: var(--foreground-color);
402 443
 	width: 100%;
@@ -435,6 +476,8 @@ textarea {
435 476
 	cursor: pointer;
436 477
 }
437 478
 
479
+.status-reply-button { color: inherit; text-decoration: inherit; }
480
+
438 481
 .status-reply-button:hover {
439 482
 	color: var(--link-color);
440 483
 	cursor: pointer;
@@ -573,6 +616,14 @@ textarea {
573 616
 	border-bottom-width: 2px;
574 617
 }
575 618
 
619
+.timeline-menu-compose {
620
+	background: var(--button-background-color);
621
+}
622
+
623
+.timeline-menu-compose a {
624
+	color: var(--button-text-color) !important;
625
+}
626
+
576 627
 #timeline-menu a {
577 628
 	text-decoration: none;
578 629
 	color: var(--text-color);

+ 1
- 2
templates/raito_fe/about.html.tera View File

@@ -1,7 +1,6 @@
1 1
 {% extends "raito_fe/index" %}
2
-
3 2
 {% block router %}
4
-<div id="text-container">
3
+<div class="generic-container">
5 4
 <h2>About this instance</h2>
6 5
 This is a placeholder text, you can edit it in "static/raito_fe/about.html"
7 6
 </div>

+ 17
- 2
templates/raito_fe/account.html.tera View File

@@ -1,5 +1,6 @@
1 1
 {% extends "raito_fe/index" %}
2 2
 {% block router %}
3
+{% if minimalmode_enabled == "false" %}
3 4
 <div id="account-header" style="background-image: url({{account_header}});">
4 5
     <div id="account-header-shadow"></div>
5 6
     <img src="{{account_avatar}}" id="account-profile-picture">
@@ -24,14 +25,28 @@
24 25
         </ul>
25 26
     </div>
26 27
 </div>
27
-
28 28
 <div id="account-bio">
29 29
     <div id="account-bio-inner">
30 30
         {{account_note | safe}}
31 31
     </div>
32 32
 </div>
33
-
34 33
 <div id="account-timeline">
35 34
     {{account_timeline | safe}}
36 35
 </div>
36
+{% else %}
37
+{{account_display_name}}
38
+<br>{{account_acct}}
39
+<ul>
40
+<li><a href="">Follow</a></li>
41
+</ul>
42
+<p>
43
+Statuses: {{account_statuses_count}}
44
+<br>Following: {{account_following_count}}
45
+<br>Followers: {{account_followers_count}}
46
+</p>
47
+<p>Bio:<br>
48
+{{account_note | safe}}
49
+</p>
50
+<hr>{{account_timeline | safe}}
51
+{% endif %}
37 52
 {% endblock router %}

+ 20
- 8
templates/raito_fe/components/status.html.tera View File

@@ -1,19 +1,18 @@
1
+{% if minimalmode_enabled == "false" %}
1 2
 <div class="status">
2 3
     <div class="status-header">
3
-        <img src="{{status_account_avatar}}" class="status-user-avatar">
4
+        <img src="{{status_account_avatar | safe}}" class="status-user-avatar">
4 5
 
5 6
         <div class="status-user-info">
6
-            <div class="status-user-info-displayname"><a href="{{status_account_url}}">{{status_account_displayname}}</a></div>
7
-            <div class="status-user-info-username"><a href="{{status_account_url}}">{{status_account_acct}}</a></div>
7
+            <div class="status-user-info-displayname"><a href="{{status_account_url | safe}}">{{status_account_displayname}}</a></div>
8
+            <div class="status-user-info-username"><a href="{{status_account_url | safe}}">{{status_account_acct}}</a></div>
8 9
         </div>
9 10
     </div>
10
-
11 11
     <div class="status-content">{{status_content | safe}}</div>
12
-
13 12
     <div class="status-info">
14 13
         <ul>
15 14
             <li>
16
-                <div class="status-reply"><a class="status-reply-button">⤷</a><a class="status-info-count">{{status_replies_count}}</a>
15
+                <div class="status-reply"><a class="status-reply-button" href="/compose?in_reply_to={{status_id}}">⤷</a><a class="status-info-count">{{status_replies_count}}</a>
17 16
                 </div>
18 17
             </li>
19 18
             <li>
@@ -26,8 +25,21 @@
26 25
             </li>
27 26
         </ul>
28 27
         <div class="status-info-meta">
29
-        <a href="{{status_url}}">View Thread</a>
30
-        <a href="{{status_uri}}">{{status_created_at}}</a>
28
+        <a href="{{status_url | safe}}">View Thread</a>
29
+        <a href="{{status_uri | safe}}">{{status_created_at}}</a>
31 30
         </div>
32 31
     </div>
33 32
 </div>
33
+{% else %}
34
+{{status_account_displayname}}
35
+<br><a href="{{status_account_url | safe}}">{{status_account_acct}}</a>
36
+<p>{{status_content | safe}}</p>
37
+<ul>
38
+    <li><a class="status-reply-button">Reply</a> ({{status_replies_count}})</li>
39
+    <li><a class="status-favourite-button">Favourite</a> ({{status_favourites_count}})</li>
40
+    <li><a class="status-share-button">Share</a> ({{status_reblogs_count}})</li>
41
+</ul>
42
+<a href="{{status_url | safe}}">View Thread</a>
43
+<br>{{status_created_at}}
44
+<hr>
45
+{% endif %}

+ 1
- 1
templates/raito_fe/components/timeline.html.tera View File

@@ -1,3 +1,3 @@
1 1
 <div id="inner-timeline">
2
-    {{ statuses | safe }}
2
+{% if javascript_enabled == "false" %}{{ statuses | safe }}{% endif %}
3 3
 </div>

+ 40
- 4
templates/raito_fe/index.html.tera View File

@@ -1,29 +1,65 @@
1 1
 <!doctype html>
2 2
 <html>
3
-
4 3
 <head>
5 4
     <meta name="viewport" content="width=device-width, initial-scale=1">
6 5
 </head>
7
-
8 6
 <body>
9 7
     <div id="wrapper">
8
+        {% if minimalmode_enabled == "false" %}
10 9
         <div id="user-menu">
11 10
             <ul>
11
+                <li class="dropdown" style="float: right;">
12
+		    {% if authenticated_account == "true" %}
13
+		    <img class="account-avatar-small" src="/static/assets/default_avatar.png">
14
+		    <span style="margin-right: 5px;">{{authenticated_account_display_name}}</span> <i class="icon-arrow-down"></i>
15
+		    <ul>
16
+            <li><a href="/account/1">Profile</a></li>
17
+            <li><a href="/logout">Logout</a></li>
18
+            </ul>
19
+		    {% else %}
20
+		    <a href="/login">Login</a>
21
+		    {% endif %}
22
+		</li>
23
+                <li><a href="/">Home</a></li>
12 24
                 <li><a href="/timeline/global">Timeline</a></li>
25
+		<li><a href="/settings">Settings</a></li>
13 26
                 <li><a href="/about">About</a></li>
14 27
             </ul>
15 28
         </div>
16 29
         <div id="side-menu">
17 30
             <div class="header">Latest activities</div>
18
-            <ul><li><center>No activities yet ...</center></li></ul>
31
+            <ul>
32
+                <li>
33
+                    <center>No activities yet ...</center>
34
+                </li>
35
+            </ul>
36
+        </div>
37
+        {% else %}
38
+        <div id="user-menu">
39
+            <ul>
40
+                <li><a href="/">Home</a></li>
41
+                <li><a href="/about">About</a></li>
42
+                <li><a href="/timeline/public">Public Timeline</a></li>
43
+                <li><a href="/timeline/global">Global Timeline</a></li>
44
+            </ul>
19 45
         </div>
46
+        {% endif %}
20 47
         <div id="timeline">
21 48
             {% block router %} {% endblock router %}
22 49
         </div>
23 50
     </div>
24 51
 </body>
52
+{% if minimalmode_enabled == "false" %}
25 53
 <style>
26 54
 {{stylesheet | safe}}
27 55
 </style>
28
-
56
+{% endif %} {% if javascript_enabled == "true" %}
57
+<script>
58
+    var mastodon_api_base_uri = '{{mastodon_api_base_uri | safe}}';
59
+</script>
60
+<script src="/static/raito_fe/js/timeline.js"></script>
61
+<script>
62
+    poll_timeline();
63
+</script>
64
+{% endif %}
29 65
 </html>

+ 34
- 0
templates/raito_fe/infoscreen.html.tera View File

@@ -0,0 +1,34 @@
1
+{% extends "raito_fe/index" %}
2
+
3
+{% block router %}
4
+<div class="generic-container">
5
+<h2>Welcome to <a href="https://git.cybre.club/kibouproject/kibou">Kibou</a>!</h2>
6
+<p>Kibou is a lightweight federated social networking server. This is one of many
7
+servers among the Fediverse, an interconnected decentralized social network.</p>
8
+<p>Kibou is written in the <a href="https://www.rust-lang.org/">Rust</a> programming language and is built upon components such as
9
+<a href="https://diesel.rs">Diesel</a> and <a href="https://rocket.rs">Rocket</a>. It also implements Mastodon's REST API,
10
+which means that you can also use Mastodon applications with Kibou.
11
+<br>
12
+<a href="http://kibou.social">Learn more</a></p>
13
+<img src="/static/assets/mascot.png" style="float:right; height: 200px; position: relative; margin-top: 30px;">
14
+<div id="registration-form">
15
+<h3>Create an account</h3>
16
+<form method="post" action="/register">
17
+    <label>E-Mail</label>
18
+    <br>
19
+    <input type="text" name="email">
20
+    <br>
21
+    <label>Username</label>
22
+    <br>
23
+    <input type="text" name="username">
24
+    <br>
25
+    <label>Password</label>
26
+    <br>
27
+    <input type="password" name="password">
28
+    <br >
29
+    <input type="submit">
30
+    <br />
31
+</form>
32
+</div>
33
+</div>
34
+{% endblock router %}

+ 21
- 0
templates/raito_fe/login.html.tera View File

@@ -0,0 +1,21 @@
1
+{% extends "raito_fe/index" %}
2
+
3
+{% block router %}
4
+<div class="generic-container">
5
+<div id="registration-form">
6
+<h3>Log In</h3>
7
+<form method="post" action="/login">
8
+    <label>Username</label>
9
+    <br>
10
+    <input type="text" name="username">
11
+    <br>
12
+    <label>Password</label>
13
+    <br>
14
+    <input type="password" name="password">
15
+    <br >
16
+    <input type="submit">
17
+    <br />
18
+</form>
19
+</div>
20
+</div>
21
+{% endblock router %}

+ 21
- 0
templates/raito_fe/settings.html.tera View File

@@ -0,0 +1,21 @@
1
+{% extends "raito_fe/index" %}
2
+
3
+{% block router %}
4
+<div class="generic-container" id="settings">
5
+<h2>Settings</h2>
6
+<form>
7
+<h3>General</h3>
8
+    <table>
9
+    <tr>
10
+    <th><button>Minimal</button></th>
11
+    <th><button>Static</button></th>
12
+    <th><button>Dynamic</button></th>
13
+    </tr>
14
+    <tr>
15
+    <td>Minimal mode disables styling and saves bandwith.</td>
16
+    <td>Static mode is the default setting.</td>
17
+    <td>Dynamic mode enables JavaScript (auto-updating timelines).</td>
18
+    </table>
19
+</form>
20
+</div>
21
+{% endblock router %}

+ 45
- 0
templates/raito_fe/status_post.html.tera View File

@@ -0,0 +1,45 @@
1
+{% extends "raito_fe/index" %}
2
+
3
+{% block router %}
4
+<div class="panel">
5
+<h2>Compose new status</h2>
6
+<form action="/compose" id="post-form" method="post">
7
+<div class="status">
8
+    <div class="status-header">
9
+        <img src="/static/assets/default_avatar.png" class="status-user-avatar">
10
+
11
+        <div class="status-user-info">
12
+            <div class="status-user-info-displayname"><a href="">You</a></div>
13
+            <div class="status-user-info-username"><a href="">you@your.instance</a></div>
14
+        </div>
15
+    </div>
16
+    <div class="status-content"><textarea name="status" form="post-form"></textarea></div>
17
+    <div class="status-info">
18
+        <ul>
19
+            <li>
20
+                <div class="status-reply">⤷ <a class="status-info-count">0</a>
21
+                </div>
22
+            </li>
23
+            <li>
24
+                <div class="status-favourite">♡ <a class="status-info-count">0</a>
25
+                </div>
26
+            </li>
27
+            <li>
28
+                <div class="status-share">⟳ <a class="status-info-count">0</a>
29
+                </div>
30
+            </li>
31
+        </ul>
32
+    </div>
33
+</div>
34
+
35
+<select name="visibility">
36
+  <option value="public">Public</option>
37
+  <option value="unlisted">Unlisted</option>
38
+  <option value="private">Private</option>
39
+  <option value="direct">Direct Message</option>
40
+</select>
41
+
42
+<input type="submit" text="Submit">
43
+</form>
44
+</div>
45
+{% endblock router %}

+ 16
- 9
templates/raito_fe/timeline_view.html.tera View File

@@ -1,18 +1,25 @@
1 1
 {% extends "raito_fe/index" %}
2
-
3 2
 {% block router %}
3
+{% if minimalmode_enabled == "false" %}
4 4
 {% if timeline_name == "Conversation" %}
5
-<div class="header">
6
-Conversation
7
-</div>
5
+<div class="header">Conversation</div>
8 6
 {% else %}
9 7
 <div id="timeline-menu" class="header">
10
-<ul>
11
-<li {% if timeline_name == "Public" %} class="timeline-menu-active-tab" {% endif %}><a href="/timeline/public">Public</a></li>
12
-<li {% if timeline_name == "Global" %} class="timeline-menu-active-tab" {% endif %}><a href="/timeline/global">Global</a></li>
13
-</ul>
8
+    <ul>
9
+    	{% if authenticated_account == "true" %}
10
+    	<li {% if timeline_name=="Home Timeline" %} class="timeline-menu-active-tab" {% endif %}><a href="/timeline/home">Home</a></li>
11
+        {% endif %}
12
+	<li {% if timeline_name=="Public Timeline" %} class="timeline-menu-active-tab" {% endif %}><a href="/timeline/public">Public</a></li>
13
+        <li {% if timeline_name=="Global Timeline" %} class="timeline-menu-active-tab" {% endif %}><a href="/timeline/global">Global</a></li>
14
+	{% if authenticated_account == "true" %}
15
+	<li class="timeline-menu-compose" style="float: right"><a href="/compose">Compose</a></li>
16
+	{% endif %}
17
+    </ul>
14 18
 </div>
15 19
 {% endif %}
16
-
20
+{% else %}
21
+<div class="header">{{timeline_name}}</div>
22
+<hr>
23
+{% endif %}
17 24
 {{timeline | safe}}
18 25
 {% endblock router %}

Loading…
Cancel
Save