diff options
Diffstat (limited to 'server/src')
37 files changed, 3434 insertions, 2452 deletions
diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 56217458..c7406b37 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -1,6 +1,7 @@ use crate::{ api::{APIError, Oper, Perform}, apub::{ApubLikeableType, ApubObjectType}, + blocking, db::{ comment::*, comment_view::*, @@ -27,13 +28,10 @@ use crate::{ UserOperation, WebsocketInfo, }, + DbPool, + LemmyError, MentionData, }; -use diesel::{ - r2d2::{ConnectionManager, Pool}, - PgConnection, -}; -use failure::Error; use log::error; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -97,14 +95,15 @@ pub struct GetCommentsResponse { comments: Vec<CommentView>, } +#[async_trait::async_trait(?Send)] impl Perform for Oper<CreateComment> { type Response = CommentResponse; - fn perform( + async fn perform( &self, - pool: Pool<ConnectionManager<PgConnection>>, + pool: &DbPool, websocket_info: Option<WebsocketInfo>, - ) -> Result<CommentResponse, Error> { + ) -> Result<CommentResponse, LemmyError> { let data: &CreateComment = &self.data; let claims = match Claims::decode(&data.auth) { @@ -114,20 +113,6 @@ impl Perform for Oper<CreateComment> { let user_id = claims.id; - let conn = pool.get()?; - - // Check for a community ban - let post = Post::read(&conn, data.post_id)?; - if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { - return Err(APIError::err("community_ban").into()); - } - - // Check for a site ban - let user = User_::read(&conn, user_id)?; - if user.banned { - return Err(APIError::err("site_ban").into()); - } - let content_slurs_removed = remove_slurs(&data.content.to_owned()); let comment_form = CommentForm { @@ -144,21 +129,48 @@ impl Perform for Oper<CreateComment> { local: true, }; - let inserted_comment = match Comment::create(&conn, &comment_form) { - Ok(comment) => comment, - Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), - }; + // Check for a community ban + let post_id = data.post_id; + let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; + + let community_id = post.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } - let updated_comment = match Comment::update_ap_id(&conn, inserted_comment.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()); + } + + let comment_form2 = comment_form.clone(); + let inserted_comment = + match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? { + Ok(comment) => comment, + Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), + }; + + let inserted_comment_id = inserted_comment.id; + let updated_comment: Comment = match blocking(pool, move |conn| { + Comment::update_ap_id(&conn, inserted_comment_id) + }) + .await? + { Ok(comment) => comment, Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), }; - updated_comment.send_create(&user, &conn)?; + updated_comment + .send_create(&user, &self.client, pool) + .await?; // Scan the comment for user mentions, add those rows let mentions = scrape_text_for_mentions(&comment_form.content); - let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post); + let recipient_ids = + send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?; // You like your own comment by default let like_form = CommentLikeForm { @@ -168,14 +180,17 @@ impl Perform for Oper<CreateComment> { score: 1, }; - let _inserted_like = match CommentLike::like(&conn, &like_form) { - Ok(like) => like, - Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), - }; + let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form); + if blocking(pool, like).await?.is_err() { + return Err(APIError::err("couldnt_like_comment").into()); + } - updated_comment.send_like(&user, &conn)?; + updated_comment.send_like(&user, &self.client, pool).await?; - let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?; + let comment_view = blocking(pool, move |conn| { + CommentView::read(&conn, inserted_comment.id, Some(user_id)) + }) + .await??; let mut res = CommentResponse { comment: comment_view, @@ -198,14 +213,15 @@ impl Perform for Oper<CreateComment> { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper<EditComment> { type Response = CommentResponse; - fn perform( + async fn perform( &self, - pool: Pool<ConnectionManager<PgConnection>>, + pool: &DbPool, websocket_info: Option<WebsocketInfo>, - ) -> Result<CommentResponse, Error> { + ) -> Result<CommentResponse, LemmyError> { let data: &EditComment = &self.data; let claims = match Claims::decode(&data.auth) { @@ -215,30 +231,44 @@ impl Perform for Oper<EditComment> { let user_id = claims.id; - let conn = pool.get()?; + let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??; - let user = User_::read(&conn, user_id)?; - - let orig_comment = CommentView::read(&conn, data.edit_id, None)?; + let edit_id = data.edit_id; + let orig_comment = + blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??; // You are allowed to mark the comment as read even if you're banned. if data.read.is_none() { // Verify its the creator or a mod, or an admin let mut editors: Vec<i32> = vec![data.creator_id]; + let community_id = orig_comment.community_id; + editors.append( + &mut blocking(pool, move |conn| { + Ok( + CommunityModeratorView::for_community(&conn, community_id)? + .into_iter() + .map(|m| m.user_id) + .collect(), + ) as Result<_, LemmyError> + }) + .await??, + ); editors.append( - &mut CommunityModeratorView::for_community(&conn, orig_comment.community_id)? - .into_iter() - .map(|m| m.user_id) - .collect(), + &mut blocking(pool, move |conn| { + Ok(UserView::admins(conn)?.into_iter().map(|a| a.id).collect()) as Result<_, LemmyError> + }) + .await??, ); - editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect()); if !editors.contains(&user_id) { return Err(APIError::err("no_comment_edit_allowed").into()); } // Check for a community ban - if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() { + let community_id = orig_comment.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { return Err(APIError::err("community_ban").into()); } @@ -250,7 +280,8 @@ impl Perform for Oper<EditComment> { let content_slurs_removed = remove_slurs(&data.content.to_owned()); - let read_comment = Comment::read(&conn, data.edit_id)?; + let edit_id = data.edit_id; + let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??; let comment_form = CommentForm { content: content_slurs_removed, @@ -270,31 +301,48 @@ impl Perform for Oper<EditComment> { local: read_comment.local, }; - let updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) { + let edit_id = data.edit_id; + let comment_form2 = comment_form.clone(); + let updated_comment = match blocking(pool, move |conn| { + Comment::update(conn, edit_id, &comment_form2) + }) + .await? + { Ok(comment) => comment, Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; if let Some(deleted) = data.deleted.to_owned() { if deleted { - updated_comment.send_delete(&user, &conn)?; + updated_comment + .send_delete(&user, &self.client, pool) + .await?; } else { - updated_comment.send_undo_delete(&user, &conn)?; + updated_comment + .send_undo_delete(&user, &self.client, pool) + .await?; } } else if let Some(removed) = data.removed.to_owned() { if removed { - updated_comment.send_remove(&user, &conn)?; + updated_comment + .send_remove(&user, &self.client, pool) + .await?; } else { - updated_comment.send_undo_remove(&user, &conn)?; + updated_comment + .send_undo_remove(&user, &self.client, pool) + .await?; } } else { - updated_comment.send_update(&user, &conn)?; + updated_comment + .send_update(&user, &self.client, pool) + .await?; } - let post = Post::read(&conn, data.post_id)?; + let post_id = data.post_id; + let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; let mentions = scrape_text_for_mentions(&comment_form.content); - let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post); + let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?; // Mod tables if let Some(removed) = data.removed.to_owned() { @@ -304,10 +352,14 @@ impl Perform for Oper<EditComment> { removed: Some(removed), reason: data.reason.to_owned(), }; - ModRemoveComment::create(&conn, &form)?; + blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??; } - let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?; + let edit_id = data.edit_id; + let comment_view = blocking(pool, move |conn| { + CommentView::read(conn, edit_id, Some(user_id)) + }) + .await??; let mut res = CommentResponse { comment: comment_view, @@ -330,14 +382,15 @@ impl Perform for Oper<EditComment> { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper<SaveComment> { type Response = CommentResponse; - fn perform( + async fn perform( &self, - pool: Pool<ConnectionManager<PgConnection>>, + pool: &DbPool, _websocket_info: Option<WebsocketInfo>, - ) -> Result<CommentResponse, Error> { + ) -> Result<CommentResponse, LemmyError> { let data: &SaveComment = &self.data; let claims = match Claims::decode(&data.auth) { @@ -352,21 +405,23 @@ impl Perform for Oper<SaveComment> { user_id, }; - let conn = pool.get()?; - if data.save { - match CommentSaved::save(&conn, &comment_saved_form) { - Ok(comment) => comment, - Err(_e) => return Err(APIError::err("couldnt_save_comment").into()), - }; + let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form); + if blocking(pool, save_comment).await?.is_err() { + return Err(APIError::err("couldnt_save_comment").into()); + } } else { - match CommentSaved::unsave(&conn, &comment_saved_form) { - Ok(comment) => comment, - Err(_e) => return Err(APIError::err("couldnt_save_comment").into()), - }; + let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form); + if blocking(pool, unsave_comment).await?.is_err() { + return Err(APIError::err("couldnt_save_comment").into()); + } } - let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?; + let comment_id = data.comment_id; + let comment_view = blocking(pool, move |conn| { + CommentView::read(conn, comment_id, Some(user_id)) + }) + .await??; Ok(CommentResponse { comment: comment_view, @@ -375,14 +430,15 @@ impl Perform for Oper<SaveComment> { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper<CreateCommentLike> { type Response = CommentResponse; - fn perform( + async fn perform( &self, - pool: Pool<ConnectionManager<PgConnection>>, + pool: &DbPool, websocket_info: Option<WebsocketInfo>, - ) -> Result<CommentResponse, Error> { + ) -> Result<CommentResponse, LemmyError> { let data: &CreateCommentLike = &self.data; let claims = match Claims::decode(&data.auth) { @@ -394,36 +450,42 @@ impl Perform for Oper<CreateCommentLike> { let mut recipient_ids = Vec::new(); - let conn = pool.get()?; - // Don't do a downvote if site has downvotes disabled if data.score == -1 { - let site = SiteView::read(&conn)?; + let site = blocking(pool, move |conn| SiteView::read(conn)).await??; if !site.enable_downvotes { return Err(APIError::err("downvotes_disabled").into()); } } // Check for a community ban - let post = Post::read(&conn, data.post_id)?; - if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { + let post_id = data.post_id; + let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; + let community_id = post.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { return Err(APIError::err("community_ban").into()); } // Check for a site ban - let user = User_::read(&conn, user_id)?; + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; if user.banned { return Err(APIError::err("site_ban").into()); } - let comment = Comment::read(&conn, data.comment_id)?; + let comment_id = data.comment_id; + let comment = blocking(pool, move |conn| Comment::read(conn, comment_id)).await??; // Add to recipient ids match comment.parent_id { Some(parent_id) => { - let parent_comment = Comment::read(&conn, parent_id)?; + let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??; if parent_comment.creator_id != user_id { - let parent_user = User_::read(&conn, parent_comment.creator_id)?; + let parent_user = blocking(pool, move |conn| { + User_::read(conn, parent_comment.creator_id) + }) + .await??; recipient_ids.push(parent_user.id); } } @@ -440,27 +502,33 @@ impl Perform for Oper<CreateCommentLike> { }; // Remove any likes first - CommentLike::remove(&conn, &like_form)?; + let like_form2 = like_form.clone(); + blocking(pool, move |conn| CommentLike::remove(conn, &like_form2)).await??; // Only add the like if the score isnt 0 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); if do_add { - let _inserted_like = match CommentLike::like(&conn, &like_form) { - Ok(like) => like, - Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), - }; + let like_form2 = like_form.clone(); + let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2); + if blocking(pool, like).await?.is_err() { + return Err(APIError::err("couldnt_like_comment").into()); + } if like_form.score == 1 { - comment.send_like(&user, &conn)?; + comment.send_like(&user, &self.client, pool).await?; } else if like_form.score == -1 { - comment.send_dislike(&user, &conn)?; + comment.send_dislike(&user, &self.client, pool).await?; } } else { - comment.send_undo_like(&user, &conn)?; + comment.send_undo_like(&user, &self.client, pool).await?; } // Have to refetch the comment to get the current state - let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?; + let comment_id = data.comment_id; + let liked_comment = blocking(pool, move |conn| { + CommentView::read(conn, comment_id, Some(user_id)) + }) + .await??; let mut res = CommentResponse { comment: liked_comment, @@ -483,14 +551,15 @@ impl Perform for Oper<CreateCommentLike> { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper<GetComments> { type Response = GetCommentsResponse; - fn perform( + async fn perform( &self, - pool: Pool<ConnectionManager<PgConnection>>, + pool: &DbPool, websocket_info: Option<WebsocketInfo>, - ) -> Result<GetCommentsResponse, Error> { + ) -> Result<GetCommentsResponse, LemmyError> { let data: &GetComments = &self.data; let user_claims: Option<Claims> = match &data.auth { @@ -509,19 +578,23 @@ impl Perform for Oper<GetComments> { let type_ = ListingType::from_str(&data.type_)?; let sort = SortType::from_str(&data.sort)?; - let conn = pool.get()?; - - let comments = match CommentQueryBuilder::create(&conn) - .listing_type(type_) - .sort(&sort) - .for_community_id(data.community_id) - .my_user_id(user_id) - .page(data.page) - .limit(data.limit) - .list() - { + let community_id = data.community_id; + let page = data.page; + let limit = data.limit; + let comments = blocking(pool, move |conn| { + CommentQueryBuilder::create(conn) + .listing_type(type_) + .sort(&sort) + .for_community_id(community_id) + .my_user_id(user_id) + .page(page) + .limit(limit) + .list() + }) + .await?; + let comments = match comments { Ok(comments) => comments, - Err(_e) => return Err(APIError::err("couldnt_get_comments").into()), + Err(_) => return Err(APIError::err("couldnt_get_comments").into()), }; if let Some(ws) = websocket_info { @@ -542,8 +615,23 @@ impl Perform for Oper<GetComments> { } } -pub fn send_local_notifs( - conn: &PgConnection, +pub async fn send_local_notifs( + mentions: Vec<MentionData>, + comment: Comment, + user: User_, + post: Post, + pool: &DbPool, +) -> Result<Vec<i32>, LemmyError> { + let ids = blocking(pool, move |conn| { + do_send_local_notifs(conn, &mentions, &comment, &user, &post) + }) + .await?; + + Ok(ids) +} + +fn do_send_local_notifs( + conn: &diesel::PgConnection, mentions: &[MentionData], comment: &Comment, user: &User_, diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 3fc67eb3..02071c57 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -7,6 +7,7 @@ use crate::{ ActorType, EndpointType, }, + blocking, db::{Bannable, Crud, Followable, Joinable, SortType}, is_valid_community_name, naive_from_unix, @@ -18,12 +19,9 @@ use crate::{ UserOperation, WebsocketInfo, }, + DbPool, + LemmyError, }; -use diesel::{ - r2d2::{ConnectionManager, Pool}, - PgConnection, -}; -use failure::Error; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -138,14 +136,15 @@ pub struct TransferCommunity { auth: String, } +#[async_trait::async_trait(?Send)] impl Perform for Oper<GetCommunity> { type Response = GetCommunityResponse; - fn perform( + async fn perform( &self, - pool: Pool<ConnectionManager<PgConnection>>, + pool: &DbPool, websocket_info: Option<WebsocketInfo>, - ) -> Result<GetCommunityResponse, Error> { + ) -> Result<GetCommunityResponse, LemmyError> { let data: &GetCommunity = &self.data; let user_id: Option<i32> = match &data.auth { @@ -159,33 +158,38 @@ impl Perform for Oper<GetCommunity> { None => None, }; - let conn = pool.get()?; - + let name = data.name.to_owned().unwrap_or_else(|| "main".to_string()); let community = match data.id { - Some(id) => Community::read(&conn, id)?, - None => { - match Community::read_from_name( - &conn, - &data.name.to_owned().unwrap_or_else(|| "main".to_string()), - ) { - Ok(community) => community, - Err(_e) => return Err(APIError::err("couldnt_find_community").into()), - } - } + Some(id) => blocking(pool, move |conn| Community::read(conn, id)).await??, + None => match blocking(pool, move |conn| Community::read_from_name(conn, &name)).await? { + Ok(community) => community, + Err(_e) => return Err(APIError::err("couldnt_find_community").into()), + }, }; - let community_view = match CommunityView::read(&conn, community.id, user_id) { + let community_id = community.id; + let community_view = match blocking(pool, move |conn| { + CommunityView::read(conn, community_id, user_id) + }) + .await? + { Ok(community) => community, Err(_e) => return Err(APIError::err("couldnt_find_community").into()), }; - let moderators = match CommunityModeratorView::for_community(&conn, community.id) { + let community_id = community.id; + let moderators: Vec<CommunityModeratorView> = match blocking(pool, move |conn| { + CommunityModeratorView::for_community(conn, community_id) + }) + .await? + { Ok(moderators) => moderators, Err(_e) => return Err(APIError::err("couldnt_find_community").into()), }; - let site_creator_id = Site::read(&conn, 1)?.creator_id; - let mut admins = UserView::admins(&conn)?; + let site = blocking(pool, move |conn| Site::read(conn, 1)).await??; + let site_creator_id = site.creator_id; + let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??; let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); let creator_user = admins.remove(creator_index); admins.insert(0, creator_user); @@ -220,14 +224,15 @@ impl Perform for Oper<GetCommunity> { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper<CreateCommunity> { type Response = CommunityResponse; - fn perform( + async fn perform( &self, - pool: Pool<ConnectionManager<PgConnection>>, + pool: &DbPool, _websocket_info: Option<WebsocketInfo>, - ) -> Result<CommunityResponse, Error> { + ) -> Result<CommunityResponse, LemmyError> { let data: &CreateCommunity = &self.data; let claims = match Claims::decode(&data.auth) { @@ -255,10 +260,9 @@ impl Perform for Oper<CreateCommunity> { let user_id = claims.id; - let conn = pool.get()?; - // Check for a site ban - if UserView::read(&conn, user_id)?.banned { + let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??; + if user_view.banned { return Err(APIError::err("site_ban").into()); } @@ -283,34 +287,36 @@ impl Perform for Oper<CreateCommunity> { published: None, }; - let inserted_community = match Community::create(&conn, &community_form) { - Ok(community) => community, - Err(_e) => return Err(APIError::err("community_already_exists").into()), - }; + let inserted_community = + match blocking(pool, move |conn| Community::create(conn, &community_form)).await? { + Ok(community) => community, + Err(_e) => return Err(APIError::err("community_already_exists").into()), + }; let community_moderator_form = CommunityModeratorForm { community_id: inserted_community.id, user_id, }; - let _inserted_community_moderator = - match CommunityModerator::join(&conn, &community_moderator_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), - }; + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + if blocking(pool, join).await?.is_err() { + return Err(APIError::err("community_moderator_already_exists").into()); + } let community_follower_form = CommunityFollowerForm { community_id: inserted_community.id, user_id, }; - let _inserted_community_follower = - match CommunityFollower::follow(&conn, &community_follower_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), - }; + let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); + if blocking(pool, follow).await?.is_err() { + return Err(APIError::err("community_follower_already_exists").into()); + } - let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?; + let community_view = blocking(pool, move |conn| { + CommunityView::read(conn, inserted_community.id, Some(user_id)) + }) + .await??; Ok(CommunityResponse { community: community_view, @@ -318,14 +324,15 @@ impl Perform for Oper<CreateCommunity> { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper<EditCommunity> { type Response = CommunityResponse; - fn perform( + async fn perform( &self, - pool: Pool<ConnectionManager<PgConnection>>, + pool: &DbPool, websocket_info: Option<WebsocketInfo>, - ) -> Result<CommunityResponse, Error> { + ) -> Result<CommunityResponse, LemmyError> { let data: &EditCommunity = &self.data; if let Err(slurs) = slur_check(&data.name) { @@ -353,28 +360,34 @@ impl Perform for Oper<EditCommunity> { let user_id = claims.id; - let conn = pool.get()?; - // Check for a site ban - let user = User_::read(&conn, user_id)?; + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; if user.banned { return Err(APIError::err("site_ban").into()); } // Verify its a mod + let edit_id = data.edit_id; let mut editors: Vec<i32> = Vec::new(); editors.append( - &mut CommunityModeratorView::for_community(&conn, data.edit_id)? - .into_iter() - .map(|m| m.user_id) - .collect(), + &mut blocking(pool, move |conn| { + CommunityModeratorView::for_community(conn, edit_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect()) + }) + .await??, + ); + editors.append( + &m |