mirror of
				https://gitlab.com/comunic/comunicapiv3
				synced 2025-10-30 23:24:42 +00:00 
			
		
		
		
	Can create group conversation
This commit is contained in:
		| @@ -67,4 +67,5 @@ pub mod call_peer_ready; | ||||
| pub mod call_peer_interrupted_streaming; | ||||
| pub mod res_check_password_token; | ||||
| pub mod removed_user_from_conv_message; | ||||
| pub mod user_is_writing_message_in_conversation; | ||||
| pub mod user_is_writing_message_in_conversation; | ||||
| pub mod res_create_conversation_for_group; | ||||
							
								
								
									
										20
									
								
								src/api_data/res_create_conversation_for_group.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/api_data/res_create_conversation_for_group.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| //! # Conversation for group creation result | ||||
| //! | ||||
| //! This structure returns the ID of the create conversation | ||||
|  | ||||
| use serde::Serialize; | ||||
|  | ||||
| use crate::data::conversation::ConvID; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| pub struct ResCreateConversationForGroup { | ||||
|     conv_id: u64 | ||||
| } | ||||
|  | ||||
| impl ResCreateConversationForGroup { | ||||
|     pub fn new(conv_id: ConvID) -> Self { | ||||
|         Self { | ||||
|             conv_id: conv_id.id() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -45,6 +45,7 @@ pub fn create(r: &mut HttpRequestHandler) -> RequestResult { | ||||
|         can_everyone_add_members: r.post_bool_opt("canEveryoneAddMembers", true), | ||||
|         color: r.post_color_opt("color")?, | ||||
|         group_id: None, | ||||
|         group_min_membership_level: None, | ||||
|         logo: None, | ||||
|     }; | ||||
|  | ||||
| @@ -146,7 +147,7 @@ pub fn add_member(r: &mut HttpRequestHandler) -> RequestResult { | ||||
|         r.bad_request("This user is already a member of this conversation!".to_string())?; | ||||
|     } | ||||
|  | ||||
|     conversations_helper::add_member(conv.id, &user_to_add, true, false, r.user_id_ref()?)?; | ||||
|     conversations_helper::add_member(conv.id, &user_to_add, true, false, Some(r.user_id_ref()?))?; | ||||
|  | ||||
|     r.success("The user was added to the conversation!") | ||||
| } | ||||
| @@ -194,7 +195,7 @@ pub fn remove_member(r: &mut HttpRequestHandler) -> RequestResult { | ||||
|         r.bad_request("This user is not a member of this conversation!".to_string())?; | ||||
|     } | ||||
|  | ||||
|     conversations_helper::remove_member(&user_to_remove, conv.id, r.user_id_ref()?)?; | ||||
|     conversations_helper::remove_member(&user_to_remove, conv.id, Some(r.user_id_ref()?))?; | ||||
|  | ||||
|     r.ok() | ||||
| } | ||||
| @@ -225,6 +226,7 @@ pub fn find_private(r: &mut HttpRequestHandler) -> RequestResult { | ||||
|             color: None, | ||||
|             logo: None, | ||||
|             group_id: None, | ||||
|             group_min_membership_level: None, | ||||
|         }; | ||||
|         let conv_id = conversations_helper::create(&new_conv)?; | ||||
|         list.push(conv_id); | ||||
|   | ||||
| @@ -8,20 +8,35 @@ use crate::api_data::advanced_group_api::AdvancedGroupApi; | ||||
| use crate::api_data::group_api::GroupApi; | ||||
| use crate::api_data::group_member_api::GroupMemberAPI; | ||||
| use crate::api_data::res_change_group_logo::ResChangeGroupLogo; | ||||
| use crate::api_data::res_create_conversation_for_group::ResCreateConversationForGroup; | ||||
| use crate::api_data::res_create_group::GroupCreationResult; | ||||
| use crate::constants::{DEFAULT_GROUP_LOGO, PATH_GROUPS_LOGOS}; | ||||
| use crate::data::base_request_handler::BaseRequestHandler; | ||||
| use crate::data::error::Res; | ||||
| use crate::data::group::{Group, GroupAccessLevel, GroupPostsCreationLevel, GroupRegistrationLevel, GroupVisibilityLevel}; | ||||
| use crate::data::group_id::GroupID; | ||||
| use crate::data::group_member::{GroupMember, GroupMembershipLevel}; | ||||
| use crate::data::http_request_handler::HttpRequestHandler; | ||||
| use crate::data::new_group::NewGroup; | ||||
| use crate::data::notification::NotifEventType; | ||||
| use crate::helpers::{groups_helper, notifications_helper, virtual_directory_helper}; | ||||
| use crate::helpers::{conversations_helper, groups_helper, notifications_helper, virtual_directory_helper}; | ||||
| use crate::helpers::virtual_directory_helper::VirtualDirType; | ||||
| use crate::routes::RequestResult; | ||||
| use crate::utils::date_utils::time; | ||||
|  | ||||
| impl HttpRequestHandler { | ||||
|     /// Get membership level for a conversation | ||||
|     pub fn post_group_membership_level_for_conversation(&mut self, name: &str) -> Res<GroupMembershipLevel> { | ||||
|         let level = GroupMembershipLevel::from_api(&self.post_string(name)?); | ||||
|  | ||||
|         if !level.is_at_least_member() { | ||||
|             self.bad_request("Specified membership level is not enough!".to_string())?; | ||||
|         } | ||||
|  | ||||
|         Ok(level) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Create a new group | ||||
| pub fn create(r: &mut HttpRequestHandler) -> RequestResult { | ||||
|     let new_group = NewGroup { | ||||
| @@ -153,6 +168,17 @@ pub fn delete_logo(r: &mut HttpRequestHandler) -> RequestResult { | ||||
|     r.set_response(ResChangeGroupLogo::new(DEFAULT_GROUP_LOGO)) | ||||
| } | ||||
|  | ||||
| /// Create a new group's conversation | ||||
| pub fn create_conversation(r: &mut HttpRequestHandler) -> RequestResult { | ||||
|     let group = r.post_group_id_with_access("group_id", GroupAccessLevel::ADMIN_ACCESS)?; | ||||
|     let min_membership_level = r.post_group_membership_level_for_conversation("min_membership_level")?; | ||||
|     let name = r.post_string("name")?; | ||||
|  | ||||
|     let conv_id = conversations_helper::create_conversation_for_group(group, min_membership_level, &name)?; | ||||
|  | ||||
|     r.set_response(ResCreateConversationForGroup::new(conv_id)) | ||||
| } | ||||
|  | ||||
| /// Get the list of members of a group | ||||
| pub fn get_members(r: &mut HttpRequestHandler) -> RequestResult { | ||||
|     let group_id = r.post_group_id("id")?; | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
|  | ||||
| use crate::data::group_id::GroupID; | ||||
| use crate::data::user::UserID; | ||||
| use crate::data::group_member::GroupMembershipLevel; | ||||
|  | ||||
| #[derive(Copy, Debug, PartialEq, Eq, Clone, Hash)] | ||||
| pub struct ConvID(u64); | ||||
| @@ -38,6 +39,7 @@ pub struct Conversation { | ||||
|     pub logo: Option<String>, | ||||
|     pub creation_time: u64, | ||||
|     pub group_id: Option<GroupID>, | ||||
|     pub min_group_membership_level: Option<GroupMembershipLevel>, | ||||
|     pub can_everyone_add_members: bool, | ||||
|     pub last_activity: u64, | ||||
|     pub members: Vec<ConversationMember>, | ||||
| @@ -59,6 +61,11 @@ impl Conversation { | ||||
|  | ||||
|     /// Check out whether this conversation is managed or not | ||||
|     pub fn is_managed(&self) -> bool { | ||||
|         self.is_linked_to_group() | ||||
|     } | ||||
|  | ||||
|     /// Check if this conversation is linked to a group | ||||
|     pub fn is_linked_to_group(&self) -> bool { | ||||
|         self.group_id.is_some() | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| use crate::data::group_id::GroupID; | ||||
| use crate::data::user::UserID; | ||||
|  | ||||
| #[derive(PartialEq, Eq, PartialOrd)] | ||||
| #[derive(PartialEq, Eq, PartialOrd, Debug)] | ||||
| pub enum GroupMembershipLevel { | ||||
|     ADMINISTRATOR = 0, | ||||
|     MODERATOR = 1, | ||||
| @@ -20,6 +20,13 @@ pub enum GroupMembershipLevel { | ||||
| } | ||||
|  | ||||
| impl GroupMembershipLevel { | ||||
|     pub fn is_at_least_member(&self) -> bool { | ||||
|         matches!( | ||||
|             &self, | ||||
|             GroupMembershipLevel::ADMINISTRATOR | GroupMembershipLevel::MODERATOR | GroupMembershipLevel::MEMBER | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     pub fn to_api(&self) -> String { | ||||
|         match self { | ||||
|             GroupMembershipLevel::ADMINISTRATOR => "administrator", | ||||
| @@ -54,6 +61,10 @@ pub struct GroupMember { | ||||
| } | ||||
|  | ||||
| impl GroupMember { | ||||
|     pub fn is_admin(&self) -> bool { | ||||
|         self.level == GroupMembershipLevel::ADMINISTRATOR | ||||
|     } | ||||
|  | ||||
|     /// Check if a member of a group is a least a moderator of this group | ||||
|     pub fn is_moderator(&self) -> bool { | ||||
|         self.level <= GroupMembershipLevel::MODERATOR | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| use std::collections::HashSet; | ||||
|  | ||||
| use crate::data::group_id::GroupID; | ||||
| use crate::data::group_member::GroupMembershipLevel; | ||||
| use crate::data::user::UserID; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| @@ -12,6 +13,7 @@ pub struct NewConversation { | ||||
|     pub owner_id: UserID, | ||||
|     pub name: Option<String>, | ||||
|     pub group_id: Option<GroupID>, | ||||
|     pub group_min_membership_level: Option<GroupMembershipLevel>, | ||||
|     pub color: Option<String>, | ||||
|     pub logo: Option<String>, | ||||
|     pub owner_following: bool, | ||||
|   | ||||
| @@ -6,10 +6,12 @@ use crate::constants::database_tables_names::{CONV_LIST_TABLE, CONV_MEMBERS_TABL | ||||
| use crate::data::conversation::{Conversation, ConversationMember, ConvID, NewConversationSettings}; | ||||
| use crate::data::conversation_message::{ConversationMessage, ConversationMessageFile, ConversationServerMessageType, UserAddedAnotherUserToConversation, UserRemovedAnotherUserToConversation}; | ||||
| use crate::data::error::{ExecError, Res, ResultBoxError}; | ||||
| use crate::data::group_id::GroupID; | ||||
| use crate::data::group_member::GroupMembershipLevel; | ||||
| use crate::data::new_conversation::NewConversation; | ||||
| use crate::data::new_conversation_message::NewConversationMessage; | ||||
| use crate::data::user::{User, UserID}; | ||||
| use crate::helpers::{database, events_helper}; | ||||
| use crate::helpers::{database, events_helper, groups_helper}; | ||||
| use crate::helpers::database::{InsertQuery, UpdateInfo}; | ||||
| use crate::helpers::events_helper::Event; | ||||
| use crate::utils::date_utils::time; | ||||
| @@ -26,25 +28,47 @@ pub fn create(conv: &NewConversation) -> Res<ConvID> { | ||||
|         .add_opt_str("logo", Option::from(&conv.logo)) | ||||
|         .add_legacy_bool("can_everyone_add_members", conv.can_everyone_add_members) | ||||
|         .add_opt_group_id("group_id", conv.group_id.clone()) | ||||
|         .add_opt_u32("min_group_membership_level", | ||||
|                      conv.group_min_membership_level.as_ref().map(|s| s.to_db())) | ||||
|         .insert()? | ||||
|         .map(|i| ConvID::new(i)) | ||||
|         .ok_or(ExecError::new("missing result conv id!"))?; | ||||
|  | ||||
|     // Add the creator of the conversation | ||||
|     add_member(conv_id, &conv.owner_id, conv.owner_following, true, &conv.owner_id)?; | ||||
|     // Initialize the list of members of the group | ||||
|     if conv.group_id.is_some() { | ||||
|         update_members_list_for_group_conversation(conv_id)?; | ||||
|     } else { | ||||
|         // Add the creator of the conversation | ||||
|         add_member(conv_id, &conv.owner_id, conv.owner_following, true, Some(&conv.owner_id))?; | ||||
|  | ||||
|     // Add other members to the conversation | ||||
|     for member in &conv.members { | ||||
|         if !member.eq(&conv.owner_id) { | ||||
|             add_member(conv_id, member, true, false, &conv.owner_id)?; | ||||
|         // Add other members to the conversation | ||||
|         for member in &conv.members { | ||||
|             if !member.eq(&conv.owner_id) { | ||||
|                 add_member(conv_id, member, true, false, Some(&conv.owner_id))?; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(conv_id) | ||||
| } | ||||
|  | ||||
| /// Create a conversation for a group | ||||
| pub fn create_conversation_for_group(group_id: GroupID, min_membership_level: GroupMembershipLevel, name: &String) -> Res<ConvID> { | ||||
|     create(&NewConversation { | ||||
|         owner_id: UserID::invalid(), | ||||
|         name: Some(name.to_string()), | ||||
|         group_id: Some(group_id), | ||||
|         group_min_membership_level: Some(min_membership_level), | ||||
|         color: None, | ||||
|         logo: None, | ||||
|         owner_following: false, | ||||
|         members: Default::default(), | ||||
|         can_everyone_add_members: false, | ||||
|     }) | ||||
| } | ||||
|  | ||||
| /// Add a member to a conversation | ||||
| pub fn add_member(conv_id: ConvID, user_id: &UserID, following: bool, admin: bool, adder: &UserID) -> Res { | ||||
| pub fn add_member(conv_id: ConvID, user_id: &UserID, following: bool, admin: bool, adder: Option<&UserID>) -> Res { | ||||
|     InsertQuery::new(CONV_MEMBERS_TABLE) | ||||
|         .add_conv_id("conv_id", conv_id) | ||||
|         .add_user_id("user_id", user_id) | ||||
| @@ -54,21 +78,25 @@ pub fn add_member(conv_id: ConvID, user_id: &UserID, following: bool, admin: boo | ||||
|         .add_u64("last_message_seen", 0) | ||||
|         .insert()?; | ||||
|  | ||||
|     // Create a message | ||||
|     if adder != user_id { | ||||
|         send_message( | ||||
|             &NewConversationMessage::new_server_message( | ||||
|                 conv_id, | ||||
|                 ConversationServerMessageType::UserAddedAnotherUserToConversation(UserAddedAnotherUserToConversation { | ||||
|                     user_who_added: adder.clone(), | ||||
|                     user_added: user_id.clone(), | ||||
|                 }), | ||||
|             ) | ||||
|         )?; | ||||
|     } else { | ||||
|         send_message(&NewConversationMessage::new_server_message( | ||||
|             conv_id, ConversationServerMessageType::UserCreatedConversation(user_id.clone()), | ||||
|         ))?; | ||||
|     // Send the messages (if possible, no messages are created for groups conversations) | ||||
|     if let Some(adder) = adder { | ||||
|  | ||||
|         // Create a message | ||||
|         if adder != user_id { | ||||
|             send_message( | ||||
|                 &NewConversationMessage::new_server_message( | ||||
|                     conv_id, | ||||
|                     ConversationServerMessageType::UserAddedAnotherUserToConversation(UserAddedAnotherUserToConversation { | ||||
|                         user_who_added: adder.clone(), | ||||
|                         user_added: user_id.clone(), | ||||
|                     }), | ||||
|                 ) | ||||
|             )?; | ||||
|         } else { | ||||
|             send_message(&NewConversationMessage::new_server_message( | ||||
|                 conv_id, ConversationServerMessageType::UserCreatedConversation(user_id.clone()), | ||||
|             ))?; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| @@ -427,10 +455,50 @@ pub fn remove_user_from_conversation(user_id: &UserID, conv: &Conversation, remo | ||||
|     if conv.is_last_admin(user_id) { | ||||
|         delete_conversation(conv) | ||||
|     } else { | ||||
|         remove_member(user_id, conv.id, remover) | ||||
|         remove_member(user_id, conv.id, Some(remover)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Update the list of members for a group conversation | ||||
| pub fn update_members_list_for_group_conversation(conv_id: ConvID) -> Res { | ||||
|     let conv = get_single(conv_id)?; | ||||
|  | ||||
|     if !conv.is_linked_to_group() { | ||||
|         return Err(ExecError::boxed_new("Attempted to update members list for a non-group conversation!")); | ||||
|     } | ||||
|  | ||||
|     let group_members = groups_helper::get_list_members(conv.group_id.as_ref().unwrap())?; | ||||
|  | ||||
|     // Add missing memberships / Update existing invalid memberships | ||||
|     for member in &group_members { | ||||
|         let conv_member = conv.members.iter().filter(|f| f.user_id == member.user_id).next(); | ||||
|  | ||||
|         if let Some(conv_member) = conv_member { | ||||
|             // Update admin status, if required | ||||
|             if conv_member.is_admin != member.is_admin() { | ||||
|                 set_admin(&conv_id, &member.user_id, member.is_admin())?; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Create the member | ||||
|         else if conv.min_group_membership_level.as_ref().unwrap() >= &member.level { | ||||
|             add_member(conv_id, &member.user_id, true, member.is_admin(), None)?; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Remove memberships that have to be removed | ||||
|     for conv_member in &conv.members { | ||||
|         let member = group_members.iter().filter(|m| m.user_id == conv_member.user_id).next(); | ||||
|  | ||||
|         // Remove the member, if required | ||||
|         if member.is_none() || conv.min_group_membership_level.as_ref().unwrap() < &member.unwrap().level { | ||||
|             remove_member(&conv_member.user_id, conv_id, None)?; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Remove permanently a conversation | ||||
| pub fn delete_conversation(conv: &Conversation) -> ResultBoxError<()> { | ||||
|     // Delete all the messages of the conversations | ||||
| @@ -460,7 +528,7 @@ pub fn delete_conversation(conv: &Conversation) -> ResultBoxError<()> { | ||||
| } | ||||
|  | ||||
| /// Delete a conversation membership | ||||
| pub fn remove_member(user_id: &UserID, conv_id: ConvID, remover: &UserID) -> ResultBoxError<()> { | ||||
| pub fn remove_member(user_id: &UserID, conv_id: ConvID, remover: Option<&UserID>) -> ResultBoxError<()> { | ||||
|     for msg in get_user_messages_for_conversations(conv_id, user_id)? { | ||||
|         delete_message(&msg)?; | ||||
|     } | ||||
| @@ -472,19 +540,21 @@ pub fn remove_member(user_id: &UserID, conv_id: ConvID, remover: &UserID) -> Res | ||||
|         .exec()?; | ||||
|  | ||||
|     // Create a message | ||||
|     if remover == user_id { | ||||
|         send_message(&NewConversationMessage::new_server_message( | ||||
|             conv_id, | ||||
|             ConversationServerMessageType::UserLeftConversation(user_id.clone()), | ||||
|         ))?; | ||||
|     } else { | ||||
|         send_message(&NewConversationMessage::new_server_message( | ||||
|             conv_id, | ||||
|             ConversationServerMessageType::UserRemovedFromConversation(UserRemovedAnotherUserToConversation { | ||||
|                 user_who_removed: remover.clone(), | ||||
|                 user_removed: user_id.clone(), | ||||
|             }), | ||||
|         ))?; | ||||
|     if let Some(remover) = remover { | ||||
|         if remover == user_id { | ||||
|             send_message(&NewConversationMessage::new_server_message( | ||||
|                 conv_id, | ||||
|                 ConversationServerMessageType::UserLeftConversation(user_id.clone()), | ||||
|             ))?; | ||||
|         } else { | ||||
|             send_message(&NewConversationMessage::new_server_message( | ||||
|                 conv_id, | ||||
|                 ConversationServerMessageType::UserRemovedFromConversation(UserRemovedAnotherUserToConversation { | ||||
|                     user_who_removed: remover.clone(), | ||||
|                     user_removed: user_id.clone(), | ||||
|                 }), | ||||
|             ))?; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Propagate event | ||||
| @@ -537,6 +607,8 @@ fn db_to_conversation_info(row: &database::RowResult) -> ResultBoxError<Conversa | ||||
|         last_activity: row.get_u64("last_activity")?, | ||||
|         creation_time: row.get_u64("creation_time")?, | ||||
|         group_id: row.get_optional_group_id("group_id")?, | ||||
|         min_group_membership_level: row.get_optional_u32("min_group_membership_level")? | ||||
|             .map(GroupMembershipLevel::from_db), | ||||
|     }) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -325,6 +325,13 @@ impl<'a> RowResult<'a> { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_optional_u32(&self, name: &str) -> ResultBoxError<Option<u32>> { | ||||
|         match self.is_null(name)? { | ||||
|             true => Ok(None), | ||||
|             false => Ok(Some(self.get_u32(name)?)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get an optional unsigned number  => Set to None if value is null / empty / 0 | ||||
|     pub fn get_optional_positive_u64(&self, name: &str) -> ResultBoxError<Option<u64>> { | ||||
|         Ok(match self.get_optional_u64(name)? { | ||||
| @@ -615,6 +622,14 @@ impl InsertQuery { | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Add an optional number. If None, Null will be inserted | ||||
|     pub fn add_opt_u32(mut self, key: &str, value: Option<u32>) -> InsertQuery { | ||||
|         self.values.insert(key.to_string(), value | ||||
|             .map(|u| Value::UInt(u as u64)) | ||||
|             .unwrap_or(Value::NULL)); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Add an optional user ID. If None, Null will be inserted | ||||
|     pub fn add_opt_user_id(self, key: &str, value: Option<UserID>) -> InsertQuery { | ||||
|         self.add_opt_u64(key, value.map(|u| u.id())) | ||||
|   | ||||
| @@ -487,6 +487,8 @@ pub fn delete(group_id: &GroupID) -> ResultBoxError { | ||||
|     // Delete all group related notifications | ||||
|     notifications_helper::delete_all_related_with_group(group_id)?; | ||||
|  | ||||
|     // TODO : delete all conversations related with the group | ||||
|  | ||||
|     // Delete all group members | ||||
|     database::DeleteQuery::new(GROUPS_MEMBERS_TABLE) | ||||
|         .cond_group_id("groups_id", group_id) | ||||
|   | ||||
| @@ -227,6 +227,7 @@ pub fn get_routes() -> Vec<Route> { | ||||
|         Route::post("/groups/checkVirtualDirectory", Box::new(groups_controller::check_virtual_dir)), | ||||
|         Route::post("/groups/upload_logo", Box::new(groups_controller::upload_logo)), | ||||
|         Route::post("/groups/delete_logo", Box::new(groups_controller::delete_logo)), | ||||
|         Route::post("/groups/create_conversation", Box::new(groups_controller::create_conversation)), | ||||
|         Route::post("/groups/get_members", Box::new(groups_controller::get_members)), | ||||
|         Route::post("/groups/invite", Box::new(groups_controller::invite_user)), | ||||
|         Route::post("/groups/cancel_invitation", Box::new(groups_controller::cancel_invitation)), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user