1
0
Fork 0
mirror of https://gitlab.com/famedly/conduit.git synced 2025-06-27 16:35:59 +00:00
conduit/src/service/admin/mod.rs

1489 lines
60 KiB
Rust
Raw Normal View History

2024-06-11 23:15:02 +02:00
use std::{collections::BTreeMap, convert::TryFrom, sync::Arc, time::Instant};
2020-11-09 12:21:04 +01:00
use clap::Parser;
use regex::Regex;
use ruma::{
api::appservice::Registration,
2022-01-20 11:51:31 +01:00
events::{
room::{
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
message::RoomMessageEventContent,
name::RoomNameEventContent,
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
},
2023-02-26 16:29:06 +01:00
TimelineEventType,
},
EventId, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId,
RoomVersionId, ServerName, UserId,
};
use serde_json::value::to_raw_value;
use tokio::sync::{mpsc, Mutex, RwLock};
2020-11-09 12:21:04 +01:00
2022-10-05 20:34:31 +02:00
use crate::{
2022-10-08 13:03:07 +02:00
api::client_server::{leave_all_rooms, AUTO_GEN_PASSWORD_LENGTH},
2022-10-05 20:34:31 +02:00
services,
utils::{self, HtmlEscape},
Error, PduEvent, Result,
};
use super::pdu::PduBuilder;
#[cfg_attr(test, derive(Debug))]
#[derive(Parser)]
#[command(name = "@conduit:server.name:", version = env!("CARGO_PKG_VERSION"))]
enum AdminCommand {
#[command(verbatim_doc_comment)]
2022-01-21 11:06:16 +02:00
/// Register an appservice using its registration YAML
///
2022-01-21 11:06:16 +02:00
/// This command needs a YAML generated by an appservice (such as a bridge),
/// which must be provided in a Markdown code-block below the command.
///
/// Registering a new bridge using the ID of an existing bridge will replace
/// the old one.
///
/// [commandbody]()
/// # ```
/// # yaml content here
/// # ```
RegisterAppservice,
2022-01-21 11:06:16 +02:00
/// Unregister an appservice using its ID
///
2022-01-21 11:06:16 +02:00
/// You can find the ID using the `list-appservices` command.
UnregisterAppservice {
/// The appservice to unregister
appservice_identifier: String,
},
2022-01-21 11:06:16 +02:00
/// List all the currently registered appservices
ListAppservices,
2022-01-21 11:06:16 +02:00
/// List all rooms the server knows about
ListRooms,
/// List users in the database
ListLocalUsers,
/// List all rooms we are currently handling an incoming pdu from
IncomingFederation,
2024-05-29 17:38:13 +01:00
/// Removes an alias from the server
RemoveAlias {
/// The alias to be removed
alias: Box<RoomAliasId>,
},
/// Deactivate a user
///
/// User will not be removed from all rooms by default.
/// Use --leave-rooms to force the user to leave all rooms
DeactivateUser {
#[arg(short, long)]
leave_rooms: bool,
user_id: Box<UserId>,
},
#[command(verbatim_doc_comment)]
/// Deactivate a list of users
///
/// Recommended to use in conjunction with list-local-users.
///
/// Users will not be removed from joined rooms by default.
/// Can be overridden with --leave-rooms flag.
/// Removing a mass amount of users from a room may cause a significant amount of leave events.
/// The time to leave rooms may depend significantly on joined rooms and servers.
///
/// [commandbody]()
/// # ```
/// # User list here
/// # ```
DeactivateAll {
#[arg(short, long)]
/// Remove users from their joined rooms
leave_rooms: bool,
#[arg(short, long)]
/// Also deactivate admin accounts
force: bool,
},
/// Get the auth_chain of a PDU
GetAuthChain {
/// An event ID (the $ character followed by the base64 reference hash)
event_id: Box<EventId>,
},
2022-01-21 11:06:16 +02:00
#[command(verbatim_doc_comment)]
/// Parse and print a PDU from a JSON
2022-01-21 11:06:16 +02:00
///
/// The PDU event is only checked for validity and is not added to the
/// database.
///
/// [commandbody]()
/// # ```
/// # PDU json content here
/// # ```
ParsePdu,
2022-01-21 11:06:16 +02:00
/// Retrieve and print a PDU by ID from the Conduit database
GetPdu {
/// An event ID (a $ followed by the base64 reference hash)
event_id: Box<EventId>,
},
2022-01-21 11:06:16 +02:00
/// Print database memory usage statistics
MemoryUsage,
/// Clears all of Conduit's database caches with index smaller than the amount
ClearDatabaseCaches { amount: u32 },
/// Clears all of Conduit's service caches with index smaller than the amount
ClearServiceCaches { amount: u32 },
2022-02-06 20:23:22 +01:00
/// Show configuration values
ShowConfig,
2022-04-07 12:11:55 +00:00
/// Reset user password
ResetPassword {
/// Username of the user for whom the password should be reset
username: String,
},
/// Create a new user
CreateUser {
/// Username of the new user
username: String,
/// Password of the new user, if unspecified one is generated
password: Option<String>,
},
/// Temporarily toggle user registration by passing either true or false as an argument, does not persist between restarts
AllowRegistration { status: Option<bool> },
/// Disables incoming federation handling for a room.
DisableRoom { room_id: Box<RoomId> },
/// Enables incoming federation handling for a room again.
EnableRoom { room_id: Box<RoomId> },
/// Sign a json object using Conduit's signing keys, putting the json in a codeblock
SignJson,
/// Verify json signatures, putting the json in a codeblock
VerifyJson,
/// Parses a JSON object as an event then creates a hash and signs it, putting a room
/// version as an argument, and the json in a codeblock
HashAndSignEvent { room_version_id: RoomVersionId },
}
2022-09-07 13:25:51 +02:00
#[derive(Debug)]
pub enum AdminRoomEvent {
ProcessMessage(String),
SendMessage(RoomMessageEventContent),
}
pub struct Service {
pub sender: mpsc::UnboundedSender<AdminRoomEvent>,
receiver: Mutex<mpsc::UnboundedReceiver<AdminRoomEvent>>,
2022-09-07 13:25:51 +02:00
}
impl Service {
2022-10-08 13:02:52 +02:00
pub fn build() -> Arc<Self> {
let (sender, receiver) = mpsc::unbounded_channel();
Arc::new(Self {
sender,
receiver: Mutex::new(receiver),
})
}
2022-09-07 13:25:51 +02:00
pub fn start_handler(self: &Arc<Self>) {
2022-10-10 14:09:11 +02:00
let self2 = Arc::clone(self);
2022-10-08 13:03:07 +02:00
tokio::spawn(async move {
self2.handler().await;
2022-10-08 13:03:07 +02:00
});
2022-10-08 13:02:52 +02:00
}
async fn handler(&self) {
let mut receiver = self.receiver.lock().await;
2022-10-08 13:02:52 +02:00
// TODO: Use futures when we have long admin commands
//let mut futures = FuturesUnordered::new();
2024-05-31 21:46:38 +01:00
let conduit_user = services().globals.server_user();
2022-10-08 13:02:52 +02:00
if let Ok(Some(conduit_room)) = services().admin.get_admin_room() {
loop {
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
};
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,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&conduit_room,
&state_lock,
)
.await.unwrap();
}
2022-09-07 13:25:51 +02:00
}
}
2022-10-08 13:02:52 +02:00
}
2022-09-07 13:25:51 +02:00
}
pub fn process_message(&self, room_message: String) {
self.sender
.send(AdminRoomEvent::ProcessMessage(room_message))
.unwrap();
}
pub fn send_message(&self, message_content: RoomMessageEventContent) {
self.sender
.send(AdminRoomEvent::SendMessage(message_content))
.unwrap();
}
// Parse and process a message from the admin room
async fn process_admin_message(&self, room_message: String) -> RoomMessageEventContent {
let mut lines = room_message.lines().filter(|l| !l.trim().is_empty());
2022-09-07 13:25:51 +02:00
let command_line = lines.next().expect("each string has at least one line");
let body: Vec<_> = lines.collect();
2022-10-10 14:09:11 +02:00
let admin_command = match self.parse_admin_command(command_line) {
2022-09-07 13:25:51 +02:00
Ok(command) => command,
Err(error) => {
let server_name = services().globals.server_name();
2022-10-10 14:09:11 +02:00
let message = error.replace("server.name", server_name.as_str());
2022-10-05 09:34:25 +02:00
let html_message = self.usage_to_html(&message, server_name);
2022-09-07 13:25:51 +02:00
return RoomMessageEventContent::text_html(message, html_message);
}
};
2022-10-05 09:34:25 +02:00
match self.process_admin_command(admin_command, body).await {
2022-09-07 13:25:51 +02:00
Ok(reply_message) => reply_message,
Err(error) => {
let markdown_message = format!(
"Encountered an error while handling the command:\n\
```\n{error}\n```",
2022-09-07 13:25:51 +02:00
);
let html_message = format!(
"Encountered an error while handling the command:\n\
<pre>\n{error}\n</pre>",
2022-09-07 13:25:51 +02:00
);
RoomMessageEventContent::text_html(markdown_message, html_message)
}
}
}
// Parse chat messages from the admin room into an AdminCommand object
fn parse_admin_command(&self, command_line: &str) -> std::result::Result<AdminCommand, String> {
// Note: argv[0] is `@conduit:servername:`, which is treated as the main command
let mut argv: Vec<_> = command_line.split_whitespace().collect();
// Replace `help command` with `command --help`
// Clap has a help subcommand, but it omits the long help description.
if argv.len() > 1 && argv[1] == "help" {
argv.remove(1);
argv.push("--help");
}
// Backwards compatibility with `register_appservice`-style commands
let command_with_dashes;
2022-10-10 14:09:11 +02:00
if argv.len() > 1 && argv[1].contains('_') {
command_with_dashes = argv[1].replace('_', "-");
2022-09-07 13:25:51 +02:00
argv[1] = &command_with_dashes;
}
AdminCommand::try_parse_from(argv).map_err(|error| error.to_string())
}
async fn process_admin_command(
&self,
command: AdminCommand,
body: Vec<&str>,
) -> Result<RoomMessageEventContent> {
let reply_message_content = match command {
AdminCommand::RegisterAppservice => {
2022-10-05 20:34:31 +02:00
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
{
2022-09-07 13:25:51 +02:00
let appservice_config = body[1..body.len() - 1].join("\n");
let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config);
2022-09-07 13:25:51 +02:00
match parsed_config {
Ok(yaml) => match services().appservice.register_appservice(yaml).await {
2022-09-07 13:25:51 +02:00
Ok(id) => RoomMessageEventContent::text_plain(format!(
"Appservice registered with ID: {id}."
2022-09-07 13:25:51 +02:00
)),
Err(e) => RoomMessageEventContent::text_plain(format!(
"Failed to register appservice: {e}"
2022-09-07 13:25:51 +02:00
)),
},
Err(e) => RoomMessageEventContent::text_plain(format!(
"Could not parse appservice config: {e}"
)),
2022-09-07 13:25:51 +02:00
}
} else {
RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
)
}
}
2022-09-07 13:25:51 +02:00
AdminCommand::UnregisterAppservice {
appservice_identifier,
2022-10-05 20:34:31 +02:00
} => match services()
.appservice
.unregister_appservice(&appservice_identifier)
.await
2022-10-05 20:34:31 +02:00
{
2022-09-07 13:25:51 +02:00
Ok(()) => RoomMessageEventContent::text_plain("Appservice unregistered."),
Err(e) => RoomMessageEventContent::text_plain(format!(
"Failed to unregister appservice: {e}"
2022-09-07 13:25:51 +02:00
)),
},
AdminCommand::ListAppservices => {
let appservices = services().appservice.iter_ids().await;
let output = format!(
"Appservices ({}): {}",
appservices.len(),
appservices.join(", ")
);
RoomMessageEventContent::text_plain(output)
2022-09-07 13:25:51 +02:00
}
AdminCommand::ListRooms => {
2022-10-08 13:02:52 +02:00
let room_ids = services().rooms.metadata.iter_ids();
let output = format!(
2022-09-07 13:25:51 +02:00
"Rooms:\n{}",
room_ids
.filter_map(|r| r.ok())
2022-09-07 13:25:51 +02:00
.map(|id| id.to_string()
+ "\tMembers: "
+ &services()
.rooms
2022-10-08 13:02:52 +02:00
.state_cache
2022-09-07 13:25:51 +02:00
.room_joined_count(&id)
.ok()
.flatten()
.unwrap_or(0)
.to_string())
.collect::<Vec<_>>()
2022-09-07 13:25:51 +02:00
.join("\n")
);
RoomMessageEventContent::text_plain(output)
}
2022-09-07 13:25:51 +02:00
AdminCommand::ListLocalUsers => 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;
2022-09-07 13:25:51 +02:00
let mut msg: String = format!("Handling {} incoming pdus:\n", map.len());
for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed();
msg += &format!(
"{} {}: {}m{}s\n",
r,
e,
elapsed.as_secs() / 60,
elapsed.as_secs() % 60
);
}
RoomMessageEventContent::text_plain(&msg)
}
2022-09-07 13:25:51 +02:00
AdminCommand::GetAuthChain { event_id } => {
let event_id = Arc::<EventId>::from(event_id);
2022-10-05 09:34:25 +02:00
if let Some(event) = services().rooms.timeline.get_pdu_json(&event_id)? {
2022-09-07 13:25:51 +02:00
let room_id_str = event
.get("room_id")
.and_then(|val| val.as_str())
.ok_or_else(|| Error::bad_database("Invalid event in database"))?;
let room_id = <&RoomId>::try_from(room_id_str).map_err(|_| {
Error::bad_database("Invalid room id field in event in database")
})?;
let start = Instant::now();
2022-10-05 20:34:31 +02:00
let count = services()
.rooms
.auth_chain
.get_auth_chain(room_id, vec![event_id])
2022-09-07 13:25:51 +02:00
.await?
.count();
let elapsed = start.elapsed();
RoomMessageEventContent::text_plain(format!(
"Loaded auth chain with length {count} in {elapsed:?}"
2022-09-07 13:25:51 +02:00
))
} else {
RoomMessageEventContent::text_plain("Event not found.")
}
}
2022-09-07 13:25:51 +02:00
AdminCommand::ParsePdu => {
2022-10-05 20:34:31 +02:00
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
{
2022-09-07 13:25:51 +02:00
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
Ok(value) => {
match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
Ok(hash) => {
let event_id = EventId::parse(format!("${hash}"));
2022-09-07 13:25:51 +02:00
match serde_json::from_value::<PduEvent>(
serde_json::to_value(value).expect("value is json"),
) {
Ok(pdu) => RoomMessageEventContent::text_plain(format!(
"EventId: {event_id:?}\n{pdu:#?}"
2022-09-07 13:25:51 +02:00
)),
Err(e) => RoomMessageEventContent::text_plain(format!(
"EventId: {event_id:?}\nCould not parse event: {e}"
2022-09-07 13:25:51 +02:00
)),
}
}
2022-09-07 13:25:51 +02:00
Err(e) => RoomMessageEventContent::text_plain(format!(
"Could not parse PDU JSON: {e:?}"
2022-09-07 13:25:51 +02:00
)),
}
}
2022-09-07 13:25:51 +02:00
Err(e) => RoomMessageEventContent::text_plain(format!(
"Invalid json in command body: {e}"
2022-09-07 13:25:51 +02:00
)),
}
2022-09-07 13:25:51 +02:00
} else {
RoomMessageEventContent::text_plain("Expected code block in command body.")
}
}
2022-09-07 13:25:51 +02:00
AdminCommand::GetPdu { event_id } => {
let mut outlier = false;
2022-10-05 20:34:31 +02:00
let mut pdu_json = services()
.rooms
.timeline
.get_non_outlier_pdu_json(&event_id)?;
2022-09-07 13:25:51 +02:00
if pdu_json.is_none() {
outlier = true;
2022-10-05 09:34:25 +02:00
pdu_json = services().rooms.timeline.get_pdu_json(&event_id)?;
}
2022-09-07 13:25:51 +02:00
match pdu_json {
Some(json) => {
2022-10-05 20:34:31 +02:00
let json_text = serde_json::to_string_pretty(&json)
.expect("canonical json is valid json");
2022-09-07 13:25:51 +02:00
RoomMessageEventContent::text_html(
format!(
"{}\n```json\n{}\n```",
if outlier {
"PDU is outlier"
} else {
"PDU was accepted"
},
json_text
),
format!(
"<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
if outlier {
"PDU is outlier"
} else {
"PDU was accepted"
},
HtmlEscape(&json_text)
),
)
}
None => RoomMessageEventContent::text_plain("PDU not found."),
2022-04-07 12:11:55 +00:00
}
}
AdminCommand::MemoryUsage => {
let response1 = services().memory_usage().await;
let response2 = services().globals.db.memory_usage();
RoomMessageEventContent::text_plain(format!(
"Services:\n{response1}\n\nDatabase:\n{response2}"
))
}
AdminCommand::ClearDatabaseCaches { amount } => {
services().globals.db.clear_caches(amount);
RoomMessageEventContent::text_plain("Done.")
}
AdminCommand::ClearServiceCaches { amount } => {
services().clear_caches(amount).await;
RoomMessageEventContent::text_plain("Done.")
}
2022-09-07 13:25:51 +02:00
AdminCommand::ShowConfig => {
// Construct and send the response
RoomMessageEventContent::text_plain(format!("{}", services().globals.config))
2022-04-07 12:11:55 +00:00
}
2022-09-07 13:25:51 +02:00
AdminCommand::ResetPassword { username } => {
let user_id = match UserId::parse_with_server_name(
username.as_str().to_lowercase(),
services().globals.server_name(),
) {
Ok(id) => id,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"The supplied username is not a valid username: {e}"
2022-09-07 13:25:51 +02:00
)))
}
};
// Checks if user is local
if user_id.server_name() != services().globals.server_name() {
return Ok(RoomMessageEventContent::text_plain(
"The specified user is not from this server!",
));
};
2022-09-07 13:25:51 +02:00
// Check if the specified user is valid
if !services().users.exists(&user_id)?
|| user_id
2022-10-05 20:34:31 +02:00
== UserId::parse_with_server_name(
"conduit",
services().globals.server_name(),
)
.expect("conduit user exists")
2022-09-07 13:25:51 +02:00
{
return Ok(RoomMessageEventContent::text_plain(
"The specified user does not exist!",
2022-09-07 13:25:51 +02:00
));
}
let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH);
2022-10-05 20:34:31 +02:00
match services()
.users
.set_password(&user_id, Some(new_password.as_str()))
{
2022-09-07 13:25:51 +02:00
Ok(()) => RoomMessageEventContent::text_plain(format!(
"Successfully reset the password for user {user_id}: {new_password}"
2022-09-07 13:25:51 +02:00
)),
Err(e) => RoomMessageEventContent::text_plain(format!(
"Couldn't reset the password for user {user_id}: {e}"
2022-09-07 13:25:51 +02:00
)),
}
}
2022-09-07 13:25:51 +02:00
AdminCommand::CreateUser { username, password } => {
2022-11-21 09:51:39 +01:00
let password =
password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
2022-09-07 13:25:51 +02:00
// Validate user id
let user_id = match UserId::parse_with_server_name(
username.as_str().to_lowercase(),
services().globals.server_name(),
) {
Ok(id) => id,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"The supplied username is not a valid username: {e}"
2022-09-07 13:25:51 +02:00
)))
}
};
// Checks if user is local
if user_id.server_name() != services().globals.server_name() {
return Ok(RoomMessageEventContent::text_plain(
"The specified user is not from this server!",
));
};
2022-09-07 13:25:51 +02:00
if user_id.is_historical() {
return Ok(RoomMessageEventContent::text_plain(format!(
"Userid {user_id} is not allowed due to historical"
2022-09-07 13:25:51 +02:00
)));
}
if services().users.exists(&user_id)? {
return Ok(RoomMessageEventContent::text_plain(format!(
"Userid {user_id} already exists"
2022-09-07 13:25:51 +02:00
)));
}
// Create user
services().users.create(&user_id, Some(password.as_str()))?;
// Default to pretty displayname
2022-06-23 06:58:34 +00:00
let mut displayname = user_id.localpart().to_owned();
// If enabled append lightning bolt to display name (default true)
if services().globals.enable_lightning_bolt() {
displayname.push_str(" ⚡️");
}
2022-10-05 20:34:31 +02:00
services()
.users
2022-10-10 14:09:11 +02:00
.set_displayname(&user_id, Some(displayname))?;
2022-09-07 13:25:51 +02:00
// Initial account data
services().account_data.update(
None,
&user_id,
ruma::events::GlobalAccountDataEventType::PushRules
.to_string()
.into(),
2022-10-05 15:33:57 +02:00
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
2022-09-07 13:25:51 +02:00
content: ruma::events::push_rules::PushRulesEventContent {
global: ruma::push::Ruleset::server_default(&user_id),
},
2022-10-05 20:34:31 +02:00
})
.expect("to json value always works"),
2022-09-07 13:25:51 +02:00
)?;
2022-09-07 13:25:51 +02:00
// we dont add a device since we're not the user, just the creator
2022-09-07 13:25:51 +02:00
// Inhibit login does not work for guests
RoomMessageEventContent::text_plain(format!(
2022-09-07 13:25:51 +02:00
"Created user with user_id: {user_id} and password: {password}"
))
}
AdminCommand::AllowRegistration { status } => {
if let Some(status) = status {
services().globals.set_registration(status).await;
RoomMessageEventContent::text_plain(if status {
"Registration is now enabled"
} else {
"Registration is now disabled"
})
} else {
RoomMessageEventContent::text_plain(
if services().globals.allow_registration().await {
"Registration is currently enabled"
} else {
"Registration is currently disabled"
},
)
}
}
2022-09-07 13:25:51 +02:00
AdminCommand::DisableRoom { room_id } => {
2022-10-08 13:02:52 +02:00
services().rooms.metadata.disable_room(&room_id, true)?;
2022-10-05 18:36:12 +02:00
RoomMessageEventContent::text_plain("Room disabled.")
2022-09-07 13:25:51 +02:00
}
AdminCommand::EnableRoom { room_id } => {
2022-10-08 13:02:52 +02:00
services().rooms.metadata.disable_room(&room_id, false)?;
2022-10-05 18:36:12 +02:00
RoomMessageEventContent::text_plain("Room enabled.")
2022-09-07 13:25:51 +02:00
}
AdminCommand::DeactivateUser {
leave_rooms,
user_id,
} => {
let user_id = Arc::<UserId>::from(user_id);
if !services().users.exists(&user_id)? {
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!(
"User {user_id} is not from this server"
))
} else {
2022-09-07 13:25:51 +02:00
RoomMessageEventContent::text_plain(format!(
"Making {user_id} leave all rooms before deactivation..."
2022-09-07 13:25:51 +02:00
));
2022-09-07 13:25:51 +02:00
services().users.deactivate_account(&user_id)?;
2022-09-07 13:25:51 +02:00
if leave_rooms {
2022-10-05 15:33:57 +02:00
leave_all_rooms(&user_id).await?;
2022-09-07 13:25:51 +02:00
}
2022-09-07 13:25:51 +02:00
RoomMessageEventContent::text_plain(format!(
"User {user_id} has been deactivated"
2022-09-07 13:25:51 +02:00
))
}
}
2022-09-07 13:25:51 +02:00
AdminCommand::DeactivateAll { leave_rooms, force } => {
2022-10-05 20:34:31 +02:00
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
{
let users = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
let mut user_ids = Vec::new();
let mut remote_ids = Vec::new();
2025-03-24 01:28:42 +00:00
let mut non_existent_ids = Vec::new();
let mut invalid_users = Vec::new();
for &user in &users {
match <&UserId>::try_from(user) {
Ok(user_id) => {
if user_id.server_name() != services().globals.server_name() {
remote_ids.push(user_id)
} else if !services().users.exists(user_id)? {
2025-03-24 01:28:42 +00:00
non_existent_ids.push(user_id)
} else {
user_ids.push(user_id)
}
}
2022-09-07 13:25:51 +02:00
Err(_) => {
invalid_users.push(user);
2022-09-07 13:25:51 +02:00
}
}
}
let mut markdown_message = String::new();
let mut html_message = String::new();
if !invalid_users.is_empty() {
markdown_message.push_str("The following user ids are not valid:\n```\n");
html_message.push_str("The following user ids are not valid:\n<pre>\n");
for invalid_user in invalid_users {
markdown_message.push_str(&format!("{invalid_user}\n"));
html_message.push_str(&format!("{invalid_user}\n"));
}
markdown_message.push_str("```\n\n");
html_message.push_str("</pre>\n\n");
}
if !remote_ids.is_empty() {
markdown_message
.push_str("The following users are not from this server:\n```\n");
html_message
.push_str("The following users are not from this server:\n<pre>\n");
for remote_id in remote_ids {
markdown_message.push_str(&format!("{remote_id}\n"));
html_message.push_str(&format!("{remote_id}\n"));
}
markdown_message.push_str("```\n\n");
html_message.push_str("</pre>\n\n");
}
2025-03-24 01:28:42 +00:00
if !non_existent_ids.is_empty() {
markdown_message.push_str("The following users do not exist:\n```\n");
html_message.push_str("The following users do not exist:\n<pre>\n");
2025-03-24 01:28:42 +00:00
for non_existent_id in non_existent_ids {
markdown_message.push_str(&format!("{non_existent_id}\n"));
html_message.push_str(&format!("{non_existent_id}\n"));
}
markdown_message.push_str("```\n\n");
html_message.push_str("</pre>\n\n");
}
if !markdown_message.is_empty() {
return Ok(RoomMessageEventContent::text_html(
markdown_message,
html_message,
));
}
2022-09-07 13:25:51 +02:00
let mut deactivation_count = 0;
let mut admins = Vec::new();
if !force {
2022-10-05 20:34:31 +02:00
user_ids.retain(|&user_id| match services().users.is_admin(user_id) {
Ok(is_admin) => match is_admin {
true => {
admins.push(user_id.localpart());
false
}
false => true,
},
Err(_) => false,
2022-09-07 13:25:51 +02:00
})
}
2022-09-07 13:25:51 +02:00
for &user_id in &user_ids {
2022-11-21 09:51:39 +01:00
if services().users.deactivate_account(user_id).is_ok() {
deactivation_count += 1
}
}
2022-09-07 13:25:51 +02:00
if leave_rooms {
for &user_id in &user_ids {
2022-10-05 15:33:57 +02:00
let _ = leave_all_rooms(user_id).await;
2022-09-07 13:25:51 +02:00
}
}
2022-09-07 13:25:51 +02:00
if admins.is_empty() {
RoomMessageEventContent::text_plain(format!(
"Deactivated {deactivation_count} accounts."
2022-09-07 13:25:51 +02:00
))
} else {
RoomMessageEventContent::text_plain(format!("Deactivated {} accounts.\nSkipped admin accounts: {:?}. Use --force to deactivate admin accounts", deactivation_count, admins.join(", ")))
}
} else {
RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
)
}
}
AdminCommand::SignJson => {
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) {
Ok(mut value) => {
ruma::signatures::sign_json(
services().globals.server_name().as_str(),
services().globals.keypair(),
&mut value,
)
.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)
}
Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")),
}
} else {
RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
)
}
}
AdminCommand::VerifyJson => {
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) {
Ok(value) => {
let pub_key_map = RwLock::new(BTreeMap::new());
services()
.rooms
.event_handler
// Generally we shouldn't be checking against expired keys unless required, so in the admin
// room it might be best to not allow expired keys
.fetch_required_signing_keys(&value, &pub_key_map)
.await?;
let mut expired_key_map = BTreeMap::new();
let mut valid_key_map = BTreeMap::new();
for (server, keys) in pub_key_map.into_inner().into_iter() {
if keys.valid_until_ts > MilliSecondsSinceUnixEpoch::now() {
valid_key_map.insert(
server,
keys.verify_keys
.into_iter()
.map(|(id, key)| (id, key.key))
.collect(),
);
} else {
expired_key_map.insert(
server,
keys.verify_keys
.into_iter()
.map(|(id, key)| (id, key.key))
.collect(),
);
}
}
if ruma::signatures::verify_json(&valid_key_map, &value).is_ok() {
RoomMessageEventContent::text_plain("Signature correct")
} else if let Err(e) =
ruma::signatures::verify_json(&expired_key_map, &value)
{
RoomMessageEventContent::text_plain(format!(
"Signature verification failed: {e}"
))
} else {
RoomMessageEventContent::text_plain(
"Signature correct (with expired keys)",
)
}
}
Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")),
}
} else {
2022-09-07 13:25:51 +02:00
RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
)
}
}
AdminCommand::HashAndSignEvent { room_version_id } => {
if body.len() > 2
// Language may be specified as part of the codeblock (e.g. "```json")
&& body[0].trim().starts_with("```")
&& body.last().unwrap().trim() == "```"
{
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
Ok(mut value) => {
if let Err(e) = ruma::signatures::hash_and_sign_event(
services().globals.server_name().as_str(),
services().globals.keypair(),
&mut value,
&room_version_id,
) {
RoomMessageEventContent::text_plain(format!("Invalid event: {e}"))
} else {
let json_text = serde_json::to_string_pretty(&value)
.expect("canonical json is valid json");
RoomMessageEventContent::text_plain(json_text)
}
}
Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")),
}
} else {
RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
)
}
}
2024-05-29 17:38:13 +01:00
AdminCommand::RemoveAlias { alias } => {
if alias.server_name() != services().globals.server_name() {
RoomMessageEventContent::text_plain(
"Cannot remove alias which is not from this server",
)
} else if services()
.rooms
.alias
.resolve_local_alias(&alias)?
.is_none()
{
RoomMessageEventContent::text_plain("No such alias exists")
} else {
2024-06-11 23:15:02 +02:00
// We execute this as the server user for two reasons
// 1. If the user can execute commands in the admin room, they can always remove the alias.
// 2. In the future, we are likely going to be able to allow users to execute commands via
// other methods, such as IPC, which would lead to us not knowing their user id
services()
.rooms
.alias
.remove_alias(&alias, services().globals.server_user())?;
2025-03-24 01:28:42 +00:00
RoomMessageEventContent::text_plain("Alias removed successfully")
2024-05-29 17:38:13 +01:00
}
}
2022-09-07 13:25:51 +02:00
};
2022-09-07 13:25:51 +02:00
Ok(reply_message_content)
}
2022-09-07 13:25:51 +02:00
// Utility to turn clap's `--help` text to HTML.
fn usage_to_html(&self, text: &str, server_name: &ServerName) -> String {
// Replace `@conduit:servername:-subcmdname` with `@conduit:servername: subcmdname`
let text = text.replace(
&format!("@conduit:{server_name}:-"),
&format!("@conduit:{server_name}: "),
2022-09-07 13:25:51 +02:00
);
// For the conduit admin room, subcommands become main commands
let text = text.replace("SUBCOMMAND", "COMMAND");
let text = text.replace("subcommand", "command");
// Escape option names (e.g. `<element-id>`) since they look like HTML tags
2022-10-10 14:09:11 +02:00
let text = text.replace('<', "&lt;").replace('>', "&gt;");
2022-09-07 13:25:51 +02:00
// Italicize the first line (command name and version text)
let re = Regex::new("^(.*?)\n").expect("Regex compilation should not fail");
let text = re.replace_all(&text, "<em>$1</em>\n");
// Unmerge wrapped lines
let text = text.replace("\n ", " ");
// Wrap option names in backticks. The lines look like:
// -V, --version Prints version information
// And are converted to:
// <code>-V, --version</code>: Prints version information
// (?m) enables multi-line mode for ^ and $
let re = Regex::new("(?m)^ (([a-zA-Z_&;-]+(, )?)+) +(.*)$")
.expect("Regex compilation should not fail");
let text = re.replace_all(&text, "<code>$1</code>: $4");
// Look for a `[commandbody]()` tag. If it exists, use all lines below it that
2022-09-07 13:25:51 +02:00
// start with a `#` in the USAGE section.
let mut text_lines: Vec<&str> = text.lines().collect();
let mut command_body = String::new();
if let Some(line_index) = text_lines
.iter()
.position(|line| *line == "[commandbody]()")
{
text_lines.remove(line_index);
2022-09-07 13:25:51 +02:00
while text_lines
.get(line_index)
2022-10-10 14:09:11 +02:00
.map(|line| line.starts_with('#'))
2022-09-07 13:25:51 +02:00
.unwrap_or(false)
{
command_body += if text_lines[line_index].starts_with("# ") {
&text_lines[line_index][2..]
} else {
&text_lines[line_index][1..]
};
command_body += "[nobr]\n";
text_lines.remove(line_index);
}
}
2022-09-07 13:25:51 +02:00
let text = text_lines.join("\n");
// Improve the usage section
let text = if command_body.is_empty() {
// Wrap the usage line in code tags
let re = Regex::new("(?m)^USAGE:\n (@conduit:.*)$")
.expect("Regex compilation should not fail");
re.replace_all(&text, "USAGE:\n<code>$1</code>").to_string()
} else {
// Wrap the usage line in a code block, and add a yaml block example
// This makes the usage of e.g. `register-appservice` more accurate
2022-10-05 20:34:31 +02:00
let re = Regex::new("(?m)^USAGE:\n (.*?)\n\n")
.expect("Regex compilation should not fail");
2022-09-07 13:25:51 +02:00
re.replace_all(&text, "USAGE:\n<pre>$1[nobr]\n[commandbodyblock]</pre>")
.replace("[commandbodyblock]", &command_body)
};
// Add HTML line-breaks
2022-10-10 14:09:11 +02:00
text.replace("\n\n\n", "\n\n")
.replace('\n', "<br>\n")
.replace("[nobr]<br>", "")
2022-09-07 13:25:51 +02:00
}
2022-09-07 13:25:51 +02:00
/// Create the admin room.
///
/// Users in this room are considered admins by conduit, and the room can be
/// used to issue admin commands by talking to the server user inside it.
pub(crate) async fn create_admin_room(&self) -> Result<()> {
let room_id = RoomId::new(services().globals.server_name());
2022-10-05 15:33:57 +02:00
services().rooms.short.get_or_create_shortroomid(&room_id)?;
2022-09-07 13:25:51 +02:00
let mutex_state = Arc::clone(
2022-10-05 20:34:31 +02:00
services()
.globals
2022-09-07 13:25:51 +02:00
.roomid_mutex_state
.write()
.await
2022-09-07 13:25:51 +02:00
.entry(room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
// Create a user for the server
2024-05-31 21:46:38 +01:00
let conduit_user = services().globals.server_user();
2022-09-07 13:25:51 +02:00
2024-05-31 21:46:38 +01:00
services().users.create(conduit_user, None)?;
2022-09-07 13:25:51 +02:00
let room_version = services().globals.default_room_version();
let mut content = match room_version {
RoomVersionId::V1
| RoomVersionId::V2
| RoomVersionId::V3
| RoomVersionId::V4
| RoomVersionId::V5
| RoomVersionId::V6
| RoomVersionId::V7
| RoomVersionId::V8
| RoomVersionId::V9
2024-05-31 21:46:38 +01:00
| RoomVersionId::V10 => RoomCreateEventContent::new_v1(conduit_user.to_owned()),
2023-12-24 19:04:48 +01:00
RoomVersionId::V11 => RoomCreateEventContent::new_v11(),
2024-04-12 05:14:39 +00:00
_ => unreachable!("Validity of room version already checked"),
};
2022-09-07 13:25:51 +02:00
content.federate = true;
content.predecessor = None;
content.room_version = room_version;
2022-09-07 13:25:51 +02:00
// 1. The room create event
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&content).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
// 2. Make conduit bot join
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: None,
avatar_url: None,
is_direct: None,
third_party_invite: None,
blurhash: None,
reason: None,
join_authorized_via_users_server: None,
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(conduit_user.to_string()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
// 3. Power levels
let mut users = BTreeMap::new();
2024-05-31 21:46:38 +01:00
users.insert(conduit_user.to_owned(), 100.into());
2022-09-07 13:25:51 +02:00
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&RoomPowerLevelsEventContent {
users,
..Default::default()
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
// 4.1 Join Rules
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomJoinRules,
content: to_raw_value(&RoomJoinRulesEventContent::new(JoinRule::Invite))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
// 4.2 History Visibility
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomHistoryVisibility,
content: to_raw_value(&RoomHistoryVisibilityEventContent::new(
HistoryVisibility::Shared,
))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
// 4.3 Guest Access
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomGuestAccess,
content: to_raw_value(&RoomGuestAccessEventContent::new(
GuestAccess::Forbidden,
))
2022-09-07 13:25:51 +02:00
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
// 5. Events implied by name and topic
2022-10-09 17:25:06 +02:00
let room_name = format!("{} Admin Room", services().globals.server_name());
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomName,
content: to_raw_value(&RoomNameEventContent::new(room_name))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomTopic,
content: to_raw_value(&RoomTopicEventContent {
topic: format!("Manage {}", services().globals.server_name()),
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
// 6. Room alias
2024-06-11 23:15:02 +02:00
let alias: OwnedRoomAliasId = services().globals.admin_alias().to_owned();
2022-09-07 13:25:51 +02:00
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCanonicalAlias,
content: to_raw_value(&RoomCanonicalAliasEventContent {
alias: Some(alias.clone()),
alt_aliases: Vec::new(),
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
2024-06-11 23:15:02 +02:00
services()
.rooms
.alias
.set_alias(&alias, &room_id, conduit_user)?;
2022-09-07 13:25:51 +02:00
Ok(())
}
2024-03-03 11:26:18 +00:00
/// Gets the room ID of the admin room
2022-09-07 13:25:51 +02:00
///
/// Errors are propagated from the database, and will have None if there is no admin room
pub(crate) fn get_admin_room(&self) -> Result<Option<OwnedRoomId>> {
2024-03-03 11:26:18 +00:00
services()
2022-09-07 13:25:51 +02:00
.rooms
2022-10-05 09:34:25 +02:00
.alias
2024-06-11 23:15:02 +02:00
.resolve_local_alias(services().globals.admin_alias())
2024-03-03 11:26:18 +00:00
}
2022-09-07 13:25:51 +02:00
/// Invite the user to the conduit admin room.
///
/// In conduit, this is equivalent to granting admin privileges.
pub(crate) async fn make_user_admin(
&self,
user_id: &UserId,
displayname: String,
) -> Result<()> {
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;
// Use the server user to grant the new admin's power level
2024-05-31 21:46:38 +01:00
let conduit_user = services().globals.server_user();
// Invite and join the real user
2022-10-05 20:34:31 +02:00
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Invite,
displayname: None,
avatar_url: None,
is_direct: None,
third_party_invite: None,
blurhash: None,
reason: None,
join_authorized_via_users_server: None,
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(user_id.to_string()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: Some(displayname),
avatar_url: None,
is_direct: None,
third_party_invite: None,
blurhash: None,
reason: None,
join_authorized_via_users_server: None,
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(user_id.to_string()),
redacts: None,
timestamp: None,
},
user_id,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
// Set power level
let mut users = BTreeMap::new();
users.insert(conduit_user.to_owned(), 100.into());
users.insert(user_id.to_owned(), 100.into());
2022-09-07 13:25:51 +02:00
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&RoomPowerLevelsEventContent {
users,
..Default::default()
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
timestamp: None,
},
2024-05-31 21:46:38 +01:00
conduit_user,
&room_id,
&state_lock,
)
.await?;
2022-09-07 13:25:51 +02:00
// Send welcome message
services().rooms.timeline.build_and_append_pdu(
2022-09-07 13:25:51 +02:00
PduBuilder {
2023-02-26 16:29:06 +01:00
event_type: TimelineEventType::RoomMessage,
2022-09-07 13:25:51 +02:00
content: to_raw_value(&RoomMessageEventContent::text_html(
2022-10-10 14:09:11 +02:00
format!("## Thank you for trying out Conduit!\n\nConduit is currently in Beta. This means you can join and participate in most Matrix rooms, but not all features are supported and you might run into bugs from time to time.\n\nHelpful links:\n> Website: https://conduit.rs\n> Git and Documentation: https://gitlab.com/famedly/conduit\n> Report issues: https://gitlab.com/famedly/conduit/-/issues\n\nFor a list of available commands, send the following message in this room: `@conduit:{}: --help`\n\nHere are some rooms you can join (by typing the command):\n\nConduit room (Ask questions and get notified on updates):\n`/join #conduit:fachschaften.org`\n\nConduit lounge (Off-topic, only Conduit users are allowed to join)\n`/join #conduit-lounge:conduit.rs`", services().globals.server_name()),
format!("<h2>Thank you for trying out Conduit!</h2>\n<p>Conduit is currently in Beta. This means you can join and participate in most Matrix rooms, but not all features are supported and you might run into bugs from time to time.</p>\n<p>Helpful links:</p>\n<blockquote>\n<p>Website: https://conduit.rs<br>Git and Documentation: https://gitlab.com/famedly/conduit<br>Report issues: https://gitlab.com/famedly/conduit/-/issues</p>\n</blockquote>\n<p>For a list of available commands, send the following message in this room: <code>@conduit:{}: --help</code></p>\n<p>Here are some rooms you can join (by typing the command):</p>\n<p>Conduit room (Ask questions and get notified on updates):<br><code>/join #conduit:fachschaften.org</code></p>\n<p>Conduit lounge (Off-topic, only Conduit users are allowed to join)<br><code>/join #conduit-lounge:conduit.rs</code></p>\n", services().globals.server_name()),
2022-09-07 13:25:51 +02:00
))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: None,
redacts: None,
timestamp: None,
2022-09-07 13:25:51 +02:00
},
2024-05-31 21:46:38 +01:00
conduit_user,
2022-09-07 13:25:51 +02:00
&room_id,
&state_lock,
).await?;
}
2022-09-07 13:25:51 +02:00
Ok(())
}
2024-06-11 23:15:02 +02:00
/// Checks whether a given user is an admin of this server
pub fn user_is_admin(&self, user_id: &UserId) -> Result<bool> {
let Some(admin_room) = self.get_admin_room()? else {
return Ok(false);
};
services().rooms.state_cache.is_joined(user_id, &admin_room)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn get_help_short() {
get_help_inner("-h");
}
#[test]
fn get_help_long() {
get_help_inner("--help");
}
#[test]
fn get_help_subcommand() {
get_help_inner("help");
}
fn get_help_inner(input: &str) {
let error = AdminCommand::try_parse_from(["argv[0] doesn't matter", input])
.unwrap_err()
.to_string();
// Search for a handful of keywords that suggest the help printed properly
assert!(error.contains("Usage:"));
assert!(error.contains("Commands:"));
assert!(error.contains("Options:"));
}
}