]> Untitled Git - lemmy.git/commitdiff
Adding post and comment ap_id columns.
authorDessalines <tyhou13@gmx.com>
Sat, 4 Apr 2020 00:04:57 +0000 (20:04 -0400)
committerDessalines <tyhou13@gmx.com>
Sat, 4 Apr 2020 00:04:57 +0000 (20:04 -0400)
15 files changed:
server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/down.sql [new file with mode: 0644]
server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/up.sql [new file with mode: 0644]
server/src/api/comment.rs
server/src/api/post.rs
server/src/api/user.rs
server/src/apub/mod.rs
server/src/db/code_migrations.rs
server/src/db/comment.rs
server/src/db/comment_view.rs
server/src/db/moderator.rs
server/src/db/post.rs
server/src/db/post_view.rs
server/src/db/user.rs
server/src/db/user_mention.rs
server/src/schema.rs

diff --git a/server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/down.sql b/server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/down.sql
new file mode 100644 (file)
index 0000000..50c95bb
--- /dev/null
@@ -0,0 +1,7 @@
+alter table post 
+drop column ap_id, 
+drop column local;
+
+alter table comment 
+drop column ap_id, 
+drop column local;
diff --git a/server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/up.sql b/server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/up.sql
new file mode 100644 (file)
index 0000000..a3fb956
--- /dev/null
@@ -0,0 +1,14 @@
+-- Add federation columns to post, comment
+
+alter table post
+-- TODO uniqueness constraints should be added on these 3 columns later
+add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
+add column local boolean not null default true
+;
+
+alter table comment
+-- TODO uniqueness constraints should be added on these 3 columns later
+add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
+add column local boolean not null default true
+;
+
index 8373a338beded5b61ad156e20d16217dc1a8be73..1528f509b74090b0d32d29fafac0da55c067846d 100644 (file)
@@ -99,6 +99,8 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
       deleted: None,
       read: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_comment = match Comment::create(&conn, &comment_form) {
@@ -106,6 +108,11 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
       Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
     };
 
+    match Comment::update_ap_id(&conn, inserted_comment.id) {
+      Ok(comment) => comment,
+      Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
+    };
+
     let mut recipient_ids = Vec::new();
 
     // Scan the comment for user mentions, add those rows
@@ -272,6 +279,8 @@ impl Perform<CommentResponse> for Oper<EditComment> {
 
     let content_slurs_removed = remove_slurs(&data.content.to_owned());
 
+    let read_comment = Comment::read(&conn, data.edit_id)?;
+
     let comment_form = CommentForm {
       content: content_slurs_removed,
       parent_id: data.parent_id,
@@ -285,6 +294,8 @@ impl Perform<CommentResponse> for Oper<EditComment> {
       } else {
         Some(naive_now())
       },
+      ap_id: read_comment.ap_id,
+      local: read_comment.local,
     };
 
     let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
index 651d5769ef5b924c319abd881e79c6ba3eb83924..cfb71941e0dcf29c737af578011b0b945c91d98f 100644 (file)
@@ -131,6 +131,8 @@ impl Perform<PostResponse> for Oper<CreatePost> {
       embed_description: iframely_description,
       embed_html: iframely_html,
       thumbnail_url: pictshare_thumbnail,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_post = match Post::create(&conn, &post_form) {
@@ -146,6 +148,11 @@ impl Perform<PostResponse> for Oper<CreatePost> {
       }
     };
 
+    match Post::update_ap_id(&conn, inserted_post.id) {
+      Ok(post) => post,
+      Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
+    };
+
     // They like their own post by default
     let like_form = PostLikeForm {
       post_id: inserted_post.id,
@@ -371,6 +378,8 @@ impl Perform<PostResponse> for Oper<EditPost> {
     let (iframely_title, iframely_description, iframely_html, pictshare_thumbnail) =
       fetch_iframely_and_pictshare_data(data.url.to_owned());
 
+    let read_post = Post::read(&conn, data.edit_id)?;
+
     let post_form = PostForm {
       name: data.name.to_owned(),
       url: data.url.to_owned(),
@@ -387,6 +396,8 @@ impl Perform<PostResponse> for Oper<EditPost> {
       embed_description: iframely_description,
       embed_html: iframely_html,
       thumbnail_url: pictshare_thumbnail,
+      ap_id: read_post.ap_id,
+      local: read_post.local,
     };
 
     let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
index 59a3d623ac247cf194470d7121959165595b99f6..629ce8e5a44fffb57e64f6f92e3453bcbb6bd2f6 100644 (file)
@@ -563,36 +563,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
       return Err(APIError::err("not_an_admin").into());
     }
 
-    let read_user = User_::read(&conn, data.user_id)?;
-
-    // TODO make addadmin easier
-    let user_form = UserForm {
-      name: read_user.name,
-      fedi_name: read_user.fedi_name,
-      email: read_user.email,
-      matrix_user_id: read_user.matrix_user_id,
-      avatar: read_user.avatar,
-      password_encrypted: read_user.password_encrypted,
-      preferred_username: read_user.preferred_username,
-      updated: Some(naive_now()),
-      admin: data.added,
-      banned: read_user.banned,
-      show_nsfw: read_user.show_nsfw,
-      theme: read_user.theme,
-      default_sort_type: read_user.default_sort_type,
-      default_listing_type: read_user.default_listing_type,
-      lang: read_user.lang,
-      show_avatars: read_user.show_avatars,
-      send_notifications_to_email: read_user.send_notifications_to_email,
-      actor_id: read_user.actor_id,
-      bio: read_user.bio,
-      local: read_user.local,
-      private_key: read_user.private_key,
-      public_key: read_user.public_key,
-      last_refreshed_at: None,
-    };
-
-    match User_::update(&conn, data.user_id, &user_form) {
+    match User_::add_admin(&conn, user_id, data.added) {
       Ok(user) => user,
       Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
     };
@@ -632,36 +603,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
       return Err(APIError::err("not_an_admin").into());
     }
 
-    let read_user = User_::read(&conn, data.user_id)?;
-
-    // TODO make bans and addadmins easier
-    let user_form = UserForm {
-      name: read_user.name,
-      fedi_name: read_user.fedi_name,
-      email: read_user.email,
-      matrix_user_id: read_user.matrix_user_id,
-      avatar: read_user.avatar,
-      password_encrypted: read_user.password_encrypted,
-      preferred_username: read_user.preferred_username,
-      updated: Some(naive_now()),
-      admin: read_user.admin,
-      banned: data.ban,
-      show_nsfw: read_user.show_nsfw,
-      theme: read_user.theme,
-      default_sort_type: read_user.default_sort_type,
-      default_listing_type: read_user.default_listing_type,
-      lang: read_user.lang,
-      show_avatars: read_user.show_avatars,
-      send_notifications_to_email: read_user.send_notifications_to_email,
-      actor_id: read_user.actor_id,
-      bio: read_user.bio,
-      local: read_user.local,
-      private_key: read_user.private_key,
-      public_key: read_user.public_key,
-      last_refreshed_at: None,
-    };
-
-    match User_::update(&conn, data.user_id, &user_form) {
+    match User_::ban_user(&conn, user_id, data.ban) {
       Ok(user) => user,
       Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
     };
@@ -790,18 +732,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
       .list()?;
 
     for reply in &replies {
-      let comment_form = CommentForm {
-        content: reply.to_owned().content,
-        parent_id: reply.to_owned().parent_id,
-        post_id: reply.to_owned().post_id,
-        creator_id: reply.to_owned().creator_id,
-        removed: None,
-        deleted: None,
-        read: Some(true),
-        updated: reply.to_owned().updated,
-      };
-
-      let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
+      match Comment::mark_as_read(&conn, reply.id) {
         Ok(comment) => comment,
         Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
       };
@@ -882,18 +813,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
       .list()?;
 
     for comment in &comments {
-      let comment_form = CommentForm {
-        content: "*Permananently Deleted*".to_string(),
-        parent_id: comment.to_owned().parent_id,
-        post_id: comment.to_owned().post_id,
-        creator_id: comment.to_owned().creator_id,
-        removed: None,
-        deleted: Some(true),
-        read: None,
-        updated: Some(naive_now()),
-      };
-
-      let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
+      let _updated_comment = match Comment::permadelete(&conn, comment.id) {
         Ok(comment) => comment,
         Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
       };
@@ -907,25 +827,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
       .list()?;
 
     for post in &posts {
-      let post_form = PostForm {
-        name: "*Permananently Deleted*".to_string(),
-        url: Some("https://deleted.com".to_string()),
-        body: Some("*Permananently Deleted*".to_string()),
-        creator_id: post.to_owned().creator_id,
-        community_id: post.to_owned().community_id,
-        removed: None,
-        deleted: Some(true),
-        nsfw: post.to_owned().nsfw,
-        locked: None,
-        stickied: None,
-        updated: Some(naive_now()),
-        embed_title: None,
-        embed_description: None,
-        embed_html: None,
-        thumbnail_url: None,
-      };
-
-      let _updated_post = match Post::update(&conn, post.id, &post_form) {
+      let _updated_post = match Post::permadelete(&conn, post.id) {
         Ok(post) => post,
         Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
       };
index 0f08fc988b2587fce847a2dc8fb1f2aaba35eefc..f4afdb1f0b53e722c9522ce23d16ca23fad5c747 100644 (file)
@@ -26,13 +26,15 @@ pub enum EndpointType {
   Community,
   User,
   Post,
+  Comment,
 }
 
 pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
   let point = match endpoint_type {
-    EndpointType::Community => "c",
-    EndpointType::User => "u",
-    EndpointType::Post => "p",
+    EndpointType::Community => "community",
+    EndpointType::User => "user",
+    EndpointType::Post => "post",
+    EndpointType::Comment => "comment",
   };
 
   Url::parse(&format!(
index e0c736e13225780bc7e2bfe0f605c3f31258024c..2fdf03d5141fd12693b1ace3fdbe381f87338061 100644 (file)
@@ -1,5 +1,7 @@
 // This is for db migrations that require code
+use super::comment::Comment;
 use super::community::{Community, CommunityForm};
+use super::post::Post;
 use super::user::{UserForm, User_};
 use super::*;
 use crate::apub::{gen_keypair_str, make_apub_endpoint, EndpointType};
@@ -9,6 +11,8 @@ use log::info;
 pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), Error> {
   user_updates_2020_04_02(conn)?;
   community_updates_2020_04_02(conn)?;
+  post_updates_2020_04_03(conn)?;
+  comment_updates_2020_04_03(conn)?;
 
   Ok(())
 }
@@ -99,3 +103,43 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
 
   Ok(())
 }
+
+fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
+  use crate::schema::post::dsl::*;
+
+  info!("Running post_updates_2020_04_03");
+
+  // Update the ap_id
+  let incorrect_posts = post
+    .filter(ap_id.eq("changeme"))
+    .filter(local.eq(true))
+    .load::<Post>(conn)?;
+
+  for cpost in &incorrect_posts {
+    Post::update_ap_id(&conn, cpost.id)?;
+  }
+
+  info!("{} post rows updated.", incorrect_posts.len());
+
+  Ok(())
+}
+
+fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
+  use crate::schema::comment::dsl::*;
+
+  info!("Running comment_updates_2020_04_03");
+
+  // Update the ap_id
+  let incorrect_comments = comment
+    .filter(ap_id.eq("changeme"))
+    .filter(local.eq(true))
+    .load::<Comment>(conn)?;
+
+  for ccomment in &incorrect_comments {
+    Comment::update_ap_id(&conn, ccomment.id)?;
+  }
+
+  info!("{} comment rows updated.", incorrect_comments.len());
+
+  Ok(())
+}
index 8110fc5ba6102f05e3b2690e1250b9023a3d424a..7550f072e30991fd6b7d9d6e572f4ab144c7c2da 100644 (file)
@@ -1,5 +1,7 @@
 use super::post::Post;
 use super::*;
+use crate::apub::{make_apub_endpoint, EndpointType};
+use crate::naive_now;
 use crate::schema::{comment, comment_like, comment_saved};
 
 // WITH RECURSIVE MyTree AS (
@@ -23,6 +25,8 @@ pub struct Comment {
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
+  pub ap_id: String,
+  pub local: bool,
 }
 
 #[derive(Insertable, AsChangeset, Clone)]
@@ -36,6 +40,8 @@ pub struct CommentForm {
   pub read: Option<bool>,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: Option<bool>,
+  pub ap_id: String,
+  pub local: bool,
 }
 
 impl Crud<CommentForm> for Comment {
@@ -68,6 +74,37 @@ impl Crud<CommentForm> for Comment {
   }
 }
 
+impl Comment {
+  pub fn update_ap_id(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
+    use crate::schema::comment::dsl::*;
+
+    let apid = make_apub_endpoint(EndpointType::Comment, &comment_id.to_string()).to_string();
+    diesel::update(comment.find(comment_id))
+      .set(ap_id.eq(apid))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
+    use crate::schema::comment::dsl::*;
+
+    diesel::update(comment.find(comment_id))
+      .set(read.eq(true))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
+    use crate::schema::comment::dsl::*;
+
+    diesel::update(comment.find(comment_id))
+      .set((
+        content.eq("*Permananently Deleted*"),
+        deleted.eq(true),
+        updated.eq(naive_now()),
+      ))
+      .get_result::<Self>(conn)
+  }
+}
+
 #[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
 #[belongs_to(Comment)]
 #[table_name = "comment_like"]
@@ -231,6 +268,8 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -244,6 +283,8 @@ mod tests {
       read: None,
       parent_id: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
@@ -259,6 +300,8 @@ mod tests {
       parent_id: None,
       published: inserted_comment.published,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let child_comment_form = CommentForm {
@@ -270,6 +313,8 @@ mod tests {
       deleted: None,
       read: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
index 97c03c536d1bfe5ae3da1e56157f8913eb680e08..f0ca723180dd491bea34b971200950aefe0fc033 100644 (file)
@@ -495,6 +495,8 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -508,6 +510,8 @@ mod tests {
       deleted: None,
       read: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
index b01dc5cc17d21b347553a05c29c99f33fd7ef5af..01acc25e5cf91c11954cea936e52bd5c2e856709 100644 (file)
@@ -527,6 +527,8 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -540,6 +542,8 @@ mod tests {
       read: None,
       parent_id: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
index bd8d9e43a2fdcad6adca3822da28f0c2aab0c3e2..b0b9bddc06fd1b77535d3f2f7c584f7e45bd1d39 100644 (file)
@@ -1,4 +1,6 @@
 use super::*;
+use crate::apub::{make_apub_endpoint, EndpointType};
+use crate::naive_now;
 use crate::schema::{post, post_like, post_read, post_saved};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
@@ -21,6 +23,8 @@ pub struct Post {
   pub embed_description: Option<String>,
   pub embed_html: Option<String>,
   pub thumbnail_url: Option<String>,
+  pub ap_id: String,
+  pub local: bool,
 }
 
 #[derive(Insertable, AsChangeset, Clone)]
@@ -41,6 +45,8 @@ pub struct PostForm {
   pub embed_description: Option<String>,
   pub embed_html: Option<String>,
   pub thumbnail_url: Option<String>,
+  pub ap_id: String,
+  pub local: bool,
 }
 
 impl Post {
@@ -58,6 +64,32 @@ impl Post {
       .filter(community_id.eq(the_community_id))
       .load::<Self>(conn)
   }
+
+  pub fn update_ap_id(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
+    use crate::schema::post::dsl::*;
+
+    let apid = make_apub_endpoint(EndpointType::Post, &post_id.to_string()).to_string();
+    diesel::update(post.find(post_id))
+      .set(ap_id.eq(apid))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn permadelete(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
+    use crate::schema::post::dsl::*;
+
+    let perma_deleted = "*Permananently Deleted*";
+    let perma_deleted_url = "https://deleted.com";
+
+    diesel::update(post.find(post_id))
+      .set((
+        name.eq(perma_deleted),
+        url.eq(perma_deleted_url),
+        body.eq(perma_deleted),
+        deleted.eq(true),
+        updated.eq(naive_now()),
+      ))
+      .get_result::<Self>(conn)
+  }
 }
 
 impl Crud<PostForm> for Post {
@@ -269,6 +301,8 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -291,6 +325,8 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     // Post Like
index 587ae52b091496625ed2a1fd3c0ceb669ca1a712..29ff2f1bd2d8bed68ea9d9363acbf953739636ae 100644 (file)
@@ -420,6 +420,8 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
index 32596c270fba56cb4b04fb5cca4dbcbf9682f4c2..1669d722c63760e2b63f04bb4ee70d31ce270d88 100644 (file)
@@ -1,7 +1,7 @@
 use super::*;
 use crate::schema::user_;
 use crate::schema::user_::dsl::*;
-use crate::{is_email_regex, Settings};
+use crate::{is_email_regex, naive_now, Settings};
 use bcrypt::{hash, DEFAULT_COST};
 use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
 
@@ -99,13 +99,28 @@ impl User_ {
     let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
 
     diesel::update(user_.find(user_id))
-      .set(password_encrypted.eq(password_hash))
+      .set((
+        password_encrypted.eq(password_hash),
+        updated.eq(naive_now()),
+      ))
       .get_result::<Self>(conn)
   }
 
   pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {
     user_.filter(name.eq(from_user_name)).first::<Self>(conn)
   }
+
+  pub fn add_admin(conn: &PgConnection, user_id: i32, added: bool) -> Result<Self, Error> {
+    diesel::update(user_.find(user_id))
+      .set(admin.eq(added))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn ban_user(conn: &PgConnection, user_id: i32, ban: bool) -> Result<Self, Error> {
+    diesel::update(user_.find(user_id))
+      .set(banned.eq(ban))
+      .get_result::<Self>(conn)
+  }
 }
 
 #[derive(Debug, Serialize, Deserialize)]
index 48814c89a8f7f1cd199a92821b43d51008dacab5..801df6fe322ef4a63c72fe885979836735401f6a 100644 (file)
@@ -153,6 +153,8 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -166,6 +168,8 @@ mod tests {
       read: None,
       parent_id: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
index 41769ded6cc6f68c27d8a90c7b3e3f1a94f2d2f0..819fcd1c5ea15bbdfec83e0725508cae16d2e1a0 100644 (file)
@@ -28,6 +28,8 @@ table! {
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
+    ap_id -> Varchar,
+    local -> Bool,
   }
 }
 
@@ -227,6 +229,8 @@ table! {
     embed_description -> Nullable<Text>,
     embed_html -> Nullable<Text>,
     thumbnail_url -> Nullable<Text>,
+    ap_id -> Varchar,
+    local -> Bool,
   }
 }