diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 5af89431a..5628c3287 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -489,6 +489,137 @@ Only the first user will be able to be the admin. `PUT /user/mention` +#### Get Private Messages +##### Request +```rust +{ + op: "GetPrivateMessages", + data: { + unread_only: bool, + page: Option, + limit: Option, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "GetPrivateMessages", + data: { + messages: Vec, + } +} +``` + +##### HTTP + +`GET /private_message/list` + +#### Create Private Message +##### Request +```rust +{ + op: "CreatePrivateMessage", + data: { + content: String, + recipient_id: i32, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "CreatePrivateMessage", + data: { + message: PrivateMessageView, + } +} +``` + +##### HTTP + +`POST /private_message` + +#### Edit Private Message +##### Request +```rust +{ + op: "EditPrivateMessage", + data: { + edit_id: i32, + content: String, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "EditPrivateMessage", + data: { + message: PrivateMessageView, + } +} +``` + +##### HTTP + +`PUT /private_message` + +#### Delete Private Message +##### Request +```rust +{ + op: "DeletePrivateMessage", + data: { + edit_id: i32, + deleted: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "DeletePrivateMessage", + data: { + message: PrivateMessageView, + } +} +``` + +##### HTTP + +`POST /private_message/delete` + +#### Mark Private Message as Read +##### Request +```rust +{ + op: "MarkPrivateMessageAsRead", + data: { + edit_id: i32, + read: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "MarkPrivateMessageAsRead", + data: { + message: PrivateMessageView, + } +} +``` + +##### HTTP + +`POST /private_message/mark_as_read` + #### Mark All As Read Marks all user replies and mentions as read. diff --git a/server/lemmy_db/src/private_message.rs b/server/lemmy_db/src/private_message.rs index 3492be2fc..30f40e6b8 100644 --- a/server/lemmy_db/src/private_message.rs +++ b/server/lemmy_db/src/private_message.rs @@ -80,6 +80,50 @@ impl PrivateMessage { .filter(ap_id.eq(object_id)) .first::(conn) } + + pub fn update_content( + conn: &PgConnection, + private_message_id: i32, + new_content: &str, + ) -> Result { + use crate::schema::private_message::dsl::*; + diesel::update(private_message.find(private_message_id)) + .set(content.eq(new_content)) + .get_result::(conn) + } + + pub fn update_deleted( + conn: &PgConnection, + private_message_id: i32, + new_deleted: bool, + ) -> Result { + use crate::schema::private_message::dsl::*; + diesel::update(private_message.find(private_message_id)) + .set(deleted.eq(new_deleted)) + .get_result::(conn) + } + + pub fn update_read( + conn: &PgConnection, + private_message_id: i32, + new_read: bool, + ) -> Result { + use crate::schema::private_message::dsl::*; + diesel::update(private_message.find(private_message_id)) + .set(read.eq(new_read)) + .get_result::(conn) + } + + pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result, Error> { + use crate::schema::private_message::dsl::*; + diesel::update( + private_message + .filter(recipient_id.eq(for_recipient_id)) + .filter(read.eq(false)), + ) + .set(read.eq(true)) + .get_results::(conn) + } } #[cfg(test)] @@ -180,6 +224,10 @@ mod tests { let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap(); let updated_private_message = PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap(); + let deleted_private_message = + PrivateMessage::update_deleted(&conn, inserted_private_message.id, true).unwrap(); + let marked_read_private_message = + PrivateMessage::update_read(&conn, inserted_private_message.id, true).unwrap(); let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap(); User_::delete(&conn, inserted_creator.id).unwrap(); User_::delete(&conn, inserted_recipient.id).unwrap(); @@ -187,6 +235,8 @@ mod tests { assert_eq!(expected_private_message, read_private_message); assert_eq!(expected_private_message, updated_private_message); assert_eq!(expected_private_message, inserted_private_message); + assert!(deleted_private_message.deleted); + assert!(marked_read_private_message.read); assert_eq!(1, num_deleted); } } diff --git a/server/src/api/user.rs b/server/src/api/user.rs index fa1a97662..a83b794a9 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -110,7 +110,7 @@ pub struct GetUserDetailsResponse { moderates: Vec, comments: Vec, posts: Vec, - admins: Vec, + admins: Vec, // TODO why is this necessary, just use GetSite } #[derive(Serialize, Deserialize)] @@ -216,9 +216,21 @@ pub struct CreatePrivateMessage { #[derive(Serialize, Deserialize)] pub struct EditPrivateMessage { edit_id: i32, - content: Option, - deleted: Option, - read: Option, + content: String, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct DeletePrivateMessage { + edit_id: i32, + deleted: bool, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct MarkPrivateMessageAsRead { + edit_id: i32, + read: bool, auth: String, } @@ -974,36 +986,10 @@ impl Perform for Oper { } } - // messages - let messages = blocking(pool, move |conn| { - PrivateMessageQueryBuilder::create(conn, user_id) - .page(1) - .limit(999) - .unread_only(true) - .list() - }) - .await??; - - // TODO: this should probably be a bulk operation - for message in &messages { - let private_message_form = PrivateMessageForm { - content: message.to_owned().content, - creator_id: message.to_owned().creator_id, - recipient_id: message.to_owned().recipient_id, - deleted: None, - read: Some(true), - updated: None, - ap_id: message.to_owned().ap_id, - local: message.local, - published: None, - }; - - let message_id = message.id; - let update_pm = - move |conn: &'_ _| PrivateMessage::update(conn, message_id, &private_message_form); - if blocking(pool, update_pm).await?.is_err() { - return Err(APIError::err("couldnt_update_private_message").into()); - } + // Mark all private_messages as read + let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id); + if blocking(pool, update_pm).await?.is_err() { + return Err(APIError::err("couldnt_update_private_message").into()); } Ok(GetRepliesResponse { replies: vec![] }) @@ -1293,59 +1279,93 @@ impl Perform for Oper { let user_id = claims.id; - let edit_id = data.edit_id; - let orig_private_message = - blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??; - // Check for a site ban let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; if user.banned { return Err(APIError::err("site_ban").into()); } - // Check to make sure they are the creator (or the recipient marking as read - if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id) - || orig_private_message.creator_id.eq(&user_id)) - { + // Checking permissions + let edit_id = data.edit_id; + let orig_private_message = + blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??; + if user_id != orig_private_message.creator_id { return Err(APIError::err("no_private_message_edit_allowed").into()); } - let content_slurs_removed = match &data.content { - Some(content) => remove_slurs(content), - None => orig_private_message.content.clone(), + // Doing the update + let content_slurs_removed = remove_slurs(&data.content); + let edit_id = data.edit_id; + let updated_private_message = match blocking(pool, move |conn| { + PrivateMessage::update_content(conn, edit_id, &content_slurs_removed) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), }; - let private_message_form = { - if data.read.is_some() { - PrivateMessageForm { - content: orig_private_message.content.to_owned(), - creator_id: orig_private_message.creator_id, - recipient_id: orig_private_message.recipient_id, - read: data.read.to_owned(), - updated: orig_private_message.updated, - deleted: Some(orig_private_message.deleted), - ap_id: orig_private_message.ap_id, - local: orig_private_message.local, - published: None, - } - } else { - PrivateMessageForm { - content: content_slurs_removed, - creator_id: orig_private_message.creator_id, - recipient_id: orig_private_message.recipient_id, - deleted: data.deleted.to_owned(), - read: Some(orig_private_message.read), - updated: Some(naive_now()), - ap_id: orig_private_message.ap_id, - local: orig_private_message.local, - published: None, - } - } + // Send the apub update + updated_private_message + .send_update(&user, &self.client, pool) + .await?; + + let edit_id = data.edit_id; + let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??; + let recipient_id = message.recipient_id; + + let res = PrivateMessageResponse { message }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendUserRoomMessage { + op: UserOperation::EditPrivateMessage, + response: res.clone(), + recipient_id, + my_id: ws.id, + }); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &DeletePrivateMessage = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; + let user_id = claims.id; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Checking permissions let edit_id = data.edit_id; + let orig_private_message = + blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??; + if user_id != orig_private_message.creator_id { + return Err(APIError::err("no_private_message_edit_allowed").into()); + } + + // Doing the update + let edit_id = data.edit_id; + let deleted = data.deleted; let updated_private_message = match blocking(pool, move |conn| { - PrivateMessage::update(conn, edit_id, &private_message_form) + PrivateMessage::update_deleted(conn, edit_id, deleted) }) .await? { @@ -1353,38 +1373,93 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), }; - if data.read.is_none() { - if let Some(deleted) = data.deleted.to_owned() { - if deleted { - updated_private_message - .send_delete(&user, &self.client, pool) - .await?; - } else { - updated_private_message - .send_undo_delete(&user, &self.client, pool) - .await?; - } - } else { - updated_private_message - .send_update(&user, &self.client, pool) - .await?; - } + // Send the apub update + if data.deleted { + updated_private_message + .send_delete(&user, &self.client, pool) + .await?; } else { updated_private_message - .send_update(&user, &self.client, pool) + .send_undo_delete(&user, &self.client, pool) .await?; } let edit_id = data.edit_id; let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??; + let recipient_id = message.recipient_id; let res = PrivateMessageResponse { message }; if let Some(ws) = websocket_info { ws.chatserver.do_send(SendUserRoomMessage { - op: UserOperation::EditPrivateMessage, + op: UserOperation::DeletePrivateMessage, + response: res.clone(), + recipient_id, + my_id: ws.id, + }); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &MarkPrivateMessageAsRead = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Checking permissions + let edit_id = data.edit_id; + let orig_private_message = + blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??; + if user_id != orig_private_message.recipient_id { + return Err(APIError::err("couldnt_update_private_message").into()); + } + + // Doing the update + let edit_id = data.edit_id; + let read = data.read; + match blocking(pool, move |conn| { + PrivateMessage::update_read(conn, edit_id, read) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), + }; + + // No need to send an apub update + + let edit_id = data.edit_id; + let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??; + let recipient_id = message.recipient_id; + + let res = PrivateMessageResponse { message }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendUserRoomMessage { + op: UserOperation::MarkPrivateMessageAsRead, response: res.clone(), - recipient_id: orig_private_message.recipient_id, + recipient_id, my_id: ws.id, }); } diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs index 35e495fa5..69ecbc8fd 100644 --- a/server/src/routes/api.rs +++ b/server/src/routes/api.rs @@ -90,7 +90,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .wrap(rate_limit.message()) .route("/list", web::get().to(route_get::)) .route("", web::post().to(route_post::)) - .route("", web::put().to(route_post::)), + .route("", web::put().to(route_post::)) + .route( + "/delete", + web::post().to(route_post::), + ) + .route( + "/mark_as_read", + web::post().to(route_post::), + ), ) // User .service( diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index cdaf4f304..0e938c7ef 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -59,6 +59,8 @@ pub enum UserOperation { PasswordChange, CreatePrivateMessage, EditPrivateMessage, + DeletePrivateMessage, + MarkPrivateMessageAsRead, GetPrivateMessages, UserJoin, GetComments, diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index aef0abb8a..4543781a3 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -448,13 +448,21 @@ impl ChatServer { UserOperation::DeleteAccount => do_user_operation::(args).await, UserOperation::PasswordReset => do_user_operation::(args).await, UserOperation::PasswordChange => do_user_operation::(args).await, + UserOperation::UserJoin => do_user_operation::(args).await, + UserOperation::SaveUserSettings => do_user_operation::(args).await, + + // Private Message ops UserOperation::CreatePrivateMessage => { do_user_operation::(args).await } UserOperation::EditPrivateMessage => do_user_operation::(args).await, + UserOperation::DeletePrivateMessage => { + do_user_operation::(args).await + } + UserOperation::MarkPrivateMessageAsRead => { + do_user_operation::(args).await + } UserOperation::GetPrivateMessages => do_user_operation::(args).await, - UserOperation::UserJoin => do_user_operation::(args).await, - UserOperation::SaveUserSettings => do_user_operation::(args).await, // Site ops UserOperation::GetModlog => do_user_operation::(args).await, diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index 41710e11b..09454b15c 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -9,17 +9,16 @@ import { FollowCommunityForm, CommunityResponse, GetFollowedCommunitiesResponse, - GetPostForm, GetPostResponse, CommentForm, CommentResponse, CommunityForm, - GetCommunityForm, GetCommunityResponse, CommentLikeForm, CreatePostLikeForm, PrivateMessageForm, EditPrivateMessageForm, + DeletePrivateMessageForm, PrivateMessageResponse, PrivateMessagesResponse, GetUserMentionsResponse, @@ -1149,16 +1148,16 @@ describe('main', () => { ); // lemmy alpha deletes the private message - let deletePrivateMessageForm: EditPrivateMessageForm = { + let deletePrivateMessageForm: DeletePrivateMessageForm = { deleted: true, edit_id: createRes.message.id, auth: lemmyAlphaAuth, }; let deleteRes: PrivateMessageResponse = await fetch( - `${lemmyAlphaApiUrl}/private_message`, + `${lemmyAlphaApiUrl}/private_message/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -1182,16 +1181,16 @@ describe('main', () => { expect(getPrivateMessagesDeletedRes.messages.length).toBe(0); // lemmy alpha undeletes the private message - let undeletePrivateMessageForm: EditPrivateMessageForm = { + let undeletePrivateMessageForm: DeletePrivateMessageForm = { deleted: false, edit_id: createRes.message.id, auth: lemmyAlphaAuth, }; let undeleteRes: PrivateMessageResponse = await fetch( - `${lemmyAlphaApiUrl}/private_message`, + `${lemmyAlphaApiUrl}/private_message/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index 8e148921f..faedb4b13 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -446,22 +446,42 @@ export class Inbox extends Component { let found: PrivateMessageI = this.state.messages.find( m => m.id === data.message.id ); - found.content = data.message.content; - found.updated = data.message.updated; - found.deleted = data.message.deleted; - // If youre in the unread view, just remove it from the list - if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) { - this.state.messages = this.state.messages.filter( - r => r.id !== data.message.id - ); - } else { - let found = this.state.messages.find(c => c.id == data.message.id); - found.read = data.message.read; + if (found) { + found.content = data.message.content; + found.updated = data.message.updated; + } + this.setState(this.state); + } else if (res.op == UserOperation.DeletePrivateMessage) { + let data = res.data as PrivateMessageResponse; + let found: PrivateMessageI = this.state.messages.find( + m => m.id === data.message.id + ); + if (found) { + found.deleted = data.message.deleted; + found.updated = data.message.updated; + } + this.setState(this.state); + } else if (res.op == UserOperation.MarkPrivateMessageAsRead) { + let data = res.data as PrivateMessageResponse; + let found: PrivateMessageI = this.state.messages.find( + m => m.id === data.message.id + ); + + if (found) { + found.updated = data.message.updated; + + // If youre in the unread view, just remove it from the list + if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) { + this.state.messages = this.state.messages.filter( + r => r.id !== data.message.id + ); + } else { + let found = this.state.messages.find(c => c.id == data.message.id); + found.read = data.message.read; + } } this.sendUnreadCount(); - window.scrollTo(0, 0); this.setState(this.state); - setupTippy(); } else if (res.op == UserOperation.MarkAllAsRead) { // Moved to be instant } else if (res.op == UserOperation.EditComment) { diff --git a/ui/src/components/private-message-form.tsx b/ui/src/components/private-message-form.tsx index b8dc88539..eb4d49a36 100644 --- a/ui/src/components/private-message-form.tsx +++ b/ui/src/components/private-message-form.tsx @@ -263,7 +263,11 @@ export class PrivateMessageForm extends Component< this.state.loading = false; this.setState(this.state); return; - } else if (res.op == UserOperation.EditPrivateMessage) { + } else if ( + res.op == UserOperation.EditPrivateMessage || + res.op == UserOperation.DeletePrivateMessage || + res.op == UserOperation.MarkPrivateMessageAsRead + ) { let data = res.data as PrivateMessageResponse; this.state.loading = false; this.props.onEdit(data.message); diff --git a/ui/src/components/private-message.tsx b/ui/src/components/private-message.tsx index 71924f0cb..ac7079307 100644 --- a/ui/src/components/private-message.tsx +++ b/ui/src/components/private-message.tsx @@ -2,7 +2,8 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { PrivateMessage as PrivateMessageI, - EditPrivateMessageForm, + DeletePrivateMessageForm, + MarkPrivateMessageAsReadForm, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils'; @@ -243,11 +244,11 @@ export class PrivateMessage extends Component< } handleDeleteClick(i: PrivateMessage) { - let form: EditPrivateMessageForm = { + let form: DeletePrivateMessageForm = { edit_id: i.props.privateMessage.id, deleted: !i.props.privateMessage.deleted, }; - WebSocketService.Instance.editPrivateMessage(form); + WebSocketService.Instance.deletePrivateMessage(form); } handleReplyCancel() { @@ -257,11 +258,11 @@ export class PrivateMessage extends Component< } handleMarkRead(i: PrivateMessage) { - let form: EditPrivateMessageForm = { + let form: MarkPrivateMessageAsReadForm = { edit_id: i.props.privateMessage.id, read: !i.props.privateMessage.read, }; - WebSocketService.Instance.editPrivateMessage(form); + WebSocketService.Instance.markPrivateMessageAsRead(form); } handleMessageCollapse(i: PrivateMessage) { diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index dc860e068..17b5f694a 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -40,6 +40,8 @@ export enum UserOperation { PasswordChange, CreatePrivateMessage, EditPrivateMessage, + DeletePrivateMessage, + MarkPrivateMessageAsRead, GetPrivateMessages, UserJoin, GetComments, @@ -834,9 +836,19 @@ export interface PrivateMessageFormParams { export interface EditPrivateMessageForm { edit_id: number; - content?: string; - deleted?: boolean; - read?: boolean; + content: string; + auth?: string; +} + +export interface DeletePrivateMessageForm { + edit_id: number; + deleted: boolean; + auth?: string; +} + +export interface MarkPrivateMessageAsReadForm { + edit_id: number; + read: boolean; auth?: string; } @@ -864,7 +876,6 @@ export interface UserJoinResponse { } export type MessageType = - | EditPrivateMessageForm | LoginForm | RegisterForm | CommunityForm @@ -900,6 +911,8 @@ export type MessageType = | PasswordChangeForm | PrivateMessageForm | EditPrivateMessageForm + | DeletePrivateMessageForm + | MarkPrivateMessageAsReadForm | GetPrivateMessagesForm | SiteConfigForm; diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index 8e4364d2d..f0fb6fc77 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -36,6 +36,8 @@ import { PasswordChangeForm, PrivateMessageForm, EditPrivateMessageForm, + DeletePrivateMessageForm, + MarkPrivateMessageAsReadForm, GetPrivateMessagesForm, GetCommentsForm, UserJoinForm, @@ -315,6 +317,18 @@ export class WebSocketService { this.ws.send(this.wsSendWrapper(UserOperation.EditPrivateMessage, form)); } + public deletePrivateMessage(form: DeletePrivateMessageForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.DeletePrivateMessage, form)); + } + + public markPrivateMessageAsRead(form: MarkPrivateMessageAsReadForm) { + this.setAuth(form); + this.ws.send( + this.wsSendWrapper(UserOperation.MarkPrivateMessageAsRead, form) + ); + } + public getPrivateMessages(form: GetPrivateMessagesForm) { this.setAuth(form); this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));