Hot-Reloading Refactor
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
parent
ae1a4fd283
commit
6c1434c165
212 changed files with 5679 additions and 4206 deletions
|
@ -1,66 +0,0 @@
|
|||
use ruma::{api::appservice::Registration, events::room::message::RoomMessageEventContent};
|
||||
|
||||
use crate::{service::admin::escape_html, services, Result};
|
||||
|
||||
pub(crate) async fn register(body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
|
||||
let appservice_config = body[1..body.len().checked_sub(1).unwrap()].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.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn unregister(_body: Vec<&str>, appservice_identifier: String) -> Result<RoomMessageEventContent> {
|
||||
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}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn show(_body: Vec<&str>, appservice_identifier: String) -> Result<RoomMessageEventContent> {
|
||||
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.")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn list(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
let appservices = services().appservice.iter_ids().await;
|
||||
let output = format!("Appservices ({}): {}", appservices.len(), appservices.join(", "));
|
||||
Ok(RoomMessageEventContent::text_plain(output))
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
use clap::Subcommand;
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use self::appservice_command::{list, register, show, unregister};
|
||||
use crate::Result;
|
||||
|
||||
pub(crate) mod appservice_command;
|
||||
|
||||
#[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> {
|
||||
Ok(match command {
|
||||
AppserviceCommand::Register => register(body).await?,
|
||||
AppserviceCommand::Unregister {
|
||||
appservice_identifier,
|
||||
} => unregister(body, appservice_identifier).await?,
|
||||
AppserviceCommand::Show {
|
||||
appservice_identifier,
|
||||
} => show(body, appservice_identifier).await?,
|
||||
AppserviceCommand::List => list(body).await?,
|
||||
})
|
||||
}
|
|
@ -1,468 +0,0 @@
|
|||
use std::{collections::BTreeMap, sync::Arc, time::Instant};
|
||||
|
||||
use ruma::{
|
||||
api::client::error::ErrorKind, events::room::message::RoomMessageEventContent, CanonicalJsonObject, EventId,
|
||||
RoomId, RoomVersionId, ServerName,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::{
|
||||
api::server_server::parse_incoming_pdu, service::sending::send::resolve_actual_dest, services, utils::HtmlEscape,
|
||||
Error, PduEvent, Result,
|
||||
};
|
||||
|
||||
pub(crate) async fn get_auth_chain(_body: Vec<&str>, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
|
||||
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
|
||||
.event_ids_iter(room_id, vec![event_id])
|
||||
.await?
|
||||
.count();
|
||||
let elapsed = start.elapsed();
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Loaded auth chain with length {count} in {elapsed:?}"
|
||||
)))
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain("Event not found."))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn parse_pdu(body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
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) => Ok(RoomMessageEventContent::text_plain(format!("EventId: {event_id:?}\n{pdu:#?}"))),
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"EventId: {event_id:?}\nCould not parse event: {e}"
|
||||
))),
|
||||
}
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Could not parse PDU JSON: {e:?}"))),
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Invalid json in command body: {e}"
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain("Expected code block in command body."))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_pdu(_body: Vec<&str>, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
|
||||
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");
|
||||
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 => Ok(RoomMessageEventContent::text_plain("PDU not found locally.")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_remote_pdu_list(
|
||||
body: Vec<&str>, server: Box<ServerName>, force: bool,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
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.",
|
||||
));
|
||||
}
|
||||
|
||||
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
|
||||
let list = body
|
||||
.clone()
|
||||
.drain(1..body.len().checked_sub(1).unwrap())
|
||||
.filter_map(|pdu| EventId::parse(pdu).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for pdu in list {
|
||||
if force {
|
||||
_ = get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await;
|
||||
} else {
|
||||
get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain("Fetched list of remote PDUs."));
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_remote_pdu(
|
||||
_body: Vec<&str>, event_id: Box<EventId>, server: Box<ServerName>,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
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.",
|
||||
));
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
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(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Remote server did not have PDU or failed sending request to remote server: {e}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_room_state(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
|
||||
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| {
|
||||
warn!("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",
|
||||
)
|
||||
})?;
|
||||
|
||||
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)
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn ping(_body: Vec<&str>, server: Box<ServerName>) -> Result<RoomMessageEventContent> {
|
||||
if server == services().globals.server_name() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Not allowed to send federation requests to ourselves.",
|
||||
));
|
||||
}
|
||||
|
||||
let timer = tokio::time::Instant::now();
|
||||
|
||||
match services()
|
||||
.sending
|
||||
.send_federation_request(&server, ruma::api::federation::discovery::get_server_version::v1::Request {})
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let ping_time = timer.elapsed();
|
||||
|
||||
let json_text_res = serde_json::to_string_pretty(&response.server);
|
||||
|
||||
if let Ok(json) = json_text_res {
|
||||
return Ok(RoomMessageEventContent::text_html(
|
||||
format!("Got response which took {ping_time:?} time:\n```json\n{json}\n```"),
|
||||
format!(
|
||||
"<p>Got response which took {ping_time:?} time:</p>\n<pre><code \
|
||||
class=\"language-json\">{}\n</code></pre>\n",
|
||||
HtmlEscape(&json)
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Got non-JSON response which took {ping_time:?} time:\n{0:?}",
|
||||
response
|
||||
)))
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Failed sending federation request to specified server from ping debug command: {e}");
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed sending federation request to specified server:\n\n{e}",
|
||||
)))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn force_device_list_updates(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
// Force E2EE device list updates for all users
|
||||
for user_id in services().users.iter().filter_map(Result::ok) {
|
||||
services().users.mark_device_key_update(&user_id)?;
|
||||
}
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Marked all devices for all users as having new keys to update",
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn change_log_level(
|
||||
_body: Vec<&str>, filter: Option<String>, reset: bool,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
if reset {
|
||||
let old_filter_layer = match EnvFilter::try_new(&services().globals.config.log) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Log level from config appears to be invalid now: {e}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
|
||||
match services()
|
||||
.globals
|
||||
.tracing_reload_handle
|
||||
.reload(&old_filter_layer)
|
||||
{
|
||||
Ok(()) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Successfully changed log level back to config value {}",
|
||||
services().globals.config.log
|
||||
)));
|
||||
},
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to modify and reload the global tracing log level: {e}"
|
||||
)));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(filter) = filter {
|
||||
let new_filter_layer = match EnvFilter::try_new(filter) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Invalid log level filter specified: {e}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
|
||||
match services()
|
||||
.globals
|
||||
.tracing_reload_handle
|
||||
.reload(&new_filter_layer)
|
||||
{
|
||||
Ok(()) => {
|
||||
return Ok(RoomMessageEventContent::text_plain("Successfully changed log level"));
|
||||
},
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to modify and reload the global tracing log level: {e}"
|
||||
)));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain("No log level was specified."))
|
||||
}
|
||||
|
||||
pub(crate) async fn sign_json(body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
|
||||
let string = body[1..body.len().checked_sub(1).unwrap()].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.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn verify_json(body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
|
||||
let string = body[1..body.len().checked_sub(1).unwrap()].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.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_true_destination(
|
||||
_body: Vec<&str>, server_name: Box<ServerName>, no_cache: bool,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
if !services().globals.config.allow_federation {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Federation is disabled on this homeserver.",
|
||||
));
|
||||
}
|
||||
|
||||
if server_name == services().globals.config.server_name {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Not allowed to send federation requests to ourselves. Please use `get-pdu` for fetching local PDUs.",
|
||||
));
|
||||
}
|
||||
|
||||
let (actual_dest, hostname_uri) = resolve_actual_dest(&server_name, no_cache, true).await?;
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Actual destination: {actual_dest:?} | Hostname URI: {hostname_uri}"
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn memory_stats() -> RoomMessageEventContent {
|
||||
let html_body = crate::alloc::memory_stats();
|
||||
|
||||
if html_body.is_empty() {
|
||||
return RoomMessageEventContent::text_plain("malloc stats are not supported on your compiled malloc.");
|
||||
}
|
||||
|
||||
RoomMessageEventContent::text_html(
|
||||
"This command's output can only be viewed by clients that render HTML.".to_owned(),
|
||||
html_body,
|
||||
)
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, EventId, RoomId, ServerName};
|
||||
|
||||
use self::debug_commands::{
|
||||
change_log_level, force_device_list_updates, get_auth_chain, get_pdu, get_remote_pdu, get_remote_pdu_list,
|
||||
get_room_state, memory_stats, parse_pdu, ping, resolve_true_destination, sign_json, verify_json,
|
||||
};
|
||||
use crate::Result;
|
||||
|
||||
pub(crate) mod debug_commands;
|
||||
|
||||
#[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>,
|
||||
},
|
||||
|
||||
/// Same as `get-remote-pdu` but accepts a codeblock newline delimited list
|
||||
/// of PDUs and a single server to fetch from
|
||||
GetRemotePduList {
|
||||
/// Argument for us to attempt to fetch all the events from the
|
||||
/// specified remote server.
|
||||
server: Box<ServerName>,
|
||||
|
||||
/// If set, ignores errors, else stops at the first error/failure.
|
||||
#[arg(short, long)]
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// - 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>,
|
||||
},
|
||||
|
||||
/// - Sends a federation request to the remote server's
|
||||
/// `/_matrix/federation/v1/version` endpoint and measures the latency it
|
||||
/// took for the server to respond
|
||||
Ping {
|
||||
server: Box<ServerName>,
|
||||
},
|
||||
|
||||
/// - Forces device lists for all local and remote users to be updated (as
|
||||
/// having new keys available)
|
||||
ForceDeviceListUpdates,
|
||||
|
||||
/// - Change tracing log level/filter on the fly
|
||||
///
|
||||
/// This accepts the same format as the `log` config option.
|
||||
ChangeLogLevel {
|
||||
/// Log level/filter
|
||||
filter: Option<String>,
|
||||
|
||||
/// Resets the log level/filter to the one in your config
|
||||
#[arg(short, long)]
|
||||
reset: bool,
|
||||
},
|
||||
|
||||
/// - 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,
|
||||
|
||||
/// - Runs a server name through conduwuit's true destination resolution
|
||||
/// process
|
||||
///
|
||||
/// Useful for debugging well-known issues
|
||||
ResolveTrueDestination {
|
||||
server_name: Box<ServerName>,
|
||||
|
||||
#[arg(short, long)]
|
||||
no_cache: bool,
|
||||
},
|
||||
|
||||
/// - Print extended memory usage
|
||||
MemoryStats,
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: DebugCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
Ok(match command {
|
||||
DebugCommand::GetAuthChain {
|
||||
event_id,
|
||||
} => get_auth_chain(body, event_id).await?,
|
||||
DebugCommand::ParsePdu => parse_pdu(body).await?,
|
||||
DebugCommand::GetPdu {
|
||||
event_id,
|
||||
} => get_pdu(body, event_id).await?,
|
||||
DebugCommand::GetRemotePdu {
|
||||
event_id,
|
||||
server,
|
||||
} => get_remote_pdu(body, event_id, server).await?,
|
||||
DebugCommand::GetRoomState {
|
||||
room_id,
|
||||
} => get_room_state(body, room_id).await?,
|
||||
DebugCommand::Ping {
|
||||
server,
|
||||
} => ping(body, server).await?,
|
||||
DebugCommand::ForceDeviceListUpdates => force_device_list_updates(body).await?,
|
||||
DebugCommand::ChangeLogLevel {
|
||||
filter,
|
||||
reset,
|
||||
} => change_log_level(body, filter, reset).await?,
|
||||
DebugCommand::SignJson => sign_json(body).await?,
|
||||
DebugCommand::VerifyJson => verify_json(body).await?,
|
||||
DebugCommand::GetRemotePduList {
|
||||
server,
|
||||
force,
|
||||
} => get_remote_pdu_list(body, server, force).await?,
|
||||
DebugCommand::ResolveTrueDestination {
|
||||
server_name,
|
||||
no_cache,
|
||||
} => resolve_true_destination(body, server_name, no_cache).await?,
|
||||
DebugCommand::MemoryStats => memory_stats(),
|
||||
})
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId, ServerName, UserId};
|
||||
|
||||
use crate::{
|
||||
service::admin::{escape_html, get_room_info},
|
||||
services,
|
||||
utils::HtmlEscape,
|
||||
Result,
|
||||
};
|
||||
|
||||
pub(crate) async fn disable_room(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
|
||||
services().rooms.metadata.disable_room(&room_id, true)?;
|
||||
Ok(RoomMessageEventContent::text_plain("Room disabled."))
|
||||
}
|
||||
|
||||
pub(crate) async fn enable_room(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
|
||||
services().rooms.metadata.disable_room(&room_id, false)?;
|
||||
Ok(RoomMessageEventContent::text_plain("Room enabled."))
|
||||
}
|
||||
|
||||
pub(crate) async fn incoming_federeation(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
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))
|
||||
}
|
||||
|
||||
pub(crate) async fn fetch_support_well_known(
|
||||
_body: Vec<&str>, server_name: Box<ServerName>,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
let response = services()
|
||||
.globals
|
||||
.client
|
||||
.default
|
||||
.get(format!("https://{server_name}/.well-known/matrix/support"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let text = response.text().await?;
|
||||
|
||||
if text.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain("Response text/body is empty."));
|
||||
}
|
||||
|
||||
if text.len() > 1500 {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Response text/body is over 1500 characters, assuming no support well-known.",
|
||||
));
|
||||
}
|
||||
|
||||
let json: serde_json::Value = match serde_json::from_str(&text) {
|
||||
Ok(json) => json,
|
||||
Err(_) => {
|
||||
return Ok(RoomMessageEventContent::text_plain("Response text/body is not valid JSON."));
|
||||
},
|
||||
};
|
||||
|
||||
let pretty_json: String = match serde_json::to_string_pretty(&json) {
|
||||
Ok(json) => json,
|
||||
Err(_) => {
|
||||
return Ok(RoomMessageEventContent::text_plain("Response text/body is not valid JSON."));
|
||||
},
|
||||
};
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Got JSON response:\n\n```json\n{pretty_json}\n```"),
|
||||
format!(
|
||||
"<p>Got JSON response:</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
|
||||
HtmlEscape(&pretty_json)
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn remote_user_in_rooms(_body: Vec<&str>, user_id: Box<UserId>) -> Result<RoomMessageEventContent> {
|
||||
if user_id.server_name() == services().globals.config.server_name {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"User belongs to our server, please use `list-joined-rooms` user admin command instead.",
|
||||
));
|
||||
}
|
||||
|
||||
if !services().users.exists(&user_id)? {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Remote user does not exist in our database.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut rooms: Vec<(OwnedRoomId, u64, String)> = services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
.filter_map(Result::ok)
|
||||
.map(|room_id| get_room_info(&room_id))
|
||||
.collect();
|
||||
|
||||
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} shares with us:\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} shares with \
|
||||
us</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))
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomId, ServerName, UserId};
|
||||
|
||||
use self::federation_commands::{
|
||||
disable_room, enable_room, fetch_support_well_known, incoming_federeation, remote_user_in_rooms,
|
||||
};
|
||||
use crate::Result;
|
||||
|
||||
pub(crate) mod federation_commands;
|
||||
|
||||
#[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>,
|
||||
},
|
||||
|
||||
/// - Fetch `/.well-known/matrix/support` from the specified server
|
||||
///
|
||||
/// Despite the name, this is not a federation endpoint and does not go
|
||||
/// through the federation / server resolution process as per-spec this is
|
||||
/// supposed to be served at the server_name.
|
||||
///
|
||||
/// Respecting homeservers put this file here for listing administration,
|
||||
/// moderation, and security inquiries. This command provides a way to
|
||||
/// easily fetch that information.
|
||||
FetchSupportWellKnown {
|
||||
server_name: Box<ServerName>,
|
||||
},
|
||||
|
||||
/// - Lists all the rooms we share/track with the specified *remote* user
|
||||
RemoteUserInRooms {
|
||||
user_id: Box<UserId>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: FederationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
Ok(match command {
|
||||
FederationCommand::DisableRoom {
|
||||
room_id,
|
||||
} => disable_room(body, room_id).await?,
|
||||
FederationCommand::EnableRoom {
|
||||
room_id,
|
||||
} => enable_room(body, room_id).await?,
|
||||
FederationCommand::IncomingFederation => incoming_federeation(body).await?,
|
||||
FederationCommand::FetchSupportWellKnown {
|
||||
server_name,
|
||||
} => fetch_support_well_known(body, server_name).await?,
|
||||
FederationCommand::RemoteUserInRooms {
|
||||
user_id,
|
||||
} => remote_user_in_rooms(body, user_id).await?,
|
||||
})
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use crate::{services, Result};
|
||||
|
||||
/// Uses the iterator in `src/database/key_value/users.rs` to iterator over
|
||||
/// every user in our database (remote and local). Reports total count, any
|
||||
/// errors if there were any, etc
|
||||
pub(crate) async fn check_all_users(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().users.db.iter();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
let users = results.collect::<Vec<_>>();
|
||||
|
||||
let total = users.len();
|
||||
let err_count = users.iter().filter(|user| user.is_err()).count();
|
||||
let ok_count = users.iter().filter(|user| user.is_ok()).count();
|
||||
|
||||
let message = format!(
|
||||
"Database query completed in {query_time:?}:\n\n```\nTotal entries: {:?}\nFailure/Invalid user count: \
|
||||
{:?}\nSuccess/Valid user count: {:?}```",
|
||||
total, err_count, ok_count
|
||||
);
|
||||
|
||||
Ok(RoomMessageEventContent::notice_html(message, String::new()))
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
use clap::Subcommand;
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use self::fsck_commands::check_all_users;
|
||||
use crate::Result;
|
||||
|
||||
pub(crate) mod fsck_commands;
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum FsckCommand {
|
||||
CheckAllUsers,
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: FsckCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
Ok(match command {
|
||||
FsckCommand::CheckAllUsers => check_all_users(body).await?,
|
||||
})
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
use ruma::{events::room::message::RoomMessageEventContent, EventId};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{service::admin::MxcUri, services, Result};
|
||||
|
||||
pub(crate) async fn delete(
|
||||
_body: Vec<&str>, mxc: Option<Box<MxcUri>>, event_id: Option<Box<EventId>>,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
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 {
|
||||
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 {url} to list of MXCs to delete");
|
||||
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 {thumbnail_url} to list of MXCs to delete");
|
||||
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 {url} to list of MXCs to delete");
|
||||
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.",
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_list(body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
|
||||
let mxc_list = body
|
||||
.clone()
|
||||
.drain(1..body.len().checked_sub(1).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut mxc_deletion_count: usize = 0;
|
||||
|
||||
for mxc in mxc_list {
|
||||
debug!("Deleting MXC {mxc} in bulk");
|
||||
services().media.delete(mxc.to_owned()).await?;
|
||||
mxc_deletion_count = mxc_deletion_count
|
||||
.checked_add(1)
|
||||
.expect("mxc_deletion_count should not get this high");
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our database and the filesystem.",
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_past_remote_media(_body: Vec<&str>, duration: String) -> Result<RoomMessageEventContent> {
|
||||
let deleted_count = services()
|
||||
.media
|
||||
.delete_all_remote_media_at_after_time(duration)
|
||||
.await?;
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Deleted {deleted_count} total files.",
|
||||
)))
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, EventId};
|
||||
|
||||
use self::media_commands::{delete, delete_list, delete_past_remote_media};
|
||||
use crate::{service::admin::MxcUri, Result};
|
||||
|
||||
pub(crate) mod media_commands;
|
||||
|
||||
#[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> {
|
||||
Ok(match command {
|
||||
MediaCommand::Delete {
|
||||
mxc,
|
||||
event_id,
|
||||
} => delete(body, mxc, event_id).await?,
|
||||
MediaCommand::DeleteList => delete_list(body).await?,
|
||||
MediaCommand::DeletePastRemoteMedia {
|
||||
duration,
|
||||
} => delete_past_remote_media(body, duration).await?,
|
||||
})
|
||||
}
|
|
@ -1,829 +0,0 @@
|
|||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use clap::Parser;
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
api::client::error::ErrorKind,
|
||||
events::{
|
||||
relation::InReplyTo,
|
||||
room::{
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
create::RoomCreateEventContent,
|
||||
guest_access::{GuestAccess, RoomGuestAccessEventContent},
|
||||
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
message::{Relation::Reply, RoomMessageEventContent},
|
||||
name::RoomNameEventContent,
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
TimelineEventType,
|
||||
},
|
||||
EventId, MxcUri, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId, RoomVersionId, ServerName, UserId,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use tokio::sync::{Mutex, MutexGuard};
|
||||
use tracing::{error, warn};
|
||||
|
||||
use self::{fsck::FsckCommand, tester::TesterCommands};
|
||||
use super::pdu::PduBuilder;
|
||||
use crate::{
|
||||
service::admin::{
|
||||
appservice::AppserviceCommand, debug::DebugCommand, federation::FederationCommand, media::MediaCommand,
|
||||
query::QueryCommand, room::RoomCommand, server::ServerCommand, user::UserCommand,
|
||||
},
|
||||
services, Error, Result,
|
||||
};
|
||||
|
||||
pub(crate) mod appservice;
|
||||
pub(crate) mod debug;
|
||||
pub(crate) mod federation;
|
||||
pub(crate) mod fsck;
|
||||
pub(crate) mod media;
|
||||
pub(crate) mod query;
|
||||
pub(crate) mod room;
|
||||
pub(crate) mod server;
|
||||
pub(crate) mod tester;
|
||||
pub(crate) mod user;
|
||||
|
||||
const PAGE_SIZE: usize = 100;
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Parser)]
|
||||
#[command(name = "@conduit:server.name:", version = env!("CARGO_PKG_VERSION"))]
|
||||
enum AdminCommand {
|
||||
#[command(subcommand)]
|
||||
/// - Commands for managing appservices
|
||||
Appservices(AppserviceCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Commands for managing local users
|
||||
Users(UserCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Commands for managing rooms
|
||||
Rooms(RoomCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Commands for managing federation
|
||||
Federation(FederationCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Commands for managing the server
|
||||
Server(ServerCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Commands for managing media
|
||||
Media(MediaCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Commands for debugging things
|
||||
Debug(DebugCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Query all the database getters and iterators
|
||||
Query(QueryCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Query all the database getters and iterators
|
||||
Fsck(FsckCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
Tester(TesterCommands),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AdminRoomEvent {
|
||||
ProcessMessage(String, Arc<EventId>),
|
||||
SendMessage(RoomMessageEventContent),
|
||||
}
|
||||
|
||||
pub(crate) struct Service {
|
||||
pub(crate) sender: loole::Sender<AdminRoomEvent>,
|
||||
receiver: Mutex<loole::Receiver<AdminRoomEvent>>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub(crate) fn build() -> Arc<Self> {
|
||||
let (sender, receiver) = loole::unbounded();
|
||||
Arc::new(Self {
|
||||
sender,
|
||||
receiver: Mutex::new(receiver),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn start_handler(self: &Arc<Self>) {
|
||||
let self2 = Arc::clone(self);
|
||||
tokio::spawn(async move {
|
||||
self2
|
||||
.handler()
|
||||
.await
|
||||
.expect("Failed to initialize admin room handler");
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) async fn process_message(&self, room_message: String, event_id: Arc<EventId>) {
|
||||
self.send(AdminRoomEvent::ProcessMessage(room_message, event_id))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn send_message(&self, message_content: RoomMessageEventContent) {
|
||||
self.send(AdminRoomEvent::SendMessage(message_content))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send(&self, message: AdminRoomEvent) {
|
||||
debug_assert!(!self.sender.is_full(), "channel full");
|
||||
debug_assert!(!self.sender.is_closed(), "channel closed");
|
||||
self.sender.send(message).expect("message sent");
|
||||
}
|
||||
|
||||
async fn handler(&self) -> Result<()> {
|
||||
let receiver = self.receiver.lock().await;
|
||||
let Ok(Some(admin_room)) = Self::get_admin_room().await else {
|
||||
return Ok(());
|
||||
};
|
||||
let server_name = services().globals.server_name();
|
||||
let server_user = UserId::parse(format!("@conduit:{server_name}")).expect("server's username is valid");
|
||||
|
||||
loop {
|
||||
debug_assert!(!receiver.is_closed(), "channel closed");
|
||||
tokio::select! {
|
||||
event = receiver.recv_async() => match event {
|
||||
Ok(event) => self.handle_event(event, &admin_room, &server_user).await?,
|
||||
Err(e) => error!("Failed to receive admin room event from channel: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_event(&self, event: AdminRoomEvent, admin_room: &OwnedRoomId, server_user: &UserId) -> Result<()> {
|
||||
let (mut message_content, reply) = match event {
|
||||
AdminRoomEvent::SendMessage(content) => (content, None),
|
||||
AdminRoomEvent::ProcessMessage(room_message, reply_id) => {
|
||||
(self.process_admin_message(room_message).await, Some(reply_id))
|
||||
},
|
||||
};
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.await
|
||||
.entry(admin_room.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
if let Some(reply) = reply {
|
||||
message_content.relates_to = Some(Reply {
|
||||
in_reply_to: InReplyTo {
|
||||
event_id: reply.into(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let response_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,
|
||||
};
|
||||
|
||||
if let Err(e) = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(response_pdu, server_user, admin_room, &state_lock)
|
||||
.await
|
||||
{
|
||||
self.handle_response_error(&e, admin_room, server_user, &state_lock)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_response_error(
|
||||
&self, e: &Error, admin_room: &OwnedRoomId, server_user: &UserId, state_lock: &MutexGuard<'_, ()>,
|
||||
) -> Result<()> {
|
||||
error!("Failed to build and append admin room response PDU: \"{e}\"");
|
||||
let error_room_message = RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to build and append admin room PDU: \"{e}\"\n\nThe original admin command may have finished \
|
||||
successfully, but we could not return the output."
|
||||
));
|
||||
|
||||
let response_pdu = PduBuilder {
|
||||
event_type: TimelineEventType::RoomMessage,
|
||||
content: to_raw_value(&error_room_message).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: None,
|
||||
redacts: None,
|
||||
};
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(response_pdu, server_user, admin_room, state_lock)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 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());
|
||||
let command_line = lines.next().expect("each string has at least one line");
|
||||
let body = lines.collect::<Vec<_>>();
|
||||
|
||||
let admin_command = match self.parse_admin_command(command_line) {
|
||||
Ok(command) => command,
|
||||
Err(error) => {
|
||||
let server_name = services().globals.server_name();
|
||||
let message = error.replace("server.name", server_name.as_str());
|
||||
let html_message = self.usage_to_html(&message, server_name);
|
||||
|
||||
return RoomMessageEventContent::text_html(message, html_message);
|
||||
},
|
||||
};
|
||||
|
||||
match self.process_admin_command(admin_command, body).await {
|
||||
Ok(reply_message) => reply_message,
|
||||
Err(error) => {
|
||||
let markdown_message = format!("Encountered an error while handling the command:\n```\n{error}\n```",);
|
||||
let html_message = format!("Encountered an error while handling the command:\n<pre>\n{error}\n</pre>",);
|
||||
|
||||
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) -> Result<AdminCommand, String> {
|
||||
// Note: argv[0] is `@conduit:servername:`, which is treated as the main command
|
||||
let mut argv = command_line.split_whitespace().collect::<Vec<_>>();
|
||||
|
||||
// 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_argv1;
|
||||
if argv.len() > 1 && argv[1].contains('_') {
|
||||
command_with_dashes_argv1 = argv[1].replace('_', "-");
|
||||
argv[1] = &command_with_dashes_argv1;
|
||||
}
|
||||
|
||||
// Backwards compatibility with `register_appservice`-style commands
|
||||
let command_with_dashes_argv2;
|
||||
if argv.len() > 2 && argv[2].contains('_') {
|
||||
command_with_dashes_argv2 = argv[2].replace('_', "-");
|
||||
argv[2] = &command_with_dashes_argv2;
|
||||
}
|
||||
|
||||
// if the user is using the `query` command (argv[1]), replace the database
|
||||
// function/table calls with underscores to match the codebase
|
||||
let command_with_dashes_argv3;
|
||||
if argv.len() > 3 && argv[1].eq("query") {
|
||||
command_with_dashes_argv3 = argv[3].replace('_', "-");
|
||||
argv[3] = &command_with_dashes_argv3;
|
||||
}
|
||||
|
||||
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::Appservices(command) => appservice::process(command, body).await?,
|
||||
AdminCommand::Media(command) => media::process(command, body).await?,
|
||||
AdminCommand::Users(command) => user::process(command, body).await?,
|
||||
AdminCommand::Rooms(command) => room::process(command, body).await?,
|
||||
AdminCommand::Federation(command) => federation::process(command, body).await?,
|
||||
AdminCommand::Server(command) => server::process(command, body).await?,
|
||||
AdminCommand::Debug(command) => debug::process(command, body).await?,
|
||||
AdminCommand::Query(command) => query::process(command, body).await?,
|
||||
AdminCommand::Fsck(command) => fsck::process(command, body).await?,
|
||||
AdminCommand::Tester(command) => tester::process(command, body).await?,
|
||||
};
|
||||
|
||||
Ok(reply_message_content)
|
||||
}
|
||||
|
||||
// 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}: "));
|
||||
|
||||
// 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
|
||||
let text = escape_html(&text);
|
||||
|
||||
// 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)^ {4}(([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
|
||||
// start with a `#` in the USAGE section.
|
||||
let mut text_lines = text.lines().collect::<Vec<&str>>();
|
||||
let mut command_body = String::new();
|
||||
|
||||
if let Some(line_index) = text_lines.iter().position(|line| *line == "[commandbody]") {
|
||||
text_lines.remove(line_index);
|
||||
|
||||
while text_lines
|
||||
.get(line_index)
|
||||
.is_some_and(|line| line.starts_with('#'))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 {4}(@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
|
||||
let re = Regex::new("(?m)^USAGE:\n {4}(.*?)\n\n").expect("Regex compilation should not fail");
|
||||
re.replace_all(&text, "USAGE:\n<pre>$1[nobr]\n[commandbodyblock]</pre>")
|
||||
.replace("[commandbodyblock]", &command_body)
|
||||
};
|
||||
|
||||
// Add HTML line-breaks
|
||||
|
||||
text.replace("\n\n\n", "\n\n")
|
||||
.replace('\n', "<br>\n")
|
||||
.replace("[nobr]<br>", "")
|
||||
}
|
||||
|
||||
/// 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());
|
||||
|
||||
services().rooms.short.get_or_create_shortroomid(&room_id)?;
|
||||
|
||||
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;
|
||||
|
||||
// Create a user for the server
|
||||
let server_user = UserId::parse_with_server_name("conduit", services().globals.server_name())
|
||||
.expect("@conduit:server_name is valid");
|
||||
|
||||
services().users.create(&server_user, None)?;
|
||||
|
||||
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
|
||||
| RoomVersionId::V10 => RoomCreateEventContent::new_v1(server_user.clone()),
|
||||
RoomVersionId::V11 => RoomCreateEventContent::new_v11(),
|
||||
_ => {
|
||||
warn!("Unexpected or unsupported room version {}", room_version);
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::BadJson,
|
||||
"Unexpected or unsupported room version found",
|
||||
));
|
||||
},
|
||||
};
|
||||
|
||||
content.federate = true;
|
||||
content.predecessor = None;
|
||||
content.room_version = room_version;
|
||||
|
||||
// 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(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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(server_user.to_string()),
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 3. Power levels
|
||||
let mut users = BTreeMap::new();
|
||||
users.insert(server_user.clone(), 100.into());
|
||||
|
||||
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(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 4.3 Guest Access
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomGuestAccess,
|
||||
content: to_raw_value(&RoomGuestAccessEventContent::new(GuestAccess::Forbidden))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 5. Events implied by name and topic
|
||||
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(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
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(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 6. Room alias
|
||||
let alias: OwnedRoomAliasId = format!("#admins:{}", services().globals.server_name())
|
||||
.try_into()
|
||||
.expect("#admins:server_name is a valid alias name");
|
||||
|
||||
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(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
services().rooms.alias.set_alias(&alias, &room_id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the room ID of the admin room
|
||||
///
|
||||
/// Errors are propagated from the database, and will have None if there is
|
||||
/// no admin room
|
||||
pub(crate) async fn get_admin_room() -> Result<Option<OwnedRoomId>> {
|
||||
let admin_room_alias: Box<RoomAliasId> = format!("#admins:{}", services().globals.server_name())
|
||||
.try_into()
|
||||
.expect("#admins:server_name is a valid alias name");
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&admin_room_alias)
|
||||
}
|
||||
|
||||
/// 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) = Self::get_admin_room().await? {
|
||||
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
|
||||
let server_user = UserId::parse_with_server_name("conduit", services().globals.server_name())
|
||||
.expect("@conduit:server_name is valid");
|
||||
|
||||
// Invite and join the real user
|
||||
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,
|
||||
},
|
||||
&server_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,
|
||||
},
|
||||
user_id,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Set power level
|
||||
let mut users = BTreeMap::new();
|
||||
users.insert(server_user.clone(), 100.into());
|
||||
users.insert(user_id.to_owned(), 100.into());
|
||||
|
||||
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(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Send welcome message
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMessage,
|
||||
content: to_raw_value(&RoomMessageEventContent::text_html(
|
||||
format!("## Thank you for trying out conduwuit!\n\nconduwuit is a fork of upstream Conduit which is 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> Git and Documentation: https://github.com/girlbossceo/conduwuit\n> Report issues: https://github.com/girlbossceo/conduwuit/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\nconduwuit room (Ask questions and get notified on updates):\n`/join #conduwuit:puppygock.gay`", services().globals.server_name()),
|
||||
format!("<h2>Thank you for trying out conduwuit!</h2>\n<p>conduwuit is a fork of upstream Conduit which is 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>Git and Documentation: https://github.com/girlbossceo/conduwuit<br>Report issues: https://github.com/girlbossceo/conduwuit/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>conduwuit room (Ask questions and get notified on updates):<br><code>/join #conduwuit:puppygock.gay</code></p>\n", services().globals.server_name()),
|
||||
))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: None,
|
||||
redacts: None,
|
||||
},
|
||||
&server_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_html(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
}
|
||||
|
||||
fn get_room_info(id: &OwnedRoomId) -> (OwnedRoomId, u64, String) {
|
||||
(
|
||||
id.clone(),
|
||||
services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(0),
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_name(id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| id.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
#[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:"));
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use super::AccountData;
|
||||
use crate::{services, Result};
|
||||
|
||||
/// All the getters and iterators from src/database/key_value/account_data.rs
|
||||
pub(crate) async fn account_data(subcommand: AccountData) -> Result<RoomMessageEventContent> {
|
||||
match subcommand {
|
||||
AccountData::ChangesSince {
|
||||
user_id,
|
||||
since,
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services()
|
||||
.account_data
|
||||
.db
|
||||
.changes_since(room_id.as_deref(), &user_id, since)?;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
AccountData::Get {
|
||||
user_id,
|
||||
kind,
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services()
|
||||
.account_data
|
||||
.db
|
||||
.get(room_id.as_deref(), &user_id, kind)?;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use super::Appservice;
|
||||
use crate::{services, Result};
|
||||
|
||||
/// All the getters and iterators from src/database/key_value/appservice.rs
|
||||
pub(crate) async fn appservice(subcommand: Appservice) -> Result<RoomMessageEventContent> {
|
||||
match subcommand {
|
||||
Appservice::GetRegistration {
|
||||
appservice_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services()
|
||||
.appservice
|
||||
.db
|
||||
.get_registration(appservice_id.as_ref());
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
Appservice::All => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().appservice.db.all();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use super::Globals;
|
||||
use crate::{services, Result};
|
||||
|
||||
/// All the getters and iterators from src/database/key_value/globals.rs
|
||||
pub(crate) async fn globals(subcommand: Globals) -> Result<RoomMessageEventContent> {
|
||||
match subcommand {
|
||||
Globals::DatabaseVersion => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().globals.db.database_version();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
Globals::CurrentCount => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().globals.db.current_count();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
Globals::LastCheckForUpdatesId => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().globals.db.last_check_for_updates_id();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
Globals::LoadKeypair => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().globals.db.load_keypair();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
Globals::SigningKeysFor {
|
||||
origin,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().globals.db.signing_keys_for(&origin);
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,223 +0,0 @@
|
|||
pub(crate) mod account_data;
|
||||
pub(crate) mod appservice;
|
||||
pub(crate) mod globals;
|
||||
pub(crate) mod presence;
|
||||
pub(crate) mod room_alias;
|
||||
pub(crate) mod sending;
|
||||
pub(crate) mod users;
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::{
|
||||
events::{room::message::RoomMessageEventContent, RoomAccountDataEventType},
|
||||
RoomAliasId, RoomId, ServerName, UserId,
|
||||
};
|
||||
|
||||
use self::{
|
||||
account_data::account_data, appservice::appservice, globals::globals, presence::presence, room_alias::room_alias,
|
||||
sending::sending, users::users,
|
||||
};
|
||||
use crate::Result;
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
/// Query tables from database
|
||||
pub(crate) enum QueryCommand {
|
||||
/// - account_data.rs iterators and getters
|
||||
#[command(subcommand)]
|
||||
AccountData(AccountData),
|
||||
|
||||
/// - appservice.rs iterators and getters
|
||||
#[command(subcommand)]
|
||||
Appservice(Appservice),
|
||||
|
||||
/// - presence.rs iterators and getters
|
||||
#[command(subcommand)]
|
||||
Presence(Presence),
|
||||
|
||||
/// - rooms/alias.rs iterators and getters
|
||||
#[command(subcommand)]
|
||||
RoomAlias(RoomAlias),
|
||||
|
||||
/// - globals.rs iterators and getters
|
||||
#[command(subcommand)]
|
||||
Globals(Globals),
|
||||
|
||||
/// - sending.rs iterators and getters
|
||||
#[command(subcommand)]
|
||||
Sending(Sending),
|
||||
|
||||
/// - users.rs iterators and getters
|
||||
#[command(subcommand)]
|
||||
Users(Users),
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
/// All the getters and iterators from src/database/key_value/account_data.rs
|
||||
pub(crate) enum AccountData {
|
||||
/// - Returns all changes to the account data that happened after `since`.
|
||||
ChangesSince {
|
||||
/// Full user ID
|
||||
user_id: Box<UserId>,
|
||||
/// UNIX timestamp since (u64)
|
||||
since: u64,
|
||||
/// Optional room ID of the account data
|
||||
room_id: Option<Box<RoomId>>,
|
||||
},
|
||||
|
||||
/// - Searches the account data for a specific kind.
|
||||
Get {
|
||||
/// Full user ID
|
||||
user_id: Box<UserId>,
|
||||
/// Account data event type
|
||||
kind: RoomAccountDataEventType,
|
||||
/// Optional room ID of the account data
|
||||
room_id: Option<Box<RoomId>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
/// All the getters and iterators from src/database/key_value/appservice.rs
|
||||
pub(crate) enum Appservice {
|
||||
/// - Gets the appservice registration info/details from the ID as a string
|
||||
GetRegistration {
|
||||
/// Appservice registration ID
|
||||
appservice_id: Box<str>,
|
||||
},
|
||||
|
||||
/// - Gets all appservice registrations with their ID and registration info
|
||||
All,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
/// All the getters and iterators from src/database/key_value/presence.rs
|
||||
pub(crate) enum Presence {
|
||||
/// - Returns the latest presence event for the given user.
|
||||
GetPresence {
|
||||
/// Full user ID
|
||||
user_id: Box<UserId>,
|
||||
},
|
||||
|
||||
/// - Iterator of the most recent presence updates that happened after the
|
||||
/// event with id `since`.
|
||||
PresenceSince {
|
||||
/// UNIX timestamp since (u64)
|
||||
since: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
/// All the getters and iterators from src/database/key_value/rooms/alias.rs
|
||||
pub(crate) enum RoomAlias {
|
||||
ResolveLocalAlias {
|
||||
/// Full room alias
|
||||
alias: Box<RoomAliasId>,
|
||||
},
|
||||
|
||||
/// - Iterator of all our local room aliases for the room ID
|
||||
LocalAliasesForRoom {
|
||||
/// Full room ID
|
||||
room_id: Box<RoomId>,
|
||||
},
|
||||
|
||||
/// - Iterator of all our local aliases in our database with their room IDs
|
||||
AllLocalAliases,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
/// All the getters and iterators from src/database/key_value/globals.rs
|
||||
pub(crate) enum Globals {
|
||||
DatabaseVersion,
|
||||
|
||||
CurrentCount,
|
||||
|
||||
LastCheckForUpdatesId,
|
||||
|
||||
LoadKeypair,
|
||||
|
||||
/// - This returns an empty `Ok(BTreeMap<..>)` when there are no keys found
|
||||
/// for the server.
|
||||
SigningKeysFor {
|
||||
origin: Box<ServerName>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
/// All the getters and iterators from src/database/key_value/sending.rs
|
||||
pub(crate) enum Sending {
|
||||
/// - Queries database for all `servercurrentevent_data`
|
||||
ActiveRequests,
|
||||
|
||||
/// - Queries database for `servercurrentevent_data` but for a specific
|
||||
/// destination
|
||||
///
|
||||
/// This command takes only *one* format of these arguments:
|
||||
///
|
||||
/// appservice_id
|
||||
/// server_name
|
||||
/// user_id AND push_key
|
||||
///
|
||||
/// See src/service/sending/mod.rs for the definition of the `Destination`
|
||||
/// enum
|
||||
ActiveRequestsFor {
|
||||
#[arg(short, long)]
|
||||
appservice_id: Option<String>,
|
||||
#[arg(short, long)]
|
||||
server_name: Option<Box<ServerName>>,
|
||||
#[arg(short, long)]
|
||||
user_id: Option<Box<UserId>>,
|
||||
#[arg(short, long)]
|
||||
push_key: Option<String>,
|
||||
},
|
||||
|
||||
/// - Queries database for `servernameevent_data` which are the queued up
|
||||
/// requests that will eventually be sent
|
||||
///
|
||||
/// This command takes only *one* format of these arguments:
|
||||
///
|
||||
/// appservice_id
|
||||
/// server_name
|
||||
/// user_id AND push_key
|
||||
///
|
||||
/// See src/service/sending/mod.rs for the definition of the `Destination`
|
||||
/// enum
|
||||
QueuedRequests {
|
||||
#[arg(short, long)]
|
||||
appservice_id: Option<String>,
|
||||
#[arg(short, long)]
|
||||
server_name: Option<Box<ServerName>>,
|
||||
#[arg(short, long)]
|
||||
user_id: Option<Box<UserId>>,
|
||||
#[arg(short, long)]
|
||||
push_key: Option<String>,
|
||||
},
|
||||
|
||||
GetLatestEduCount {
|
||||
server_name: Box<ServerName>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
/// All the getters and iterators from src/database/key_value/users.rs
|
||||
pub(crate) enum Users {
|
||||
Iter,
|
||||
}
|
||||
|
||||
/// Processes admin query commands
|
||||
pub(crate) async fn process(command: QueryCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
Ok(match command {
|
||||
QueryCommand::AccountData(command) => account_data(command).await?,
|
||||
QueryCommand::Appservice(command) => appservice(command).await?,
|
||||
QueryCommand::Presence(command) => presence(command).await?,
|
||||
QueryCommand::RoomAlias(command) => room_alias(command).await?,
|
||||
QueryCommand::Globals(command) => globals(command).await?,
|
||||
QueryCommand::Sending(command) => sending(command).await?,
|
||||
QueryCommand::Users(command) => users(command).await?,
|
||||
})
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use super::Presence;
|
||||
use crate::{services, Result};
|
||||
|
||||
/// All the getters and iterators in key_value/presence.rs
|
||||
pub(crate) async fn presence(subcommand: Presence) -> Result<RoomMessageEventContent> {
|
||||
match subcommand {
|
||||
Presence::GetPresence {
|
||||
user_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().presence.db.get_presence(&user_id)?;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
Presence::PresenceSince {
|
||||
since,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().presence.db.presence_since(since);
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
let presence_since: Vec<(_, _, _)> = results.collect();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", presence_since),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
presence_since
|
||||
),
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use super::RoomAlias;
|
||||
use crate::{services, Result};
|
||||
|
||||
/// All the getters and iterators in src/database/key_value/rooms/alias.rs
|
||||
pub(crate) async fn room_alias(subcommand: RoomAlias) -> Result<RoomMessageEventContent> {
|
||||
match subcommand {
|
||||
RoomAlias::ResolveLocalAlias {
|
||||
alias,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().rooms.alias.db.resolve_local_alias(&alias);
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
RoomAlias::LocalAliasesForRoom {
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().rooms.alias.db.local_aliases_for_room(&room_id);
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
let aliases: Vec<_> = results.collect();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", aliases),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
aliases
|
||||
),
|
||||
))
|
||||
},
|
||||
RoomAlias::AllLocalAliases => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().rooms.alias.db.all_local_aliases();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
let aliases: Vec<_> = results.collect();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", aliases),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
aliases
|
||||
),
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use super::Sending;
|
||||
use crate::{service::sending::Destination, services, Result};
|
||||
|
||||
/// All the getters and iterators in key_value/sending.rs
|
||||
pub(crate) async fn sending(subcommand: Sending) -> Result<RoomMessageEventContent> {
|
||||
match subcommand {
|
||||
Sending::ActiveRequests => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().sending.db.active_requests();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
let active_requests: Result<Vec<(_, _, _)>> = results.collect();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", active_requests),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
active_requests
|
||||
),
|
||||
))
|
||||
},
|
||||
Sending::QueuedRequests {
|
||||
appservice_id,
|
||||
server_name,
|
||||
user_id,
|
||||
push_key,
|
||||
} => {
|
||||
if appservice_id.is_none() && server_name.is_none() && user_id.is_none() && push_key.is_none() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"An appservice ID, server name, or a user ID with push key must be specified via arguments. See \
|
||||
--help for more details.",
|
||||
));
|
||||
}
|
||||
|
||||
let (results, query_time) = match (appservice_id, server_name, user_id, push_key) {
|
||||
(Some(appservice_id), None, None, None) => {
|
||||
if appservice_id.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"An appservice ID, server name, or a user ID with push key must be specified via \
|
||||
arguments. See --help for more details.",
|
||||
));
|
||||
}
|
||||
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services()
|
||||
.sending
|
||||
.db
|
||||
.queued_requests(&Destination::Appservice(appservice_id));
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
(results, query_time)
|
||||
},
|
||||
(None, Some(server_name), None, None) => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services()
|
||||
.sending
|
||||
.db
|
||||
.queued_requests(&Destination::Normal(server_name.into()));
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
(results, query_time)
|
||||
},
|
||||
(None, None, Some(user_id), Some(push_key)) => {
|
||||
if push_key.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"An appservice ID, server name, or a user ID with push key must be specified via \
|
||||
arguments. See --help for more details.",
|
||||
));
|
||||
}
|
||||
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services()
|
||||
.sending
|
||||
.db
|
||||
.queued_requests(&Destination::Push(user_id.into(), push_key));
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
(results, query_time)
|
||||
},
|
||||
(Some(_), Some(_), Some(_), Some(_)) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"An appservice ID, server name, or a user ID with push key must be specified via arguments. \
|
||||
Not all of them See --help for more details.",
|
||||
));
|
||||
},
|
||||
_ => {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"An appservice ID, server name, or a user ID with push key must be specified via arguments. \
|
||||
See --help for more details.",
|
||||
));
|
||||
},
|
||||
};
|
||||
|
||||
let queued_requests = results.collect::<Result<Vec<(_, _)>>>();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", queued_requests),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
queued_requests
|
||||
),
|
||||
))
|
||||
},
|
||||
Sending::ActiveRequestsFor {
|
||||
appservice_id,
|
||||
server_name,
|
||||
user_id,
|
||||
push_key,
|
||||
} => {
|
||||
if appservice_id.is_none() && server_name.is_none() && user_id.is_none() && push_key.is_none() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"An appservice ID, server name, or a user ID with push key must be specified via arguments. See \
|
||||
--help for more details.",
|
||||
));
|
||||
}
|
||||
|
||||
let (results, query_time) = match (appservice_id, server_name, user_id, push_key) {
|
||||
(Some(appservice_id), None, None, None) => {
|
||||
if appservice_id.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"An appservice ID, server name, or a user ID with push key must be specified via \
|
||||
arguments. See --help for more details.",
|
||||
));
|
||||
}
|
||||
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services()
|
||||
.sending
|
||||
.db
|
||||
.active_requests_for(&Destination::Appservice(appservice_id));
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
(results, query_time)
|
||||
},
|
||||
(None, Some(server_name), None, None) => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services()
|
||||
.sending
|
||||
.db
|
||||
.active_requests_for(&Destination::Normal(server_name.into()));
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
(results, query_time)
|
||||
},
|
||||
(None, None, Some(user_id), Some(push_key)) => {
|
||||
if push_key.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"An appservice ID, server name, or a user ID with push key must be specified via \
|
||||
arguments. See --help for more details.",
|
||||
));
|
||||
}
|
||||
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services()
|
||||
.sending
|
||||
.db
|
||||
.active_requests_for(&Destination::Push(user_id.into(), push_key));
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
(results, query_time)
|
||||
},
|
||||
(Some(_), Some(_), Some(_), Some(_)) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"An appservice ID, server name, or a user ID with push key must be specified via arguments. \
|
||||
Not all of them See --help for more details.",
|
||||
));
|
||||
},
|
||||
_ => {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"An appservice ID, server name, or a user ID with push key must be specified via arguments. \
|
||||
See --help for more details.",
|
||||
));
|
||||
},
|
||||
};
|
||||
|
||||
let active_requests = results.collect::<Result<Vec<(_, _)>>>();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", active_requests),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
active_requests
|
||||
),
|
||||
))
|
||||
},
|
||||
Sending::GetLatestEduCount {
|
||||
server_name,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().sending.db.get_latest_educount(&server_name);
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
results
|
||||
),
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use super::Users;
|
||||
use crate::{services, Result};
|
||||
|
||||
/// All the getters and iterators in key_value/users.rs
|
||||
pub(crate) async fn users(subcommand: Users) -> Result<RoomMessageEventContent> {
|
||||
match subcommand {
|
||||
Users::Iter => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services().users.db.iter();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
let users = results.collect::<Vec<_>>();
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(
|
||||
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", users),
|
||||
format!(
|
||||
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
|
||||
users
|
||||
),
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
use clap::Subcommand;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomId, RoomOrAliasId};
|
||||
|
||||
use self::room_commands::list;
|
||||
use crate::Result;
|
||||
|
||||
pub(crate) mod room_alias_commands;
|
||||
pub(crate) mod room_commands;
|
||||
pub(crate) mod room_directory_commands;
|
||||
pub(crate) mod room_moderation_commands;
|
||||
|
||||
#[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),
|
||||
}
|
||||
|
||||
#[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>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[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>,
|
||||
},
|
||||
}
|
||||
|
||||
#[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 (room IDs and room aliases) 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: RoomCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
Ok(match command {
|
||||
RoomCommand::Alias(command) => room_alias_commands::process(command, body).await?,
|
||||
|
||||
RoomCommand::Directory(command) => room_directory_commands::process(command, body).await?,
|
||||
|
||||
RoomCommand::Moderation(command) => room_moderation_commands::process(command, body).await?,
|
||||
|
||||
RoomCommand::List {
|
||||
page,
|
||||
} => list(body, page).await?,
|
||||
})
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId};
|
||||
|
||||
use super::RoomAliasCommand;
|
||||
use crate::{service::admin::escape_html, services, Result};
|
||||
|
||||
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}"))),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
|
||||
|
||||
use crate::{
|
||||
service::admin::{escape_html, get_room_info, PAGE_SIZE},
|
||||
services, Result,
|
||||
};
|
||||
|
||||
pub(crate) async fn list(_body: Vec<&str>, page: Option<usize>) -> Result<RoomMessageEventContent> {
|
||||
// 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).saturating_mul(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))
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
|
||||
|
||||
use super::RoomDirectoryCommand;
|
||||
use crate::{
|
||||
service::admin::{escape_html, get_room_info, PAGE_SIZE},
|
||||
services, Result,
|
||||
};
|
||||
|
||||
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).saturating_mul(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))
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,505 +0,0 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use ruma::{
|
||||
events::room::message::RoomMessageEventContent, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId,
|
||||
};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use super::RoomModerationCommand;
|
||||
use crate::{
|
||||
api::client_server::{get_alias_helper, leave_room},
|
||||
service::admin::{escape_html, Service},
|
||||
services,
|
||||
utils::user_id::user_is_local,
|
||||
Result,
|
||||
};
|
||||
|
||||
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().await? {
|
||||
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 = if let Some(room_id) = services().rooms.alias.resolve_local_alias(&room_alias)? {
|
||||
room_id
|
||||
} else {
|
||||
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, None).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| {
|
||||
user_is_local(local_user)
|
||||
// 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)
|
||||
&& (user_is_local(local_user)
|
||||
&& 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 admin_room_alias: Box<RoomAliasId> = format!("#admins:{}", services().globals.server_name())
|
||||
.try_into()
|
||||
.expect("#admins:server_name is a valid alias name");
|
||||
|
||||
let mut room_ban_count = 0;
|
||||
let mut room_ids: Vec<OwnedRoomId> = Vec::new();
|
||||
|
||||
for &room in &rooms_s {
|
||||
match <&RoomOrAliasId>::try_from(room) {
|
||||
Ok(room_alias_or_id) => {
|
||||
if let Some(admin_room_id) = Service::get_admin_room().await? {
|
||||
if room.to_owned().eq(&admin_room_id) || room.to_owned().eq(&admin_room_alias) {
|
||||
info!("User specified admin room in bulk ban list, ignoring");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if room_alias_or_id.is_room_id() {
|
||||
let room_id = match RoomId::parse(room_alias_or_id) {
|
||||
Ok(room_id) => room_id,
|
||||
Err(e) => {
|
||||
if force {
|
||||
// ignore rooms we failed to parse if we're force banning
|
||||
warn!(
|
||||
"Error parsing room \"{room}\" during bulk room banning, ignoring \
|
||||
error and logging here: {e}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"{room} is not a valid room ID or room alias, please fix the list and try \
|
||||
again: {e}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
|
||||
room_ids.push(room_id);
|
||||
}
|
||||
|
||||
if room_alias_or_id.is_room_alias_id() {
|
||||
match RoomAliasId::parse(room_alias_or_id) {
|
||||
Ok(room_alias) => {
|
||||
let room_id = if let Some(room_id) =
|
||||
services().rooms.alias.resolve_local_alias(&room_alias)?
|
||||
{
|
||||
room_id
|
||||
} else {
|
||||
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, None).await {
|
||||
Ok(response) => {
|
||||
debug!(
|
||||
"Got federation response fetching room ID for room {room}: \
|
||||
{:?}",
|
||||
response
|
||||
);
|
||||
response.room_id
|
||||
},
|
||||
Err(e) => {
|
||||
// don't fail if force blocking
|
||||
if force {
|
||||
warn!("Failed to resolve room alias {room} to a room ID: {e}");
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to resolve room alias {room} to a room ID: {e}"
|
||||
)));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
room_ids.push(room_id);
|
||||
},
|
||||
Err(e) => {
|
||||
if force {
|
||||
// ignore rooms we failed to parse if we're force deleting
|
||||
error!(
|
||||
"Error parsing room \"{room}\" during bulk room banning, ignoring \
|
||||
error and logging here: {e}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"{room} is not a valid room ID or room alias, please fix the list and try \
|
||||
again: {e}"
|
||||
)));
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
if force {
|
||||
// ignore rooms we failed to parse if we're force deleting
|
||||
error!(
|
||||
"Error parsing room \"{room}\" during bulk room banning, ignoring error and \
|
||||
logging here: {e}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"{room} is not a valid room ID or room alias, 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 {room_ban_count} total rooms, evicted all users, and disabled \
|
||||
incoming federation with the room."
|
||||
)));
|
||||
}
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Finished bulk room ban, banned {room_ban_count} total rooms and evicted all users."
|
||||
)));
|
||||
}
|
||||
|
||||
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 = if let Some(room_id) = services().rooms.alias.resolve_local_alias(&room_alias)? {
|
||||
room_id
|
||||
} else {
|
||||
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, None).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
|
||||
)))
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
pub(crate) mod server_commands;
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use self::server_commands::{
|
||||
backup_database, clear_database_caches, clear_service_caches, list_backups, list_database_files, memory_usage,
|
||||
show_config, uptime,
|
||||
};
|
||||
use crate::Result;
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum ServerCommand {
|
||||
/// - Time elapsed since startup
|
||||
Uptime,
|
||||
|
||||
/// - 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> {
|
||||
Ok(match command {
|
||||
ServerCommand::Uptime => uptime(body).await?,
|
||||
ServerCommand::ShowConfig => show_config(body).await?,
|
||||
ServerCommand::MemoryUsage => memory_usage(body).await?,
|
||||
ServerCommand::ClearDatabaseCaches {
|
||||
amount,
|
||||
} => clear_database_caches(body, amount).await?,
|
||||
ServerCommand::ClearServiceCaches {
|
||||
amount,
|
||||
} => clear_service_caches(body, amount).await?,
|
||||
ServerCommand::ListBackups => list_backups(body).await?,
|
||||
ServerCommand::BackupDatabase => backup_database(body).await?,
|
||||
ServerCommand::ListDatabaseFiles => list_database_files(body).await?,
|
||||
})
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use crate::{services, Result};
|
||||
|
||||
pub(crate) async fn uptime(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
let seconds = services()
|
||||
.globals
|
||||
.started
|
||||
.elapsed()
|
||||
.expect("standard duration")
|
||||
.as_secs();
|
||||
let result = format!(
|
||||
"up {} days, {} hours, {} minutes, {} seconds.",
|
||||
seconds / 86400,
|
||||
(seconds % 86400) / 60 / 60,
|
||||
(seconds % 3600) / 60,
|
||||
seconds % 60,
|
||||
);
|
||||
|
||||
Ok(RoomMessageEventContent::notice_html(String::new(), result))
|
||||
}
|
||||
|
||||
pub(crate) async fn show_config(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
// Construct and send the response
|
||||
Ok(RoomMessageEventContent::text_plain(format!("{}", services().globals.config)))
|
||||
}
|
||||
|
||||
pub(crate) async fn memory_usage(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
let response0 = services().memory_usage().await;
|
||||
let response1 = services().globals.db.memory_usage();
|
||||
let response2 = crate::alloc::memory_usage();
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Services:\n{response0}\n\nDatabase:\n{response1}\n{}",
|
||||
if !response2.is_empty() {
|
||||
format!("Allocator:\n {response2}")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_database_caches(_body: Vec<&str>, amount: u32) -> Result<RoomMessageEventContent> {
|
||||
services().globals.db.clear_caches(amount);
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain("Done."))
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_service_caches(_body: Vec<&str>, amount: u32) -> Result<RoomMessageEventContent> {
|
||||
services().clear_caches(amount).await;
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain("Done."))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_backups(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
let result = services().globals.db.backup_list()?;
|
||||
|
||||
if result.is_empty() {
|
||||
Ok(RoomMessageEventContent::text_plain("No backups found."))
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain(result))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn backup_database(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
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))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_database_files(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
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))
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
//! test commands generally used for hot lib reloadable functions.
|
||||
//! see <https://github.com/rksm/hot-lib-reloader-rs?tab=readme-ov-file#usage> for more details if you are a dev
|
||||
|
||||
//#[cfg(not(feature = "hot_reload"))]
|
||||
//#[allow(unused_imports)]
|
||||
//#[allow(clippy::wildcard_imports)]
|
||||
// non hot reloadable functions (?)
|
||||
//use hot_lib::*;
|
||||
#[cfg(feature = "hot_reload")]
|
||||
#[allow(unused_imports)]
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use hot_lib_funcs::*;
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use crate::{debug_error, Result};
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
#[hot_lib_reloader::hot_module(dylib = "lib")]
|
||||
mod hot_lib_funcs {
|
||||
// these will be functions from lib.rs, so `use hot_lib_funcs::test_command;`
|
||||
hot_functions_from_file!("hot_lib/src/lib.rs");
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(clap::Subcommand)]
|
||||
pub(crate) enum TestCommands {
|
||||
// !admin test test1
|
||||
Test1,
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: TestCommands, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
Ok(match command {
|
||||
TestCommands::Test1 => {
|
||||
debug_error!("before calling test_command");
|
||||
test_command();
|
||||
debug_error!("after calling test_command");
|
||||
|
||||
RoomMessageEventContent::notice_plain(String::from("loaded"))
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use crate::Result;
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(clap::Subcommand)]
|
||||
pub(crate) enum TesterCommands {
|
||||
Tester,
|
||||
}
|
||||
pub(crate) async fn process(command: TesterCommands, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
Ok(match command {
|
||||
TesterCommands::Tester => RoomMessageEventContent::notice_plain(String::from("complete")),
|
||||
})
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
pub(crate) mod user_commands;
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use self::user_commands::{create, deactivate, deactivate_all, list, list_joined_rooms, reset_password};
|
||||
use crate::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: String,
|
||||
},
|
||||
|
||||
/// - 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: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) async fn process(command: UserCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
Ok(match command {
|
||||
UserCommand::List => list(body).await?,
|
||||
UserCommand::Create {
|
||||
username,
|
||||
password,
|
||||
} => create(body, username, password).await?,
|
||||
UserCommand::Deactivate {
|
||||
leave_rooms,
|
||||
user_id,
|
||||
} => deactivate(body, leave_rooms, user_id).await?,
|
||||
UserCommand::ResetPassword {
|
||||
username,
|
||||
} => reset_password(body, username).await?,
|
||||
UserCommand::DeactivateAll {
|
||||
leave_rooms,
|
||||
force,
|
||||
} => deactivate_all(body, leave_rooms, force).await?,
|
||||
UserCommand::ListJoinedRooms {
|
||||
user_id,
|
||||
} => list_joined_rooms(body, user_id).await?,
|
||||
})
|
||||
}
|
|
@ -1,370 +0,0 @@
|
|||
use std::{fmt::Write as _, sync::Arc};
|
||||
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, 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::{self, user_id::user_is_local},
|
||||
Result,
|
||||
};
|
||||
|
||||
pub(crate) async fn list(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||
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())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
_body: Vec<&str>, username: String, password: Option<String>,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
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_is_local(&user_id) {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} does not belong to our server."
|
||||
)));
|
||||
}
|
||||
|
||||
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
|
||||
.config
|
||||
.new_user_displayname_suffix
|
||||
.is_empty()
|
||||
{
|
||||
_ = write!(displayname, " {}", services().globals.config.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}`"
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn deactivate(
|
||||
_body: Vec<&str>, leave_rooms: bool, user_id: String,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
// Validate user id
|
||||
let user_id =
|
||||
match UserId::parse_with_server_name(user_id.as_str().to_lowercase(), services().globals.server_name()) {
|
||||
Ok(id) => Arc::<UserId>::from(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."
|
||||
)));
|
||||
}
|
||||
|
||||
// don't deactivate the conduit service account
|
||||
if user_id
|
||||
== UserId::parse_with_server_name("conduit", services().globals.server_name()).expect("conduit user exists")
|
||||
{
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Not allowed to deactivate the Conduit service account.",
|
||||
));
|
||||
}
|
||||
|
||||
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"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn reset_password(_body: Vec<&str>, username: String) -> Result<RoomMessageEventContent> {
|
||||
// Validate user id
|
||||
let user_id =
|
||||
match UserId::parse_with_server_name(username.as_str().to_lowercase(), services().globals.server_name()) {
|
||||
Ok(id) => Arc::<UserId>::from(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}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn deactivate_all(body: Vec<&str>, leave_rooms: bool, force: bool) -> Result<RoomMessageEventContent> {
|
||||
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: u16 = 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;
|
||||
}
|
||||
|
||||
// don't deactivate the conduit service account
|
||||
if user_id
|
||||
== UserId::parse_with_server_name("conduit", services().globals.server_name())
|
||||
.expect("conduit user exists")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// user does not exist on our server
|
||||
if !services().users.exists(user_id)? {
|
||||
continue;
|
||||
}
|
||||
|
||||
if services().users.deactivate_account(user_id).is_ok() {
|
||||
deactivation_count = deactivation_count.saturating_add(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.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn list_joined_rooms(_body: Vec<&str>, user_id: String) -> Result<RoomMessageEventContent> {
|
||||
// Validate user id
|
||||
let user_id =
|
||||
match UserId::parse_with_server_name(user_id.as_str().to_lowercase(), services().globals.server_name()) {
|
||||
Ok(id) => Arc::<UserId>::from(id),
|
||||
Err(e) => {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"The supplied username is not a valid username: {e}"
|
||||
)))
|
||||
},
|
||||
};
|
||||
|
||||
if !user_is_local(&user_id) {
|
||||
return Ok(RoomMessageEventContent::text_plain("User does not belong to our server."));
|
||||
}
|
||||
|
||||
if !services().users.exists(&user_id)? {
|
||||
return Ok(RoomMessageEventContent::text_plain("User does not exist on this server."));
|
||||
}
|
||||
|
||||
let mut rooms: Vec<(OwnedRoomId, u64, String)> = services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
.filter_map(Result::ok)
|
||||
.map(|room_id| get_room_info(&room_id))
|
||||
.collect();
|
||||
|
||||
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.len(),
|
||||
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.len(),
|
||||
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
Add a link
Reference in a new issue