@ -20,6 +20,7 @@ use ruma::{
message ::RoomMessageEventContent ,
name ::RoomNameEventContent ,
power_levels ::RoomPowerLevelsEventContent ,
redaction ::RoomRedactionEventContent ,
topic ::RoomTopicEventContent ,
} ,
TimelineEventType ,
@ -182,8 +183,7 @@ enum AdminCommand {
#[ derive(Debug) ]
pub enum AdminRoomEvent {
ProcessMessage ( String ) ,
SendMessage ( RoomMessageEventContent ) ,
ProcessMessage ( String , ruma ::OwnedEventId ) ,
}
pub struct Service {
@ -220,58 +220,150 @@ impl Service {
tokio ::select ! {
Some ( event ) = receiver . recv ( ) = > {
let message_content = match event {
AdminRoomEvent ::SendMessage ( content ) = > content ,
AdminRoomEvent ::ProcessMessage ( room_message ) = > self . process_admin_message ( room_message ) . await
AdminRoomEvent ::ProcessMessage ( room_message , event_id ) = > self . process_admin_message ( room_message , & event_id ) . await
} ;
if let Some ( message_content ) = message_content {
let mutex_state = Arc ::clone (
services ( ) . globals
. roomid_mutex_state
. write ( )
. await
. entry ( conduit_room . to_owned ( ) )
. or_default ( ) ,
) ;
let state_lock = mutex_state . lock ( ) . await ;
let mutex_state = Arc ::clone (
services ( ) . globals
. roomid_mutex_state
. write ( )
. await
. entry ( conduit_room . to_owned ( ) )
. or_default ( ) ,
) ;
let state_lock = mutex_state . lock ( ) . await ;
services ( )
. rooms
. timeline
. build_and_append_pdu (
PduBuilder {
event_type : TimelineEventType ::RoomMessage ,
content : to_raw_value ( & message_content )
. expect ( "event is valid, we just created it" ) ,
unsigned : None ,
state_key : None ,
redacts : None ,
} ,
& conduit_user ,
& conduit_room ,
& state_lock ,
)
. await . unwrap ( ) ;
services ( )
. rooms
. timeline
. build_and_append_pdu (
PduBuilder {
event_type : TimelineEventType ::RoomMessage ,
content : to_raw_value ( & message_content )
. expect ( "event is valid, we just created it" ) ,
unsigned : None ,
state_key : None ,
redacts : None ,
} ,
& conduit_user ,
& conduit_room ,
& state_lock ,
)
. await . unwrap ( ) ;
}
}
}
}
}
}
pub fn process_message ( & self , room_message : String ) {
pub fn process_message ( & self , room_message : String , event_id : & EventId ) {
self . sender
. send ( AdminRoomEvent ::ProcessMessage ( room_message ) )
. send ( AdminRoomEvent ::ProcessMessage (
room_message ,
event_id . into ( ) ,
) )
. unwrap ( ) ;
}
pub fn send_message ( & self , message_content : RoomMessageEventContent ) {
self . sender
. send ( AdminRoomEvent ::SendMessage ( message_content ) )
. unwrap ( ) ;
/// Delete user message in the conduit admin room.
pub async fn delete_user_message (
& self ,
event_id : & EventId ,
reason : Option < impl Into < String > > ,
) -> Result < ( ) > {
let conduit_user =
UserId ::parse_with_server_name ( "conduit" , services ( ) . globals . server_name ( ) )
. expect ( "@conduit:server_name is valid" ) ;
if let Some ( room_id ) = services ( ) . admin . get_admin_room ( ) ? {
let mutex_state = Arc ::clone (
services ( )
. globals
. roomid_mutex_state
. write ( )
. await
. entry ( room_id . clone ( ) )
. or_default ( ) ,
) ;
let state_lock = mutex_state . lock ( ) . await ;
services ( )
. rooms
. timeline
. build_and_append_pdu (
PduBuilder {
event_type : TimelineEventType ::RoomRedaction ,
content : to_raw_value ( & RoomRedactionEventContent {
redacts : Some ( event_id . into ( ) ) ,
reason : reason . map ( Into ::into ) ,
} )
. expect ( "event is valid, we just created it" ) ,
unsigned : None ,
state_key : None ,
redacts : Some ( event_id . into ( ) ) ,
} ,
& conduit_user ,
& room_id ,
& state_lock ,
)
. await ? ;
}
Ok ( ( ) )
}
// Parse and process a message from the admin room
async fn process_admin_message ( & self , room_message : String ) -> RoomMessageEventContent {
/// Send the message content and return it's result
///
/// Note: Will return Ok(None) if there is no admin room
pub async fn send_message (
& self ,
message_content : & RoomMessageEventContent ,
) -> Result < Option < Arc < EventId > > > {
let conduit_user =
UserId ::parse_with_server_name ( "conduit" , services ( ) . globals . server_name ( ) )
. expect ( "@conduit:server_name is valid" ) ;
if let Some ( room_id ) = services ( ) . admin . get_admin_room ( ) ? {
let mutex_state = Arc ::clone (
services ( )
. globals
. roomid_mutex_state
. write ( )
. await
. entry ( room_id . clone ( ) )
. or_default ( ) ,
) ;
let state_lock = mutex_state . lock ( ) . await ;
services ( )
. rooms
. timeline
. build_and_append_pdu (
PduBuilder {
event_type : TimelineEventType ::RoomMessage ,
content : to_raw_value ( message_content )
. expect ( "event is valid, we just created it" ) ,
unsigned : None ,
state_key : None ,
redacts : None ,
} ,
& conduit_user ,
& room_id ,
& state_lock ,
)
. await
. map ( Some )
} else {
Ok ( None )
}
}
/// Parse and process a message from the admin room
///
/// May return `Option::None` if there is no process case for the message
async fn process_admin_message (
& self ,
room_message : String ,
event_id : & EventId ,
) -> Option < RoomMessageEventContent > {
let mut lines = room_message . lines ( ) . filter ( | l | ! l . trim ( ) . is_empty ( ) ) ;
let command_line = lines . next ( ) . expect ( "each string has at least one line" ) ;
let body : Vec < _ > = lines . collect ( ) ;
@ -283,12 +375,16 @@ impl Service {
let message = error . replace ( "server.name" , server_name . as_str ( ) ) ;
let html_message = self . usage_to_html ( & message , server_name ) ;
return RoomMessageEventContent ::text_html ( message , html_message ) ;
return Some ( RoomMessageEventContent ::text_html ( message , html_message ) ) ;
}
} ;
match self . process_admin_command ( admin_command , body ) . await {
Ok ( reply_message ) = > reply_message ,
match self
. process_admin_command ( admin_command , body , event_id )
. await
{
Ok ( Some ( reply_message ) ) = > Some ( reply_message ) ,
Ok ( None ) = > None ,
Err ( error ) = > {
let markdown_message = format! (
" Encountered an error while handling the command :\ n \
@ -299,7 +395,10 @@ impl Service {
< pre > \ n { error } \ n < / pre > " ,
) ;
RoomMessageEventContent ::text_html ( markdown_message , html_message )
Some ( RoomMessageEventContent ::text_html (
markdown_message ,
html_message ,
) )
}
}
}
@ -326,18 +425,22 @@ impl Service {
AdminCommand ::try_parse_from ( argv ) . map_err ( | error | error . to_string ( ) )
}
/// Process the entered admin command
///
/// May return `Ok(Option::None)` if there is no process case for the message
async fn process_admin_command (
& self ,
command : AdminCommand ,
body : Vec < & str > ,
) -> Result < RoomMessageEventContent > {
event_id : & EventId ,
) -> Result < Option < RoomMessageEventContent > > {
let reply_message_content = match command {
AdminCommand ::RegisterAppservice = > {
if body . len ( ) > 2 & & body [ 0 ] . trim ( ) = = "```" & & body . last ( ) . unwrap ( ) . trim ( ) = = "```"
{
let appservice_config = body [ 1 .. body . len ( ) - 1 ] . join ( "\n" ) ;
let parsed_config = serde_yaml ::from_str ::< Registration > ( & appservice_config ) ;
match parsed_config {
Some ( match parsed_config {
Ok ( yaml ) = > match services ( ) . appservice . register_appservice ( yaml ) . await {
Ok ( id ) = > RoomMessageEventContent ::text_plain ( format! (
"Appservice registered with ID: {id}."
@ -349,25 +452,27 @@ impl Service {
Err ( e ) = > RoomMessageEventContent ::text_plain ( format! (
"Could not parse appservice config: {e}"
) ) ,
}
} )
} else {
RoomMessageEventContent ::text_plain (
Some ( RoomMessageEventContent ::text_plain (
"Expected code block in command body. Add --help for details." ,
)
) )
}
}
AdminCommand ::UnregisterAppservice {
appservice_identifier ,
} = > match services ( )
. appservice
. unregister_appservice ( & appservice_identifier )
. await
{
Ok ( ( ) ) = > RoomMessageEventContent ::text_plain ( "Appservice unregistered." ) ,
Err ( e ) = > RoomMessageEventContent ::text_plain ( format! (
"Failed to unregister appservice: {e}"
) ) ,
} ,
} = > Some (
match services ( )
. appservice
. unregister_appservice ( & appservice_identifier )
. await
{
Ok ( ( ) ) = > RoomMessageEventContent ::text_plain ( "Appservice unregistered." ) ,
Err ( e ) = > RoomMessageEventContent ::text_plain ( format! (
"Failed to unregister appservice: {e}"
) ) ,
} ,
) ,
AdminCommand ::ListAppservices = > {
let appservices = services ( ) . appservice . iter_ids ( ) . await ;
let output = format! (
@ -375,7 +480,7 @@ impl Service {
appservices . len ( ) ,
appservices . join ( ", " )
) ;
RoomMessageEventContent ::text_plain ( output )
Some ( RoomMessageEventContent ::text_plain ( output ) )
}
AdminCommand ::ListRooms = > {
let room_ids = services ( ) . rooms . metadata . iter_ids ( ) ;
@ -396,16 +501,16 @@ impl Service {
. collect ::< Vec < _ > > ( )
. join ( "\n" )
) ;
RoomMessageEventContent ::text_plain ( output )
Some ( RoomMessageEventContent ::text_plain ( output ) )
}
AdminCommand ::ListLocalUsers = > match services ( ) . users . list_local_users ( ) {
AdminCommand ::ListLocalUsers = > Some ( match services ( ) . users . list_local_users ( ) {
Ok ( users ) = > {
let mut msg : String = format! ( "Found {} local user account(s):\n" , users . len ( ) ) ;
msg + = & users . join ( "\n" ) ;
RoomMessageEventContent ::text_plain ( & msg )
}
Err ( e ) = > RoomMessageEventContent ::text_plain ( e . to_string ( ) ) ,
} ,
} ) ,
AdminCommand ::IncomingFederation = > {
let map = services ( ) . globals . roomid_federationhandletime . read ( ) . await ;
let mut msg : String = format! ( "Handling {} incoming pdus:\n" , map . len ( ) ) ;
@ -420,7 +525,7 @@ impl Service {
elapsed . as_secs ( ) % 60
) ;
}
RoomMessageEventContent ::text_plain ( & msg )
Some ( RoomMessageEventContent ::text_plain ( & msg ) )
}
AdminCommand ::GetAuthChain { event_id } = > {
let event_id = Arc ::< EventId > ::from ( event_id ) ;
@ -441,18 +546,18 @@ impl Service {
. await ?
. count ( ) ;
let elapsed = start . elapsed ( ) ;
RoomMessageEventContent ::text_plain ( format! (
Some ( RoomMessageEventContent ::text_plain ( format! (
"Loaded auth chain with length {count} in {elapsed:?}"
) )
) ) )
} else {
RoomMessageEventContent ::text_plain ( "Event not found." )
Some ( RoomMessageEventContent ::text_plain ( "Event not found." ) )
}
}
AdminCommand ::ParsePdu = > {
if body . len ( ) > 2 & & body [ 0 ] . trim ( ) = = "```" & & body . last ( ) . unwrap ( ) . trim ( ) = = "```"
{
let string = body [ 1 .. body . len ( ) - 1 ] . join ( "\n" ) ;
match serde_json ::from_str ( & string ) {
Some ( match serde_json ::from_str ( & string ) {
Ok ( value ) = > {
match ruma ::signatures ::reference_hash ( & value , & RoomVersionId ::V6 ) {
Ok ( hash ) = > {
@ -477,9 +582,11 @@ impl Service {
Err ( e ) = > RoomMessageEventContent ::text_plain ( format! (
"Invalid json in command body: {e}"
) ) ,
}
} )
} else {
RoomMessageEventContent ::text_plain ( "Expected code block in command body." )
Some ( RoomMessageEventContent ::text_plain (
"Expected code block in command body." ,
) )
}
}
AdminCommand ::GetPdu { event_id } = > {
@ -496,7 +603,7 @@ impl Service {
Some ( json ) = > {
let json_text = serde_json ::to_string_pretty ( & json )
. expect ( "canonical json is valid json" ) ;
RoomMessageEventContent ::text_html (
Some ( RoomMessageEventContent ::text_html (
format! (
"{}\n```json\n{}\n```" ,
if outlier {
@ -515,32 +622,35 @@ impl Service {
} ,
HtmlEscape ( & json_text )
) ,
)
) )
}
None = > RoomMessageEventContent ::text_plain ( "PDU not found." ) ,
None = > Some ( RoomMessageEventContent ::text_plain ( "PDU not found." ) ) ,
}
}
AdminCommand ::MemoryUsage = > {
let response1 = services ( ) . memory_usage ( ) . await ;
let response2 = services ( ) . globals . db . memory_usage ( ) ;
RoomMessageEventContent ::text_plain ( format! (
Some ( RoomMessageEventContent ::text_plain ( format! (
"Services:\n{response1}\n\nDatabase:\n{response2}"
) )
) ) )
}
AdminCommand ::ClearDatabaseCaches { amount } = > {
services ( ) . globals . db . clear_caches ( amount ) ;
RoomMessageEventContent ::text_plain ( "Done." )
Some ( RoomMessageEventContent ::text_plain ( "Done." ) )
}
AdminCommand ::ClearServiceCaches { amount } = > {
services ( ) . clear_caches ( amount ) . await ;
RoomMessageEventContent ::text_plain ( "Done." )
Some ( RoomMessageEventContent ::text_plain ( "Done." ) )
}
AdminCommand ::ShowConfig = > {
// Construct and send the response
RoomMessageEventContent ::text_plain ( format! ( "{}" , services ( ) . globals . config ) )
Some ( RoomMessageEventContent ::text_plain ( format! (
"{}" ,
services ( ) . globals . config
) ) )
}
AdminCommand ::ResetPassword { username } = > {
let user_id = match UserId ::parse_with_server_name (
@ -549,17 +659,17 @@ impl Service {
) {
Ok ( id ) = > id ,
Err ( e ) = > {
return Ok ( RoomMessageEventContent ::text_plain ( format! (
return Ok ( Some ( RoomMessageEventContent ::text_plain ( format! (
"The supplied username is not a valid username: {e}"
) ) )
) ) ) )
}
} ;
// Checks if user is local
if user_id . server_name ( ) ! = services ( ) . globals . server_name ( ) {
return Ok ( RoomMessageEventContent ::text_plain (
return Ok ( Some ( RoomMessageEventContent ::text_plain (
"The specified user is not from this server!" ,
) ) ;
) ) ) ;
} ;
// Check if the specified user is valid
@ -571,26 +681,55 @@ impl Service {
)
. expect ( "conduit user exists" )
{
return Ok ( RoomMessageEventContent ::text_plain (
return Ok ( Some ( RoomMessageEventContent ::text_plain (
"The specified user does not exist!" ,
) ) ;
) ) ) ;
}
let new_password = utils ::random_string ( AUTO_GEN_PASSWORD_LENGTH ) ;
match services ( )
if let Err ( err ) = services ( )
. users
. set_password ( & user_id , Some ( new_password . as_str ( ) ) )
{
Ok ( ( ) ) = > RoomMessageEventContent ::text_plain ( format! (
"Successfully reset the password for user {user_id}: {new_password}"
) ) ,
Err ( e ) = > RoomMessageEventContent ::text_plain ( format! (
"Couldn't reset the password for user {user_id}: {e}"
) ) ,
Some ( RoomMessageEventContent ::text_plain ( format! (
"Couldn't reset the password for user {user_id}: {err}"
) ) )
} else {
// Send the reset password message to the user, we
// need it's event id to delete it after 60s
let Some ( sended_message_event_id ) = services ( )
. admin
. send_message ( & RoomMessageEventContent ::text_plain ( format! (
"Successfully reset the password for user {user_id}: {new_password} (This message will be deleted after 60s)"
) ) )
. await ?
else {
return Ok ( None ) ;
} ;
// Delete the message after 60s because it's contain a plain password
// and the admin room are not encrypted
tokio ::spawn ( async move {
tokio ::time ::sleep ( tokio ::time ::Duration ::from_secs ( 60 ) ) . await ;
if let Err ( err ) = services ( )
. admin
. delete_user_message (
sended_message_event_id . as_ref ( ) ,
Some ( "Message contained a plaintext password" ) ,
)
. await
{
tracing ::warn ! (
"Couldn't delete message containing a plaintext password {err}"
)
}
} ) ;
None
}
}
AdminCommand ::CreateUser { username , password } = > {
let is_auto_generated_password = password . is_none ( ) ;
let password =
password . unwrap_or_else ( | | utils ::random_string ( AUTO_GEN_PASSWORD_LENGTH ) ) ;
// Validate user id
@ -600,20 +739,20 @@ impl Service {
) {
Ok ( id ) = > id ,
Err ( e ) = > {
return Ok ( RoomMessageEventContent ::text_plain ( format! (
return Ok ( Some ( RoomMessageEventContent ::text_plain ( format! (
"The supplied username is not a valid username: {e}"
) ) )
) ) ) )
}
} ;
if user_id . is_historical ( ) {
return Ok ( RoomMessageEventContent ::text_plain ( format! (
return Ok ( Some ( RoomMessageEventContent ::text_plain ( format! (
"Userid {user_id} is not allowed due to historical"
) ) ) ;
) ) ) ) ;
}
if services ( ) . users . exists ( & user_id ) ? {
return Ok ( RoomMessageEventContent ::text_plain ( format! (
return Ok ( Some ( RoomMessageEventContent ::text_plain ( format! (
"Userid {user_id} already exists"
) ) ) ;
) ) ) ) ;
}
// Create user
services ( ) . users . create ( & user_id , Some ( password . as_str ( ) ) ) ? ;
@ -631,6 +770,7 @@ impl Service {
. set_displayname ( & user_id , Some ( displayname ) ) ? ;
// Initial account data
// we dont add a device since we're not the user, just the creator
services ( ) . account_data . update (
None ,
& user_id ,
@ -645,20 +785,57 @@ impl Service {
. expect ( "to json value always works" ) ,
) ? ;
// we dont add a device since we're not the user, just the creator
// We'll delete the user message because it's contain a plain password
// and the admin room are not encrypted
if ! is_auto_generated_password {
services ( )
. admin
. delete_user_message (
event_id ,
Some ( "Message contained a plaintext password" ) ,
)
. await ? ;
}
// Inhibit login does not work for guests
RoomMessageEventContent ::text_plain ( format! (
"Created user with user_id: {user_id} and password: {password}"
) )
// Send the created user message to the user, we
// need it's event id to delete it after 60s
let Some ( sended_message_event_id ) = services ( )
. admin
. send_message ( & RoomMessageEventContent ::text_plain ( format! (
"Created user with user_id: {user_id} and password: {password} (This message will be deleted after 60s)"
) ) )
. await ?
else {
return Ok ( None ) ;
} ;
// Delete the message after 60s because it's contain a plain password
// and the admin room are not encrypted
tokio ::spawn ( async move {
tokio ::time ::sleep ( tokio ::time ::Duration ::from_secs ( 60 ) ) . await ;
if let Err ( err ) = services ( )
. admin
. delete_user_message (
sended_message_event_id . as_ref ( ) ,
Some ( "Message contained a plaintext password" ) ,
)
. await
{
tracing ::warn ! (
"Couldn't delete message containing a plaintext password {err}"
)
}
} ) ;
None
}
AdminCommand ::DisableRoom { room_id } = > {
services ( ) . rooms . metadata . disable_room ( & room_id , true ) ? ;
RoomMessageEventContent ::text_plain ( "Room disabled." )
Some ( RoomMessageEventContent ::text_plain ( "Room disabled." ) )
}
AdminCommand ::EnableRoom { room_id } = > {
services ( ) . rooms . metadata . disable_room ( & room_id , false ) ? ;
RoomMessageEventContent ::text_plain ( "Room enabled." )
Some ( RoomMessageEventContent ::text_plain ( "Room enabled." ) )
}
AdminCommand ::DeactivateUser {
leave_rooms ,
@ -666,14 +843,15 @@ impl Service {
} = > {
let user_id = Arc ::< UserId > ::from ( user_id ) ;
if ! services ( ) . users . exists ( & user_id ) ? {
RoomMessageEventContent ::text_plain ( format! (
Some ( RoomMessageEventContent ::text_plain ( format! (
"User {user_id} doesn't exist on this server"
) )
) ) )
} else if user_id . server_name ( ) ! = services ( ) . globals . server_name ( ) {
RoomMessageEventContent ::text_plain ( format! (
Some ( RoomMessageEventContent ::text_plain ( format! (
"User {user_id} is not from this server"
) )
) ) )
} else {
// FIXME: Why this is here!
RoomMessageEventContent ::text_plain ( format! (
"Making {user_id} leave all rooms before deactivation..."
) ) ;
@ -684,9 +862,9 @@ impl Service {
leave_all_rooms ( & user_id ) . await ? ;
}
RoomMessageEventContent ::text_plain ( format! (
Some ( RoomMessageEventContent ::text_plain ( format! (
"User {user_id} has been deactivated"
) )
) ) )
}
}
AdminCommand ::DeactivateAll { leave_rooms , force } = > {
@ -751,10 +929,10 @@ impl Service {
html_message . push_str ( "</pre>\n\n" ) ;
}
if ! markdown_message . is_empty ( ) {
return Ok ( RoomMessageEventContent ::text_html (
return Ok ( Some ( RoomMessageEventContent ::text_html (
markdown_message ,
html_message ,
) ) ;
) ) ) ;
}
let mut deactivation_count = 0 ;
@ -786,16 +964,16 @@ impl Service {
}
if admins . is_empty ( ) {
RoomMessageEventContent ::text_plain ( format! (
Some ( RoomMessageEventContent ::text_plain ( format! (
"Deactivated {deactivation_count} accounts."
) )
) ) )
} else {
RoomMessageEventContent ::text_plain ( format! ( "Deactivated {} accounts.\nSkipped admin accounts: {:?}. Use --force to deactivate admin accounts" , deactivation_count , admins . join ( ", " ) ) )
Some ( RoomMessageEventContent ::text_plain ( format! ( "Deactivated {} accounts.\nSkipped admin accounts: {:?}. Use --force to deactivate admin accounts" , deactivation_count , admins . join ( ", " ) ) ) )
}
} else {
RoomMessageEventContent ::text_plain (
Some ( RoomMessageEventContent ::text_plain (
"Expected code block in command body. Add --help for details." ,
)
) )
}
}
AdminCommand ::SignJson = > {
@ -812,14 +990,16 @@ impl Service {
. expect ( "our request json is what ruma expects" ) ;
let json_text = serde_json ::to_string_pretty ( & value )
. expect ( "canonical json is valid json" ) ;
RoomMessageEventContent ::text_plain ( json_text )
Some ( RoomMessageEventContent ::text_plain ( json_text ) )
}
Err ( e ) = > RoomMessageEventContent ::text_plain ( format! ( "Invalid json: {e}" ) ) ,
Err ( e ) = > Some ( RoomMessageEventContent ::text_plain ( format! (
"Invalid json: {e}"
) ) ) ,
}
} else {
RoomMessageEventContent ::text_plain (
Some ( RoomMessageEventContent ::text_plain (
"Expected code block in command body. Add --help for details." ,
)
) )
}
}
AdminCommand ::VerifyJson = > {
@ -838,18 +1018,22 @@ impl Service {
let pub_key_map = pub_key_map . read ( ) . await ;
match ruma ::signatures ::verify_json ( & pub_key_map , & value ) {
Ok ( _ ) = > RoomMessageEventContent ::text_plain ( "Signature correct" ) ,
Err ( e ) = > RoomMessageEventContent ::text_plain ( format! (
Ok ( _ ) = > {
Some ( RoomMessageEventContent ::text_plain ( "Signature correct" ) )
}
Err ( e ) = > Some ( RoomMessageEventContent ::text_plain ( format! (
"Signature verification failed: {e}"
) ) ,
) ) ) ,
}
}
Err ( e ) = > RoomMessageEventContent ::text_plain ( format! ( "Invalid json: {e}" ) ) ,
Err ( e ) = > Some ( RoomMessageEventContent ::text_plain ( format! (
"Invalid json: {e}"
) ) ) ,
}
} else {
RoomMessageEventContent ::text_plain (
Some ( RoomMessageEventContent ::text_plain (
"Expected code block in command body. Add --help for details." ,
)
) )
}
}
} ;