split admin commands into modules.
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
parent
6b1933914d
commit
d3c9f5595a
11 changed files with 2035 additions and 1960 deletions
92
src/service/admin/appservice.rs
Normal file
92
src/service/admin/appservice.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
use clap::Subcommand;
|
||||
use ruma::{api::appservice::Registration, events::room::message::RoomMessageEventContent};
|
||||
|
||||
use crate::{service::admin::escape_html, services, Result};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum AppserviceCommand {
|
||||
/// - Register an appservice using its registration YAML
|
||||
///
|
||||
/// 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.
|
||||
Register,
|
||||
|
||||
/// - Unregister an appservice using its ID
|
||||
///
|
||||
/// You can find the ID using the `list-appservices` command.
|
||||
Unregister {
|
||||
/// The appservice to unregister
|
||||
appservice_identifier: String,
|
||||
},
|
||||
|
||||
/// - Show an appservice's config using its ID
|
||||
///
|
||||
/// You can find the ID using the `list-appservices` command.
|
||||
Show {
|
||||
/// The appservice to show
|
||||
appservice_identifier: String,
|
||||
},
|
||||
|
||||
/// - List all the currently registered appservices
|
||||
List,
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: AppserviceCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
match command {
|
||||
AppserviceCommand::Register => {
|
||||
if body.len() > 2 && body[0].trim().starts_with("```") && 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 {
|
||||
Ok(yaml) => match services().appservice.register_appservice(yaml).await {
|
||||
Ok(id) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Appservice registered with ID: {id}."
|
||||
))),
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to register appservice: {e}"
|
||||
))),
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Could not parse appservice config: {e}"
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
))
|
||||
}
|
||||
},
|
||||
AppserviceCommand::Unregister {
|
||||
appservice_identifier,
|
||||
} => match services().appservice.unregister_appservice(&appservice_identifier).await {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Appservice unregistered.")),
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to unregister appservice: {e}"
|
||||
))),
|
||||
},
|
||||
AppserviceCommand::Show {
|
||||
appservice_identifier,
|
||||
} => match services().appservice.get_registration(&appservice_identifier).await {
|
||||
Some(config) => {
|
||||
let config_str = serde_yaml::to_string(&config).expect("config should've been validated on register");
|
||||
let output = format!("Config for {}:\n\n```yaml\n{}\n```", appservice_identifier, config_str,);
|
||||
let output_html = format!(
|
||||
"Config for {}:\n\n<pre><code class=\"language-yaml\">{}</code></pre>",
|
||||
escape_html(&appservice_identifier),
|
||||
escape_html(&config_str),
|
||||
);
|
||||
Ok(RoomMessageEventContent::text_html(output, output_html))
|
||||
},
|
||||
None => Ok(RoomMessageEventContent::text_plain("Appservice does not exist.")),
|
||||
},
|
||||
AppserviceCommand::List => {
|
||||
let appservices = services().appservice.iter_ids().await;
|
||||
let output = format!("Appservices ({}): {}", appservices.len(), appservices.join(", "));
|
||||
Ok(RoomMessageEventContent::text_plain(output))
|
||||
},
|
||||
}
|
||||
}
|
294
src/service/admin/debug.rs
Normal file
294
src/service/admin/debug.rs
Normal file
|
@ -0,0 +1,294 @@
|
|||
use std::{collections::BTreeMap, sync::Arc, time::Instant};
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::{
|
||||
api::client::error::ErrorKind, events::room::message::RoomMessageEventContent, CanonicalJsonObject, EventId,
|
||||
RoomId, RoomVersionId, ServerName,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::{api::server_server::parse_incoming_pdu, services, utils::HtmlEscape, Error, PduEvent, Result};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum DebugCommand {
|
||||
/// - Get the auth_chain of a PDU
|
||||
GetAuthChain {
|
||||
/// An event ID (the $ character followed by the base64 reference hash)
|
||||
event_id: Box<EventId>,
|
||||
},
|
||||
|
||||
/// - Parse and print a PDU from a JSON
|
||||
///
|
||||
/// The PDU event is only checked for validity and is not added to the
|
||||
/// database.
|
||||
///
|
||||
/// This command needs a JSON blob provided in a Markdown code block below
|
||||
/// the command.
|
||||
ParsePdu,
|
||||
|
||||
/// - Retrieve and print a PDU by ID from the conduwuit database
|
||||
GetPdu {
|
||||
/// An event ID (a $ followed by the base64 reference hash)
|
||||
event_id: Box<EventId>,
|
||||
},
|
||||
|
||||
/// - Attempts to retrieve a PDU from a remote server. Inserts it into our
|
||||
/// database/timeline if found and we do not have this PDU already
|
||||
/// (following normal event auth rules, handles it as an incoming PDU).
|
||||
GetRemotePdu {
|
||||
/// An event ID (a $ followed by the base64 reference hash)
|
||||
event_id: Box<EventId>,
|
||||
|
||||
/// Argument for us to attempt to fetch the event from the
|
||||
/// specified remote server.
|
||||
server: Box<ServerName>,
|
||||
},
|
||||
|
||||
/// - Gets all the room state events for the specified room.
|
||||
///
|
||||
/// This is functionally equivalent to `GET
|
||||
/// /_matrix/client/v3/rooms/{roomid}/state`, except the admin command does
|
||||
/// *not* check if the sender user is allowed to see state events. This is
|
||||
/// done because it's implied that server admins here have database access
|
||||
/// and can see/get room info themselves anyways if they were malicious
|
||||
/// admins.
|
||||
///
|
||||
/// Of course the check is still done on the actual client API.
|
||||
GetRoomState {
|
||||
/// Room ID
|
||||
room_id: Box<RoomId>,
|
||||
},
|
||||
|
||||
/// - Forces device lists for all local and remote users to be updated (as
|
||||
/// having new keys available)
|
||||
ForceDeviceListUpdates,
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: DebugCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
Ok(match command {
|
||||
DebugCommand::GetAuthChain {
|
||||
event_id,
|
||||
} => {
|
||||
let event_id = Arc::<EventId>::from(event_id);
|
||||
if let Some(event) = services().rooms.timeline.get_pdu_json(&event_id)? {
|
||||
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();
|
||||
let count = services().rooms.auth_chain.get_auth_chain(room_id, vec![event_id]).await?.count();
|
||||
let elapsed = start.elapsed();
|
||||
RoomMessageEventContent::text_plain(format!("Loaded auth chain with length {count} in {elapsed:?}"))
|
||||
} else {
|
||||
RoomMessageEventContent::text_plain("Event not found.")
|
||||
}
|
||||
},
|
||||
DebugCommand::ParsePdu => {
|
||||
if body.len() > 2 && 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(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
|
||||
Ok(hash) => {
|
||||
let event_id = EventId::parse(format!("${hash}"));
|
||||
|
||||
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:#?}"))
|
||||
},
|
||||
Err(e) => RoomMessageEventContent::text_plain(format!(
|
||||
"EventId: {event_id:?}\nCould not parse event: {e}"
|
||||
)),
|
||||
}
|
||||
},
|
||||
Err(e) => RoomMessageEventContent::text_plain(format!("Could not parse PDU JSON: {e:?}")),
|
||||
},
|
||||
Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json in command body: {e}")),
|
||||
}
|
||||
} else {
|
||||
RoomMessageEventContent::text_plain("Expected code block in command body.")
|
||||
}
|
||||
},
|
||||
DebugCommand::GetPdu {
|
||||
event_id,
|
||||
} => {
|
||||
let mut outlier = false;
|
||||
let mut pdu_json = services().rooms.timeline.get_non_outlier_pdu_json(&event_id)?;
|
||||
if pdu_json.is_none() {
|
||||
outlier = true;
|
||||
pdu_json = services().rooms.timeline.get_pdu_json(&event_id)?;
|
||||
}
|
||||
match pdu_json {
|
||||
Some(json) => {
|
||||
let json_text = serde_json::to_string_pretty(&json).expect("canonical json is valid json");
|
||||
return Ok(RoomMessageEventContent::text_html(
|
||||
format!(
|
||||
"{}\n```json\n{}\n```",
|
||||
if outlier {
|
||||
"Outlier PDU found in our database"
|
||||
} else {
|
||||
"PDU found in our database"
|
||||
},
|
||||
json_text
|
||||
),
|
||||
format!(
|
||||
"<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
|
||||
if outlier {
|
||||
"Outlier PDU found in our database"
|
||||
} else {
|
||||
"PDU found in our database"
|
||||
},
|
||||
HtmlEscape(&json_text)
|
||||
),
|
||||
));
|
||||
},
|
||||
None => {
|
||||
return Ok(RoomMessageEventContent::text_plain("PDU not found locally."));
|
||||
},
|
||||
}
|
||||
},
|
||||
DebugCommand::GetRemotePdu {
|
||||
event_id,
|
||||
server,
|
||||
} => {
|
||||
if !services().globals.config.allow_federation {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Federation is disabled on this homeserver.",
|
||||
));
|
||||
}
|
||||
|
||||
if server == services().globals.server_name() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Not allowed to send federation requests to ourselves. Please use `get-pdu` for fetching local \
|
||||
PDUs.",
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: use Futures as some requests may take a while so we dont block the
|
||||
// admin room
|
||||
match services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&server,
|
||||
ruma::api::federation::event::get_event::v1::Request {
|
||||
event_id: event_id.clone().into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let json: CanonicalJsonObject = serde_json::from_str(response.pdu.get()).map_err(|e| {
|
||||
warn!(
|
||||
"Requested event ID {event_id} from server but failed to convert from RawValue to \
|
||||
CanonicalJsonObject (malformed event/response?): {e}"
|
||||
);
|
||||
Error::BadRequest(ErrorKind::Unknown, "Received response from server but failed to parse PDU")
|
||||
})?;
|
||||
|
||||
debug!("Attempting to parse PDU: {:?}", &response.pdu);
|
||||
let parsed_pdu = {
|
||||
let parsed_result = parse_incoming_pdu(&response.pdu);
|
||||
let (event_id, value, room_id) = match parsed_result {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse PDU: {e}");
|
||||
info!("Full PDU: {:?}", &response.pdu);
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to parse PDU remote server {server} sent us: {e}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
|
||||
vec![(event_id, value, room_id)]
|
||||
};
|
||||
|
||||
let pub_key_map = RwLock::new(BTreeMap::new());
|
||||
|
||||
debug!("Attempting to fetch homeserver signing keys for {server}");
|
||||
services()
|
||||
.rooms
|
||||
.event_handler
|
||||
.fetch_required_signing_keys(
|
||||
parsed_pdu.iter().map(|(_event_id, event, _room_id)| event),
|
||||
&pub_key_map,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("Could not fetch all signatures for PDUs from {server}: {e:?}");
|
||||
});
|
||||
|
||||
info!("Attempting to handle event ID {event_id} as backfilled PDU");
|
||||
services().rooms.timeline.backfill_pdu(&server, response.pdu, &pub_key_map).await?;
|
||||
|
||||
let json_text = serde_json::to_string_pretty(&json).expect("canonical json is valid json");
|
||||
|
||||
return Ok(RoomMessageEventContent::text_html(
|
||||
format!(
|
||||
"{}\n```json\n{}\n```",
|
||||
"Got PDU from specified server and handled as backfilled PDU successfully. Event body:",
|
||||
json_text
|
||||
),
|
||||
format!(
|
||||
"<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
|
||||
"Got PDU from specified server and handled as backfilled PDU successfully. Event body:",
|
||||
HtmlEscape(&json_text)
|
||||
),
|
||||
));
|
||||
},
|
||||
Err(_) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Remote server did not have PDU or failed sending request to remote server.",
|
||||
));
|
||||
},
|
||||
}
|
||||
},
|
||||
DebugCommand::GetRoomState {
|
||||
room_id,
|
||||
} => {
|
||||
let room_state = services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(&room_id)
|
||||
.await?
|
||||
.values()
|
||||
.map(|pdu| pdu.to_state_event())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if room_state.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Unable to find room state in our database (vector is empty)",
|
||||
));
|
||||
}
|
||||
|
||||
let json_text = serde_json::to_string_pretty(&room_state).map_err(|e| {
|
||||
error!("Failed converting room state vector in our database to pretty JSON: {e}");
|
||||
Error::bad_database(
|
||||
"Failed to convert room state events to pretty JSON, possible invalid room state events in our \
|
||||
database",
|
||||
)
|
||||
})?;
|
||||
|
||||
return Ok(RoomMessageEventContent::text_html(
|
||||
format!("{}\n```json\n{}\n```", "Found full room state", json_text),
|
||||
format!(
|
||||
"<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
|
||||
"Found full room state",
|
||||
HtmlEscape(&json_text)
|
||||
),
|
||||
));
|
||||
},
|
||||
DebugCommand::ForceDeviceListUpdates => {
|
||||
// Force E2EE device list updates for all users
|
||||
for user_id in services().users.iter().filter_map(std::result::Result::ok) {
|
||||
services().users.mark_device_key_update(&user_id)?;
|
||||
}
|
||||
RoomMessageEventContent::text_plain("Marked all devices for all users as having new keys to update")
|
||||
},
|
||||
})
|
||||
}
|
110
src/service/admin/federation.rs
Normal file
110
src/service/admin/federation.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use std::{collections::BTreeMap, fmt::Write as _};
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{services, Result};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum FederationCommand {
|
||||
/// - List all rooms we are currently handling an incoming pdu from
|
||||
IncomingFederation,
|
||||
|
||||
/// - 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>,
|
||||
},
|
||||
|
||||
/// - Verify json signatures
|
||||
///
|
||||
/// This command needs a JSON blob provided in a Markdown code block below
|
||||
/// the command.
|
||||
SignJson,
|
||||
|
||||
/// - Verify json signatures
|
||||
///
|
||||
/// This command needs a JSON blob provided in a Markdown code block below
|
||||
/// the command.
|
||||
VerifyJson,
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: FederationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
match command {
|
||||
FederationCommand::DisableRoom {
|
||||
room_id,
|
||||
} => {
|
||||
services().rooms.metadata.disable_room(&room_id, true)?;
|
||||
Ok(RoomMessageEventContent::text_plain("Room disabled."))
|
||||
},
|
||||
FederationCommand::EnableRoom {
|
||||
room_id,
|
||||
} => {
|
||||
services().rooms.metadata.disable_room(&room_id, false)?;
|
||||
Ok(RoomMessageEventContent::text_plain("Room enabled."))
|
||||
},
|
||||
FederationCommand::IncomingFederation => {
|
||||
let map = services().globals.roomid_federationhandletime.read().await;
|
||||
let mut msg = format!("Handling {} incoming pdus:\n", map.len());
|
||||
|
||||
for (r, (e, i)) in map.iter() {
|
||||
let elapsed = i.elapsed();
|
||||
let _ = writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60);
|
||||
}
|
||||
Ok(RoomMessageEventContent::text_plain(&msg))
|
||||
},
|
||||
FederationCommand::SignJson => {
|
||||
if body.len() > 2 && 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) => {
|
||||
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");
|
||||
Ok(RoomMessageEventContent::text_plain(json_text))
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))),
|
||||
}
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
))
|
||||
}
|
||||
},
|
||||
FederationCommand::VerifyJson => {
|
||||
if body.len() > 2 && 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(value) => {
|
||||
let pub_key_map = RwLock::new(BTreeMap::new());
|
||||
|
||||
services().rooms.event_handler.fetch_required_signing_keys([&value], &pub_key_map).await?;
|
||||
|
||||
let pub_key_map = pub_key_map.read().await;
|
||||
match ruma::signatures::verify_json(&pub_key_map, &value) {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")),
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Signature verification failed: {e}"
|
||||
))),
|
||||
}
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))),
|
||||
}
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
213
src/service/admin/media.rs
Normal file
213
src/service/admin/media.rs
Normal file
|
@ -0,0 +1,213 @@
|
|||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, EventId};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{service::admin::MxcUri, services, Result};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum MediaCommand {
|
||||
/// - Deletes a single media file from our database and on the filesystem
|
||||
/// via a single MXC URL
|
||||
Delete {
|
||||
/// The MXC URL to delete
|
||||
#[arg(long)]
|
||||
mxc: Option<Box<MxcUri>>,
|
||||
|
||||
/// - The message event ID which contains the media and thumbnail MXC
|
||||
/// URLs
|
||||
#[arg(long)]
|
||||
event_id: Option<Box<EventId>>,
|
||||
},
|
||||
|
||||
/// - Deletes a codeblock list of MXC URLs from our database and on the
|
||||
/// filesystem
|
||||
DeleteList,
|
||||
|
||||
/// - Deletes all remote media in the last X amount of time using filesystem
|
||||
/// metadata first created at date.
|
||||
DeletePastRemoteMedia {
|
||||
/// - The duration (at or after), e.g. "5m" to delete all media in the
|
||||
/// past 5 minutes
|
||||
duration: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: MediaCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
match command {
|
||||
MediaCommand::Delete {
|
||||
mxc,
|
||||
event_id,
|
||||
} => {
|
||||
if event_id.is_some() && mxc.is_some() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Please specify either an MXC or an event ID, not both.",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(mxc) = mxc {
|
||||
if !mxc.to_string().starts_with("mxc://") {
|
||||
return Ok(RoomMessageEventContent::text_plain("MXC provided is not valid."));
|
||||
}
|
||||
|
||||
debug!("Got MXC URL: {}", mxc);
|
||||
services().media.delete(mxc.to_string()).await?;
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Deleted the MXC from our database and on our filesystem.",
|
||||
));
|
||||
} else if let Some(event_id) = event_id {
|
||||
debug!("Got event ID to delete media from: {}", event_id);
|
||||
|
||||
let mut mxc_urls = vec![];
|
||||
let mut mxc_deletion_count = 0;
|
||||
|
||||
// parsing the PDU for any MXC URLs begins here
|
||||
if let Some(event_json) = services().rooms.timeline.get_pdu_json(&event_id)? {
|
||||
if let Some(content_key) = event_json.get("content") {
|
||||
debug!("Event ID has \"content\".");
|
||||
let content_obj = content_key.as_object();
|
||||
|
||||
if let Some(content) = content_obj {
|
||||
// 1. attempts to parse the "url" key
|
||||
debug!("Attempting to go into \"url\" key for main media file");
|
||||
if let Some(url) = content.get("url") {
|
||||
debug!("Got a URL in the event ID {event_id}: {url}");
|
||||
|
||||
if url.to_string().starts_with("\"mxc://") {
|
||||
debug!("Pushing URL {} to list of MXCs to delete", url);
|
||||
let final_url = url.to_string().replace('"', "");
|
||||
mxc_urls.push(final_url);
|
||||
} else {
|
||||
info!(
|
||||
"Found a URL in the event ID {event_id} but did not start with mxc://, \
|
||||
ignoring"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. attempts to parse the "info" key
|
||||
debug!("Attempting to go into \"info\" key for thumbnails");
|
||||
if let Some(info_key) = content.get("info") {
|
||||
debug!("Event ID has \"info\".");
|
||||
let info_obj = info_key.as_object();
|
||||
|
||||
if let Some(info) = info_obj {
|
||||
if let Some(thumbnail_url) = info.get("thumbnail_url") {
|
||||
debug!("Found a thumbnail_url in info key: {thumbnail_url}");
|
||||
|
||||
if thumbnail_url.to_string().starts_with("\"mxc://") {
|
||||
debug!("Pushing thumbnail URL {} to list of MXCs to delete", thumbnail_url);
|
||||
let final_thumbnail_url = thumbnail_url.to_string().replace('"', "");
|
||||
mxc_urls.push(final_thumbnail_url);
|
||||
} else {
|
||||
info!(
|
||||
"Found a thumbnail URL in the event ID {event_id} but did not start \
|
||||
with mxc://, ignoring"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
info!("No \"thumbnail_url\" key in \"info\" key, assuming no thumbnails.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. attempts to parse the "file" key
|
||||
debug!("Attempting to go into \"file\" key");
|
||||
if let Some(file_key) = content.get("file") {
|
||||
debug!("Event ID has \"file\".");
|
||||
let file_obj = file_key.as_object();
|
||||
|
||||
if let Some(file) = file_obj {
|
||||
if let Some(url) = file.get("url") {
|
||||
debug!("Found url in file key: {url}");
|
||||
|
||||
if url.to_string().starts_with("\"mxc://") {
|
||||
debug!("Pushing URL {} to list of MXCs to delete", url);
|
||||
let final_url = url.to_string().replace('"', "");
|
||||
mxc_urls.push(final_url);
|
||||
} else {
|
||||
info!(
|
||||
"Found a URL in the event ID {event_id} but did not start with \
|
||||
mxc://, ignoring"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
info!("No \"url\" key in \"file\" key.");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Event ID does not have a \"content\" key or failed parsing the event ID JSON.",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Event ID does not have a \"content\" key, this is not a message or an event type that \
|
||||
contains media.",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Event ID does not exist or is not known to us.",
|
||||
));
|
||||
}
|
||||
|
||||
if mxc_urls.is_empty() {
|
||||
// we shouldn't get here (should have errored earlier) but just in case for
|
||||
// whatever reason we do...
|
||||
info!("Parsed event ID {event_id} but did not contain any MXC URLs.");
|
||||
return Ok(RoomMessageEventContent::text_plain("Parsed event ID but found no MXC URLs."));
|
||||
}
|
||||
|
||||
for mxc_url in mxc_urls {
|
||||
services().media.delete(mxc_url).await?;
|
||||
mxc_deletion_count += 1;
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Deleted {mxc_deletion_count} total MXCs from our database and the filesystem from event ID \
|
||||
{event_id}."
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Please specify either an MXC using --mxc or an event ID using --event-id of the message containing \
|
||||
an image. See --help for details.",
|
||||
))
|
||||
},
|
||||
MediaCommand::DeleteList => {
|
||||
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
|
||||
let mxc_list = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
|
||||
|
||||
let mut mxc_deletion_count = 0;
|
||||
|
||||
for mxc in mxc_list {
|
||||
debug!("Deleting MXC {} in bulk", mxc);
|
||||
services().media.delete(mxc.to_owned()).await?;
|
||||
mxc_deletion_count += 1;
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Finished bulk MXC deletion, deleted {} total MXCs from our database and the filesystem.",
|
||||
mxc_deletion_count
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
))
|
||||
},
|
||||
MediaCommand::DeletePastRemoteMedia {
|
||||
duration,
|
||||
} => {
|
||||
let deleted_count = services().media.delete_all_remote_media_at_after_time(duration).await?;
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Deleted {} total files.",
|
||||
deleted_count
|
||||
)))
|
||||
},
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
90
src/service/admin/room.rs
Normal file
90
src/service/admin/room.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
|
||||
|
||||
use crate::{
|
||||
service::admin::{
|
||||
escape_html, get_room_info, room_alias, room_alias::RoomAliasCommand, room_directory,
|
||||
room_directory::RoomDirectoryCommand, room_moderation, room_moderation::RoomModerationCommand, PAGE_SIZE,
|
||||
},
|
||||
services, Result,
|
||||
};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum RoomCommand {
|
||||
/// - List all rooms the server knows about
|
||||
List {
|
||||
page: Option<usize>,
|
||||
},
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Manage moderation of remote or local rooms
|
||||
Moderation(RoomModerationCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Manage rooms' aliases
|
||||
Alias(RoomAliasCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Manage the room directory
|
||||
Directory(RoomDirectoryCommand),
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: RoomCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
match command {
|
||||
RoomCommand::Alias(command) => room_alias::process(command, body).await,
|
||||
|
||||
RoomCommand::Directory(command) => room_directory::process(command, body).await,
|
||||
|
||||
RoomCommand::Moderation(command) => room_moderation::process(command, body).await,
|
||||
|
||||
RoomCommand::List {
|
||||
page,
|
||||
} => {
|
||||
// TODO: i know there's a way to do this with clap, but i can't seem to find it
|
||||
let page = page.unwrap_or(1);
|
||||
let mut rooms = services()
|
||||
.rooms
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.filter_map(Result::ok)
|
||||
.map(|id: OwnedRoomId| get_room_info(&id))
|
||||
.collect::<Vec<_>>();
|
||||
rooms.sort_by_key(|r| r.1);
|
||||
rooms.reverse();
|
||||
|
||||
let rooms = rooms.into_iter().skip(page.saturating_sub(1) * PAGE_SIZE).take(PAGE_SIZE).collect::<Vec<_>>();
|
||||
|
||||
if rooms.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain("No more rooms."));
|
||||
};
|
||||
|
||||
let output_plain = format!(
|
||||
"Rooms:\n{}",
|
||||
rooms
|
||||
.iter()
|
||||
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
let output_html = format!(
|
||||
"<table><caption>Room list - page \
|
||||
{page}</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
|
||||
rooms.iter().fold(String::new(), |mut output, (id, members, name)| {
|
||||
writeln!(
|
||||
output,
|
||||
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
|
||||
escape_html(id.as_ref()),
|
||||
members,
|
||||
escape_html(name)
|
||||
)
|
||||
.unwrap();
|
||||
output
|
||||
})
|
||||
);
|
||||
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
|
||||
},
|
||||
}
|
||||
}
|
160
src/service/admin/room_alias.rs
Normal file
160
src/service/admin/room_alias.rs
Normal file
|
@ -0,0 +1,160 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId, RoomId};
|
||||
|
||||
use crate::{service::admin::escape_html, services, Result};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum RoomAliasCommand {
|
||||
/// - Make an alias point to a room.
|
||||
Set {
|
||||
#[arg(short, long)]
|
||||
/// Set the alias even if a room is already using it
|
||||
force: bool,
|
||||
|
||||
/// The room id to set the alias on
|
||||
room_id: Box<RoomId>,
|
||||
|
||||
/// The alias localpart to use (`alias`, not `#alias:servername.tld`)
|
||||
room_alias_localpart: String,
|
||||
},
|
||||
|
||||
/// - Remove an alias
|
||||
Remove {
|
||||
/// The alias localpart to remove (`alias`, not `#alias:servername.tld`)
|
||||
room_alias_localpart: String,
|
||||
},
|
||||
|
||||
/// - Show which room is using an alias
|
||||
Which {
|
||||
/// The alias localpart to look up (`alias`, not
|
||||
/// `#alias:servername.tld`)
|
||||
room_alias_localpart: String,
|
||||
},
|
||||
|
||||
/// - List aliases currently being used
|
||||
List {
|
||||
/// If set, only list the aliases for this room
|
||||
room_id: Option<Box<RoomId>>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
match command {
|
||||
RoomAliasCommand::Set {
|
||||
ref room_alias_localpart,
|
||||
..
|
||||
}
|
||||
| RoomAliasCommand::Remove {
|
||||
ref room_alias_localpart,
|
||||
}
|
||||
| RoomAliasCommand::Which {
|
||||
ref room_alias_localpart,
|
||||
} => {
|
||||
let room_alias_str = format!("#{}:{}", room_alias_localpart, services().globals.server_name());
|
||||
let room_alias = match RoomAliasId::parse_box(room_alias_str) {
|
||||
Ok(alias) => alias,
|
||||
Err(err) => return Ok(RoomMessageEventContent::text_plain(format!("Failed to parse alias: {}", err))),
|
||||
};
|
||||
match command {
|
||||
RoomAliasCommand::Set {
|
||||
force,
|
||||
room_id,
|
||||
..
|
||||
} => match (force, services().rooms.alias.resolve_local_alias(&room_alias)) {
|
||||
(true, Ok(Some(id))) => match services().rooms.alias.set_alias(&room_alias, &room_id) {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Successfully overwrote alias (formerly {})",
|
||||
id
|
||||
))),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {}", err))),
|
||||
},
|
||||
(false, Ok(Some(id))) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Refusing to overwrite in use alias for {}, use -f or --force to overwrite",
|
||||
id
|
||||
))),
|
||||
(_, Ok(None)) => match services().rooms.alias.set_alias(&room_alias, &room_id) {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Successfully set alias")),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))),
|
||||
},
|
||||
(_, Err(err)) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {err}"))),
|
||||
},
|
||||
RoomAliasCommand::Remove {
|
||||
..
|
||||
} => match services().rooms.alias.resolve_local_alias(&room_alias) {
|
||||
Ok(Some(id)) => match services().rooms.alias.remove_alias(&room_alias) {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!("Removed alias from {}", id))),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {}", err))),
|
||||
},
|
||||
Ok(None) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {}", err))),
|
||||
},
|
||||
RoomAliasCommand::Which {
|
||||
..
|
||||
} => match services().rooms.alias.resolve_local_alias(&room_alias) {
|
||||
Ok(Some(id)) => Ok(RoomMessageEventContent::text_plain(format!("Alias resolves to {}", id))),
|
||||
Ok(None) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {}", err))),
|
||||
},
|
||||
RoomAliasCommand::List {
|
||||
..
|
||||
} => unreachable!(),
|
||||
}
|
||||
},
|
||||
RoomAliasCommand::List {
|
||||
room_id,
|
||||
} => {
|
||||
if let Some(room_id) = room_id {
|
||||
let aliases = services().rooms.alias.local_aliases_for_room(&room_id).collect::<Result<Vec<_>, _>>();
|
||||
match aliases {
|
||||
Ok(aliases) => {
|
||||
let plain_list = aliases.iter().fold(String::new(), |mut output, alias| {
|
||||
writeln!(output, "- {alias}").unwrap();
|
||||
output
|
||||
});
|
||||
|
||||
let html_list = aliases.iter().fold(String::new(), |mut output, alias| {
|
||||
writeln!(output, "<li>{}</li>", escape_html(alias.as_ref())).unwrap();
|
||||
output
|
||||
});
|
||||
|
||||
let plain = format!("Aliases for {room_id}:\n{plain_list}");
|
||||
let html = format!("Aliases for {room_id}:\n<ul>{html_list}</ul>");
|
||||
Ok(RoomMessageEventContent::text_html(plain, html))
|
||||
},
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to list aliases: {}", err))),
|
||||
}
|
||||
} else {
|
||||
let aliases = services().rooms.alias.all_local_aliases().collect::<Result<Vec<_>, _>>();
|
||||
match aliases {
|
||||
Ok(aliases) => {
|
||||
let server_name = services().globals.server_name();
|
||||
let plain_list = aliases.iter().fold(String::new(), |mut output, (alias, id)| {
|
||||
writeln!(output, "- `{alias}` -> #{id}:{server_name}").unwrap();
|
||||
output
|
||||
});
|
||||
|
||||
let html_list = aliases.iter().fold(String::new(), |mut output, (alias, id)| {
|
||||
writeln!(
|
||||
output,
|
||||
"<li><code>{}</code> -> #{}:{}</li>",
|
||||
escape_html(alias.as_ref()),
|
||||
escape_html(id.as_ref()),
|
||||
server_name
|
||||
)
|
||||
.unwrap();
|
||||
output
|
||||
});
|
||||
|
||||
let plain = format!("Aliases:\n{plain_list}");
|
||||
let html = format!("Aliases:\n<ul>{html_list}</ul>");
|
||||
Ok(RoomMessageEventContent::text_html(plain, html))
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Unable to list room aliases: {e}"))),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
93
src/service/admin/room_directory.rs
Normal file
93
src/service/admin/room_directory.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId};
|
||||
|
||||
use crate::{
|
||||
service::admin::{escape_html, get_room_info, PAGE_SIZE},
|
||||
services, Result,
|
||||
};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum RoomDirectoryCommand {
|
||||
/// - Publish a room to the room directory
|
||||
Publish {
|
||||
/// The room id of the room to publish
|
||||
room_id: Box<RoomId>,
|
||||
},
|
||||
|
||||
/// - Unpublish a room to the room directory
|
||||
Unpublish {
|
||||
/// The room id of the room to unpublish
|
||||
room_id: Box<RoomId>,
|
||||
},
|
||||
|
||||
/// - List rooms that are published
|
||||
List {
|
||||
page: Option<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: RoomDirectoryCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
match command {
|
||||
RoomDirectoryCommand::Publish {
|
||||
room_id,
|
||||
} => match services().rooms.directory.set_public(&room_id) {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Room published")),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to update room: {}", err))),
|
||||
},
|
||||
RoomDirectoryCommand::Unpublish {
|
||||
room_id,
|
||||
} => match services().rooms.directory.set_not_public(&room_id) {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Room unpublished")),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to update room: {}", err))),
|
||||
},
|
||||
RoomDirectoryCommand::List {
|
||||
page,
|
||||
} => {
|
||||
// TODO: i know there's a way to do this with clap, but i can't seem to find it
|
||||
let page = page.unwrap_or(1);
|
||||
let mut rooms = services()
|
||||
.rooms
|
||||
.directory
|
||||
.public_rooms()
|
||||
.filter_map(Result::ok)
|
||||
.map(|id: OwnedRoomId| get_room_info(&id))
|
||||
.collect::<Vec<_>>();
|
||||
rooms.sort_by_key(|r| r.1);
|
||||
rooms.reverse();
|
||||
|
||||
let rooms = rooms.into_iter().skip(page.saturating_sub(1) * PAGE_SIZE).take(PAGE_SIZE).collect::<Vec<_>>();
|
||||
|
||||
if rooms.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain("No more rooms."));
|
||||
};
|
||||
|
||||
let output_plain = format!(
|
||||
"Rooms:\n{}",
|
||||
rooms
|
||||
.iter()
|
||||
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
let output_html = format!(
|
||||
"<table><caption>Room directory - page \
|
||||
{page}</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
|
||||
rooms.iter().fold(String::new(), |mut output, (id, members, name)| {
|
||||
writeln!(
|
||||
output,
|
||||
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
|
||||
escape_html(id.as_ref()),
|
||||
members,
|
||||
escape_html(name.as_ref())
|
||||
)
|
||||
.unwrap();
|
||||
output
|
||||
})
|
||||
);
|
||||
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
|
||||
},
|
||||
}
|
||||
}
|
480
src/service/admin/room_moderation.rs
Normal file
480
src/service/admin/room_moderation.rs
Normal file
|
@ -0,0 +1,480 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::{
|
||||
api::client_server::{get_alias_helper, leave_room},
|
||||
service::admin::{escape_html, Service},
|
||||
services, Result,
|
||||
};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum RoomModerationCommand {
|
||||
/// - Bans a room from local users joining and evicts all our local users
|
||||
/// from the room. Also blocks any invites (local and remote) for the
|
||||
/// banned room.
|
||||
///
|
||||
/// Server admins (users in the conduwuit admin room) will not be evicted
|
||||
/// and server admins can still join the room. To evict admins too, use
|
||||
/// --force (also ignores errors) To disable incoming federation of the
|
||||
/// room, use --disable-federation
|
||||
BanRoom {
|
||||
#[arg(short, long)]
|
||||
/// Evicts admins out of the room and ignores any potential errors when
|
||||
/// making our local users leave the room
|
||||
force: bool,
|
||||
|
||||
#[arg(long)]
|
||||
/// Disables incoming federation of the room after banning and evicting
|
||||
/// users
|
||||
disable_federation: bool,
|
||||
|
||||
/// The room in the format of `!roomid:example.com` or a room alias in
|
||||
/// the format of `#roomalias:example.com`
|
||||
room: Box<RoomOrAliasId>,
|
||||
},
|
||||
|
||||
/// - Bans a list of rooms from a newline delimited codeblock similar to
|
||||
/// `user deactivate-all`
|
||||
BanListOfRooms {
|
||||
#[arg(short, long)]
|
||||
/// Evicts admins out of the room and ignores any potential errors when
|
||||
/// making our local users leave the room
|
||||
force: bool,
|
||||
|
||||
#[arg(long)]
|
||||
/// Disables incoming federation of the room after banning and evicting
|
||||
/// users
|
||||
disable_federation: bool,
|
||||
},
|
||||
|
||||
/// - Unbans a room to allow local users to join again
|
||||
///
|
||||
/// To re-enable incoming federation of the room, use --enable-federation
|
||||
UnbanRoom {
|
||||
#[arg(long)]
|
||||
/// Enables incoming federation of the room after unbanning
|
||||
enable_federation: bool,
|
||||
|
||||
/// The room in the format of `!roomid:example.com` or a room alias in
|
||||
/// the format of `#roomalias:example.com`
|
||||
room: Box<RoomOrAliasId>,
|
||||
},
|
||||
|
||||
/// - List of all rooms we have banned
|
||||
ListBannedRooms,
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
match command {
|
||||
RoomModerationCommand::BanRoom {
|
||||
force,
|
||||
room,
|
||||
disable_federation,
|
||||
} => {
|
||||
debug!("Got room alias or ID: {}", room);
|
||||
|
||||
let admin_room_alias: Box<RoomAliasId> = format!("#admins:{}", services().globals.server_name())
|
||||
.try_into()
|
||||
.expect("#admins:server_name is a valid alias name");
|
||||
|
||||
if let Some(admin_room_id) = Service::get_admin_room()? {
|
||||
if room.to_string().eq(&admin_room_id) || room.to_string().eq(&admin_room_alias) {
|
||||
return Ok(RoomMessageEventContent::text_plain("Not allowed to ban the admin room."));
|
||||
}
|
||||
}
|
||||
|
||||
let room_id = if room.is_room_id() {
|
||||
let room_id = match RoomId::parse(&room) {
|
||||
Ok(room_id) => room_id,
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to parse room ID {room}. Please note that this requires a full room ID \
|
||||
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
|
||||
)))
|
||||
},
|
||||
};
|
||||
|
||||
debug!("Room specified is a room ID, banning room ID");
|
||||
|
||||
services().rooms.metadata.ban_room(&room_id, true)?;
|
||||
|
||||
room_id
|
||||
} else if room.is_room_alias_id() {
|
||||
let room_alias = match RoomAliasId::parse(&room) {
|
||||
Ok(room_alias) => room_alias,
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to parse room ID {room}. Please note that this requires a full room ID \
|
||||
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
|
||||
)))
|
||||
},
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Room specified is not a room ID, attempting to resolve room alias to a room ID locally, if not \
|
||||
using get_alias_helper to fetch room ID remotely"
|
||||
);
|
||||
|
||||
let room_id = match services().rooms.alias.resolve_local_alias(&room_alias)? {
|
||||
Some(room_id) => room_id,
|
||||
None => {
|
||||
debug!(
|
||||
"We don't have this room alias to a room ID locally, attempting to fetch room ID over \
|
||||
federation"
|
||||
);
|
||||
|
||||
match get_alias_helper(room_alias).await {
|
||||
Ok(response) => {
|
||||
debug!("Got federation response fetching room ID for room {room}: {:?}", response);
|
||||
response.room_id
|
||||
},
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to resolve room alias {room} to a room ID: {e}"
|
||||
)));
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
services().rooms.metadata.ban_room(&room_id, true)?;
|
||||
|
||||
room_id
|
||||
} else {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Room specified is not a room ID or room alias. Please note that this requires a full room ID \
|
||||
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`)",
|
||||
));
|
||||
};
|
||||
|
||||
debug!("Making all users leave the room {}", &room);
|
||||
if force {
|
||||
for local_user in services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&room_id)
|
||||
.filter_map(|user| {
|
||||
user.ok().filter(|local_user| {
|
||||
local_user.server_name() == services().globals.server_name()
|
||||
// additional wrapped check here is to avoid adding remote users
|
||||
// who are in the admin room to the list of local users (would fail auth check)
|
||||
&& (local_user.server_name()
|
||||
== services().globals.server_name()
|
||||
&& services()
|
||||
.users
|
||||
.is_admin(local_user)
|
||||
.unwrap_or(true)) // since this is a force
|
||||
// operation, assume user
|
||||
// is an admin if somehow
|
||||
// this fails
|
||||
})
|
||||
})
|
||||
.collect::<Vec<OwnedUserId>>()
|
||||
{
|
||||
debug!(
|
||||
"Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)",
|
||||
&local_user, &room_id
|
||||
);
|
||||
|
||||
_ = leave_room(&local_user, &room_id, None).await;
|
||||
}
|
||||
} else {
|
||||
for local_user in services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&room_id)
|
||||
.filter_map(|user| {
|
||||
user.ok().filter(|local_user| {
|
||||
local_user.server_name() == services().globals.server_name()
|
||||
// additional wrapped check here is to avoid adding remote users
|
||||
// who are in the admin room to the list of local users (would fail auth check)
|
||||
&& (local_user.server_name()
|
||||
== services().globals.server_name()
|
||||
&& !services()
|
||||
.users
|
||||
.is_admin(local_user)
|
||||
.unwrap_or(false))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<OwnedUserId>>()
|
||||
{
|
||||
debug!("Attempting leave for user {} in room {}", &local_user, &room_id);
|
||||
if let Err(e) = leave_room(&local_user, &room_id, None).await {
|
||||
error!(
|
||||
"Error attempting to make local user {} leave room {} during room banning: {}",
|
||||
&local_user, &room_id, e
|
||||
);
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Error attempting to make local user {} leave room {} during room banning (room is still \
|
||||
banned but not removing any more users): {}\nIf you would like to ignore errors, use \
|
||||
--force",
|
||||
&local_user, &room_id, e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if disable_federation {
|
||||
services().rooms.metadata.disable_room(&room_id, true)?;
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Room banned, removed all our local users, and disabled incoming federation with room.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Room banned and removed all our local users, use disable-room to stop receiving new inbound \
|
||||
federation events as well if needed.",
|
||||
))
|
||||
},
|
||||
RoomModerationCommand::BanListOfRooms {
|
||||
force,
|
||||
disable_federation,
|
||||
} => {
|
||||
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
|
||||
let rooms_s = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
|
||||
|
||||
let mut room_ban_count = 0;
|
||||
let mut room_ids: Vec<&RoomId> = Vec::new();
|
||||
|
||||
for &room_id in &rooms_s {
|
||||
match <&RoomId>::try_from(room_id) {
|
||||
Ok(owned_room_id) => {
|
||||
// silently ignore deleting admin room
|
||||
if let Some(admin_room_id) = Service::get_admin_room()? {
|
||||
if owned_room_id.eq(&admin_room_id) {
|
||||
info!("User specified admin room in bulk ban list, ignoring");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
room_ids.push(owned_room_id);
|
||||
},
|
||||
Err(e) => {
|
||||
if force {
|
||||
// ignore rooms we failed to parse if we're force deleting
|
||||
error!(
|
||||
"Error parsing room ID {room_id} during bulk room banning, ignoring error and \
|
||||
logging here: {e}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"{room_id} is not a valid room ID, please fix the list and try again: {e}"
|
||||
)));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for room_id in room_ids {
|
||||
if services().rooms.metadata.ban_room(room_id, true).is_ok() {
|
||||
debug!("Banned {room_id} successfully");
|
||||
room_ban_count += 1;
|
||||
}
|
||||
|
||||
debug!("Making all users leave the room {}", &room_id);
|
||||
if force {
|
||||
for local_user in services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(room_id)
|
||||
.filter_map(|user| {
|
||||
user.ok().filter(|local_user| {
|
||||
local_user.server_name() == services().globals.server_name()
|
||||
// additional wrapped check here is to avoid adding remote users
|
||||
// who are in the admin room to the list of local users (would fail auth check)
|
||||
&& (local_user.server_name()
|
||||
== services().globals.server_name()
|
||||
&& services()
|
||||
.users
|
||||
.is_admin(local_user)
|
||||
.unwrap_or(true)) // since this is a
|
||||
// force operation,
|
||||
// assume user is
|
||||
// an admin if
|
||||
// somehow this
|
||||
// fails
|
||||
})
|
||||
})
|
||||
.collect::<Vec<OwnedUserId>>()
|
||||
{
|
||||
debug!(
|
||||
"Attempting leave for user {} in room {} (forced, ignoring all errors, evicting \
|
||||
admins too)",
|
||||
&local_user, room_id
|
||||
);
|
||||
_ = leave_room(&local_user, room_id, None).await;
|
||||
}
|
||||
} else {
|
||||
for local_user in services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(room_id)
|
||||
.filter_map(|user| {
|
||||
user.ok().filter(|local_user| {
|
||||
local_user.server_name() == services().globals.server_name()
|
||||
// additional wrapped check here is to avoid adding remote users
|
||||
// who are in the admin room to the list of local users (would fail auth check)
|
||||
&& (local_user.server_name()
|
||||
== services().globals.server_name()
|
||||
&& !services()
|
||||
.users
|
||||
.is_admin(local_user)
|
||||
.unwrap_or(false))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<OwnedUserId>>()
|
||||
{
|
||||
debug!("Attempting leave for user {} in room {}", &local_user, &room_id);
|
||||
if let Err(e) = leave_room(&local_user, room_id, None).await {
|
||||
error!(
|
||||
"Error attempting to make local user {} leave room {} during bulk room banning: {}",
|
||||
&local_user, &room_id, e
|
||||
);
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Error attempting to make local user {} leave room {} during room banning (room \
|
||||
is still banned but not removing any more users and not banning any more rooms): \
|
||||
{}\nIf you would like to ignore errors, use --force",
|
||||
&local_user, &room_id, e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if disable_federation {
|
||||
services().rooms.metadata.disable_room(room_id, true)?;
|
||||
}
|
||||
}
|
||||
|
||||
if disable_federation {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Finished bulk room ban, banned {} total rooms, evicted all users, and disabled incoming \
|
||||
federation with the room.",
|
||||
room_ban_count
|
||||
)));
|
||||
}
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Finished bulk room ban, banned {} total rooms and evicted all users.",
|
||||
room_ban_count
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
))
|
||||
},
|
||||
RoomModerationCommand::UnbanRoom {
|
||||
room,
|
||||
enable_federation,
|
||||
} => {
|
||||
let room_id = if room.is_room_id() {
|
||||
let room_id = match RoomId::parse(&room) {
|
||||
Ok(room_id) => room_id,
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to parse room ID {room}. Please note that this requires a full room ID \
|
||||
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
|
||||
)))
|
||||
},
|
||||
};
|
||||
|
||||
debug!("Room specified is a room ID, unbanning room ID");
|
||||
|
||||
services().rooms.metadata.ban_room(&room_id, false)?;
|
||||
|
||||
room_id
|
||||
} else if room.is_room_alias_id() {
|
||||
let room_alias = match RoomAliasId::parse(&room) {
|
||||
Ok(room_alias) => room_alias,
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to parse room ID {room}. Please note that this requires a full room ID \
|
||||
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
|
||||
)))
|
||||
},
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Room specified is not a room ID, attempting to resolve room alias to a room ID locally, if not \
|
||||
using get_alias_helper to fetch room ID remotely"
|
||||
);
|
||||
|
||||
let room_id = match services().rooms.alias.resolve_local_alias(&room_alias)? {
|
||||
Some(room_id) => room_id,
|
||||
None => {
|
||||
debug!(
|
||||
"We don't have this room alias to a room ID locally, attempting to fetch room ID over \
|
||||
federation"
|
||||
);
|
||||
|
||||
match get_alias_helper(room_alias).await {
|
||||
Ok(response) => {
|
||||
debug!("Got federation response fetching room ID for room {room}: {:?}", response);
|
||||
response.room_id
|
||||
},
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to resolve room alias {room} to a room ID: {e}"
|
||||
)));
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
services().rooms.metadata.ban_room(&room_id, false)?;
|
||||
|
||||
room_id
|
||||
} else {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Room specified is not a room ID or room alias. Please note that this requires a full room ID \
|
||||
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`)",
|
||||
));
|
||||
};
|
||||
|
||||
if enable_federation {
|
||||
services().rooms.metadata.disable_room(&room_id, false)?;
|
||||
return Ok(RoomMessageEventContent::text_plain("Room unbanned."));
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Room unbanned, you may need to re-enable federation with the room using enable-room if this is a \
|
||||
remote room to make it fully functional.",
|
||||
))
|
||||
},
|
||||
RoomModerationCommand::ListBannedRooms => {
|
||||
let rooms = services().rooms.metadata.list_banned_rooms().collect::<Result<Vec<_>, _>>();
|
||||
|
||||
match rooms {
|
||||
Ok(room_ids) => {
|
||||
// TODO: add room name from our state cache if available, default to the room ID
|
||||
// as the room name if we dont have it TODO: do same if we have a room alias for
|
||||
// this
|
||||
let plain_list = room_ids.iter().fold(String::new(), |mut output, room_id| {
|
||||
writeln!(output, "- `{}`", room_id).unwrap();
|
||||
output
|
||||
});
|
||||
|
||||
let html_list = room_ids.iter().fold(String::new(), |mut output, room_id| {
|
||||
writeln!(output, "<li><code>{}</code></li>", escape_html(room_id.as_ref())).unwrap();
|
||||
output
|
||||
});
|
||||
|
||||
let plain = format!("Rooms:\n{}", plain_list);
|
||||
let html = format!("Rooms:\n<ul>{}</ul>", html_list);
|
||||
Ok(RoomMessageEventContent::text_html(plain, html))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to list banned rooms: {}", e);
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Unable to list room aliases: {}",
|
||||
e
|
||||
)))
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
106
src/service/admin/server.rs
Normal file
106
src/service/admin/server.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use clap::Subcommand;
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use crate::{services, Result};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum ServerCommand {
|
||||
/// - Show configuration values
|
||||
ShowConfig,
|
||||
|
||||
/// - 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,
|
||||
},
|
||||
|
||||
/// - Performs an online backup of the database (only available for RocksDB
|
||||
/// at the moment)
|
||||
BackupDatabase,
|
||||
|
||||
/// - List database backups
|
||||
ListBackups,
|
||||
|
||||
/// - List database files
|
||||
ListDatabaseFiles,
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: ServerCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
match command {
|
||||
ServerCommand::ShowConfig => {
|
||||
// Construct and send the response
|
||||
Ok(RoomMessageEventContent::text_plain(format!("{}", services().globals.config)))
|
||||
},
|
||||
ServerCommand::MemoryUsage => {
|
||||
let response1 = services().memory_usage().await;
|
||||
let response2 = services().globals.db.memory_usage();
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Services:\n{response1}\n\nDatabase:\n{response2}"
|
||||
)))
|
||||
},
|
||||
ServerCommand::ClearDatabaseCaches {
|
||||
amount,
|
||||
} => {
|
||||
services().globals.db.clear_caches(amount);
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain("Done."))
|
||||
},
|
||||
ServerCommand::ClearServiceCaches {
|
||||
amount,
|
||||
} => {
|
||||
services().clear_caches(amount).await;
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain("Done."))
|
||||
},
|
||||
ServerCommand::ListBackups => {
|
||||
let result = services().globals.db.backup_list()?;
|
||||
|
||||
if result.is_empty() {
|
||||
Ok(RoomMessageEventContent::text_plain("No backups found."))
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain(result))
|
||||
}
|
||||
},
|
||||
ServerCommand::BackupDatabase => {
|
||||
if !cfg!(feature = "rocksdb") {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Only RocksDB supports online backups in conduwuit.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut result = tokio::task::spawn_blocking(move || match services().globals.db.backup() {
|
||||
Ok(()) => String::new(),
|
||||
Err(e) => (*e).to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if result.is_empty() {
|
||||
result = services().globals.db.backup_list()?;
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(&result))
|
||||
},
|
||||
ServerCommand::ListDatabaseFiles => {
|
||||
if !cfg!(feature = "rocksdb") {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Only RocksDB supports listing files in conduwuit.",
|
||||
));
|
||||
}
|
||||
|
||||
let result = services().globals.db.file_list()?;
|
||||
Ok(RoomMessageEventContent::notice_html(String::new(), result))
|
||||
},
|
||||
}
|
||||
}
|
361
src/service/admin/user.rs
Normal file
361
src/service/admin/user.rs
Normal file
|
@ -0,0 +1,361 @@
|
|||
use std::{fmt::Write as _, sync::Arc};
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, UserId};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{
|
||||
api::client_server::{join_room_by_id_helper, leave_all_rooms, AUTO_GEN_PASSWORD_LENGTH},
|
||||
service::admin::{escape_html, get_room_info},
|
||||
services, utils, Result,
|
||||
};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum UserCommand {
|
||||
/// - Create a new user
|
||||
Create {
|
||||
/// Username of the new user
|
||||
username: String,
|
||||
/// Password of the new user, if unspecified one is generated
|
||||
password: Option<String>,
|
||||
},
|
||||
|
||||
/// - Reset user password
|
||||
ResetPassword {
|
||||
/// Username of the user for whom the password should be reset
|
||||
username: String,
|
||||
},
|
||||
|
||||
/// - Deactivate a user
|
||||
///
|
||||
/// User will not be removed from all rooms by default.
|
||||
/// Use --leave-rooms to force the user to leave all rooms
|
||||
Deactivate {
|
||||
#[arg(short, long)]
|
||||
leave_rooms: bool,
|
||||
user_id: Box<UserId>,
|
||||
},
|
||||
|
||||
/// - 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.
|
||||
///
|
||||
/// This command needs a newline separated list of users provided in a
|
||||
/// Markdown code block below the command.
|
||||
DeactivateAll {
|
||||
#[arg(short, long)]
|
||||
/// Remove users from their joined rooms
|
||||
leave_rooms: bool,
|
||||
#[arg(short, long)]
|
||||
/// Also deactivate admin accounts
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// - List local users in the database
|
||||
List,
|
||||
|
||||
/// - Lists all the rooms (local and remote) that the specified user is
|
||||
/// joined in
|
||||
ListJoinedRooms {
|
||||
user_id: Box<UserId>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: UserCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
match command {
|
||||
UserCommand::List => match services().users.list_local_users() {
|
||||
Ok(users) => {
|
||||
let mut msg = format!("Found {} local user account(s):\n", users.len());
|
||||
msg += &users.join("\n");
|
||||
Ok(RoomMessageEventContent::text_plain(&msg))
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(e.to_string())),
|
||||
},
|
||||
UserCommand::Create {
|
||||
username,
|
||||
password,
|
||||
} => {
|
||||
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
|
||||
// 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}"
|
||||
)))
|
||||
},
|
||||
};
|
||||
if user_id.is_historical() {
|
||||
return Ok(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!("Userid {user_id} already exists")));
|
||||
}
|
||||
// Create user
|
||||
services().users.create(&user_id, Some(password.as_str()))?;
|
||||
|
||||
// Default to pretty displayname
|
||||
let mut displayname = user_id.localpart().to_owned();
|
||||
|
||||
// If `new_user_displayname_suffix` is set, registration will push whatever
|
||||
// content is set to the user's display name with a space before it
|
||||
if !services().globals.new_user_displayname_suffix().is_empty() {
|
||||
displayname.push_str(&(" ".to_owned() + services().globals.new_user_displayname_suffix()));
|
||||
}
|
||||
|
||||
services().users.set_displayname(&user_id, Some(displayname)).await?;
|
||||
|
||||
// Initial account data
|
||||
services().account_data.update(
|
||||
None,
|
||||
&user_id,
|
||||
ruma::events::GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||
content: ruma::events::push_rules::PushRulesEventContent {
|
||||
global: ruma::push::Ruleset::server_default(&user_id),
|
||||
},
|
||||
})
|
||||
.expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
if !services().globals.config.auto_join_rooms.is_empty() {
|
||||
for room in &services().globals.config.auto_join_rooms {
|
||||
if !services().rooms.state_cache.server_in_room(services().globals.server_name(), room)? {
|
||||
warn!("Skipping room {room} to automatically join as we have never joined before.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_id_server_name) = room.server_name() {
|
||||
match join_room_by_id_helper(
|
||||
Some(&user_id),
|
||||
room,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[room_id_server_name.to_owned(), services().globals.server_name().to_owned()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
},
|
||||
Err(e) => {
|
||||
// don't return this error so we don't fail registrations
|
||||
error!("Failed to automatically join room {room} for user {user_id}: {e}");
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we dont add a device since we're not the user, just the creator
|
||||
|
||||
// Inhibit login does not work for guests
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Created user with user_id: {user_id} and password: `{password}`"
|
||||
)))
|
||||
},
|
||||
UserCommand::Deactivate {
|
||||
leave_rooms,
|
||||
user_id,
|
||||
} => {
|
||||
let user_id = Arc::<UserId>::from(user_id);
|
||||
|
||||
// check if user belongs to our server
|
||||
if user_id.server_name() != services().globals.server_name() {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} does not belong to our server."
|
||||
)));
|
||||
}
|
||||
|
||||
if services().users.exists(&user_id)? {
|
||||
RoomMessageEventContent::text_plain(format!("Making {user_id} leave all rooms before deactivation..."));
|
||||
|
||||
services().users.deactivate_account(&user_id)?;
|
||||
|
||||
if leave_rooms {
|
||||
leave_all_rooms(&user_id).await?;
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} has been deactivated"
|
||||
)))
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} doesn't exist on this server"
|
||||
)))
|
||||
}
|
||||
},
|
||||
UserCommand::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}"
|
||||
)))
|
||||
},
|
||||
};
|
||||
|
||||
// check if user belongs to our server
|
||||
if user_id.server_name() != services().globals.server_name() {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} does not belong to our server."
|
||||
)));
|
||||
}
|
||||
|
||||
// Check if the specified user is valid
|
||||
if !services().users.exists(&user_id)?
|
||||
|| user_id
|
||||
== UserId::parse_with_server_name("conduit", services().globals.server_name())
|
||||
.expect("conduit user exists")
|
||||
{
|
||||
return Ok(RoomMessageEventContent::text_plain("The specified user does not exist!"));
|
||||
}
|
||||
|
||||
let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH);
|
||||
|
||||
match services().users.set_password(&user_id, Some(new_password.as_str())) {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Successfully reset the password for user {user_id}: `{new_password}`"
|
||||
))),
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Couldn't reset the password for user {user_id}: {e}"
|
||||
))),
|
||||
}
|
||||
},
|
||||
UserCommand::DeactivateAll {
|
||||
leave_rooms,
|
||||
force,
|
||||
} => {
|
||||
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
|
||||
let usernames = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
|
||||
|
||||
let mut user_ids: Vec<&UserId> = Vec::new();
|
||||
|
||||
for &username in &usernames {
|
||||
match <&UserId>::try_from(username) {
|
||||
Ok(user_id) => user_ids.push(user_id),
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"{username} is not a valid username: {e}"
|
||||
)))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let mut deactivation_count = 0;
|
||||
let mut admins = Vec::new();
|
||||
|
||||
if !force {
|
||||
user_ids.retain(|&user_id| match services().users.is_admin(user_id) {
|
||||
Ok(is_admin) => {
|
||||
if is_admin {
|
||||
admins.push(user_id.localpart());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
},
|
||||
Err(_) => false,
|
||||
});
|
||||
}
|
||||
|
||||
for &user_id in &user_ids {
|
||||
// check if user belongs to our server and skips over non-local users
|
||||
if user_id.server_name() != services().globals.server_name() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if services().users.deactivate_account(user_id).is_ok() {
|
||||
deactivation_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if leave_rooms {
|
||||
for &user_id in &user_ids {
|
||||
_ = leave_all_rooms(user_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
if admins.is_empty() {
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Deactivated {deactivation_count} accounts."
|
||||
)))
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Deactivated {} accounts.\nSkipped admin accounts: {:?}. Use --force to deactivate admin \
|
||||
accounts",
|
||||
deactivation_count,
|
||||
admins.join(", ")
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
))
|
||||
}
|
||||
},
|
||||
UserCommand::ListJoinedRooms {
|
||||
user_id,
|
||||
} => {
|
||||
if user_id.server_name() != services().globals.server_name() {
|
||||
return Ok(RoomMessageEventContent::text_plain("User does not belong to our server."));
|
||||
}
|
||||
|
||||
let mut rooms = vec![]; // room ID, members joined, room name
|
||||
|
||||
for room_id in services().rooms.state_cache.rooms_joined(&user_id) {
|
||||
let room_id = room_id?;
|
||||
rooms.push(get_room_info(&room_id));
|
||||
}
|
||||
|
||||
if rooms.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain("User is not in any rooms."));
|
||||
}
|
||||
|
||||
rooms.sort_by_key(|r| r.1);
|
||||
rooms.reverse();
|
||||
|
||||
let output_plain = format!(
|
||||
"Rooms {user_id} Joined:\n{}",
|
||||
rooms
|
||||
.iter()
|
||||
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
let output_html = format!(
|
||||
"<table><caption>Rooms {user_id} \
|
||||
Joined</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
|
||||
rooms.iter().fold(String::new(), |mut output, (id, members, name)| {
|
||||
writeln!(
|
||||
output,
|
||||
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
|
||||
escape_html(id.as_ref()),
|
||||
members,
|
||||
escape_html(name)
|
||||
)
|
||||
.unwrap();
|
||||
output
|
||||
})
|
||||
);
|
||||
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue