summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--APPSERVICES.md8
-rw-r--r--Cargo.toml3
-rw-r--r--src/database/admin.rs302
3 files changed, 175 insertions, 138 deletions
diff --git a/APPSERVICES.md b/APPSERVICES.md
index 894bc6f..257166e 100644
--- a/APPSERVICES.md
+++ b/APPSERVICES.md
@@ -18,7 +18,7 @@ First, go into the #admins room of your homeserver. The first person that
registered on the homeserver automatically joins it. Then send a message into
the room like this:
- @conduit:your.server.name: register_appservice
+ @conduit:your.server.name: register-appservice
```
paste
the
@@ -31,7 +31,7 @@ the room like this:
```
You can confirm it worked by sending a message like this:
-`@conduit:your.server.name: list_appservices`
+`@conduit:your.server.name: list-appservices`
The @conduit bot should answer with `Appservices (1): your-bridge`
@@ -46,9 +46,9 @@ could help.
To remove an appservice go to your admin room and execute
-```@conduit:your.server.name: unregister_appservice <name>```
+```@conduit:your.server.name: unregister-appservice <name>```
-where `<name>` one of the output of `list_appservices`.
+where `<name>` one of the output of `list-appservices`.
### Tested appservices
diff --git a/Cargo.toml b/Cargo.toml
index c87d949..08afe1f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -83,6 +83,9 @@ thread_local = "1.1.3"
# used for TURN server authentication
hmac = "0.11.0"
sha-1 = "0.9.8"
+# used for conduit's CLI and admin room command parsing
+structopt = { version = "0.3.25", default-features = false }
+pulldown-cmark = "0.9.1"
[features]
default = ["conduit_bin", "backend_sqlite", "backend_rocksdb"]
diff --git a/src/database/admin.rs b/src/database/admin.rs
index 518d758..55724db 100644
--- a/src/database/admin.rs
+++ b/src/database/admin.rs
@@ -5,6 +5,7 @@ use crate::{
pdu::PduBuilder,
server_server, Database, PduEvent,
};
+use regex::Regex;
use rocket::{
futures::{channel::mpsc, stream::StreamExt},
http::RawStr,
@@ -14,6 +15,7 @@ use ruma::{
EventId, RoomId, RoomVersionId, UserId,
};
use serde_json::value::to_raw_value;
+use structopt::StructOpt;
use tokio::sync::{MutexGuard, RwLock, RwLockReadGuard};
use tracing::warn;
@@ -146,78 +148,98 @@ impl Admin {
}
pub fn parse_admin_command(db: &Database, command_line: &str, body: Vec<&str>) -> AdminCommand {
- let mut parts = command_line.split_whitespace().skip(1);
+ let mut argv: Vec<_> = command_line.split_whitespace().skip(1).collect();
- let command_name = match parts.next() {
- Some(command) => command,
+ let command_name = match argv.get(0) {
+ Some(command) => *command,
None => {
- let message = "No command given. Use <code>help</code> for a list of commands.";
+ let markdown_message = "No command given. Use `help` for a list of commands.";
+ let html_message = markdown_to_html(&markdown_message);
+
return AdminCommand::SendMessage(RoomMessageEventContent::text_html(
- html_to_markdown(message),
- message,
+ markdown_message,
+ html_message,
));
}
};
- let args: Vec<_> = parts.collect();
+ // Backwards compatibility with `register_appservice`-style commands
+ let command_with_dashes;
+ if command_line.contains("_") {
+ command_with_dashes = command_name.replace("_", "-");
+ argv[0] = &command_with_dashes;
+ }
- match try_parse_admin_command(db, command_name, args, body) {
+ match try_parse_admin_command(db, argv, body) {
Ok(admin_command) => admin_command,
Err(error) => {
- let message = format!(
- "Encountered error while handling <code>{}</code> command:\n\
- <pre>{}</pre>",
+ let markdown_message = format!(
+ "Encountered an error while handling the `{}` command:\n\
+ ```\n{}\n```",
command_name, error,
);
+ let html_message = markdown_to_html(&markdown_message);
AdminCommand::SendMessage(RoomMessageEventContent::text_html(
- html_to_markdown(&message),
- message,
+ markdown_message,
+ html_message,
))
}
}
}
-// Helper for `RoomMessageEventContent::text_html`, which needs the content as
-// both markdown and HTML.
-fn html_to_markdown(text: &str) -> String {
- text.replace("<p>", "")
- .replace("</p>", "\n")
- .replace("<pre>", "```\n")
- .replace("</pre>", "\n```")
- .replace("<code>", "`")
- .replace("</code>", "`")
- .replace("<li>", "* ")
- .replace("</li>", "")
- .replace("<ul>\n", "")
- .replace("</ul>\n", "")
+#[derive(StructOpt)]
+enum AdminCommands {
+ #[structopt(verbatim_doc_comment)]
+ /// Register a bridge using its registration YAML
+ ///
+ /// This command needs a YAML generated by an appservice (such as a mautrix
+ /// bridge), which must be provided in a code-block below the command.
+ ///
+ /// Example:
+ /// ````
+ /// @conduit:example.com: register-appservice
+ /// ```
+ /// yaml content here
+ /// ```
+ /// ````
+ RegisterAppservice,
+ /// Unregister a bridge using its ID
+ UnregisterAppservice { appservice_identifier: String },
+ /// List all the currently registered bridges
+ ListAppservices,
+ /// Get the auth_chain of a PDU
+ GetAuthChain { event_id: Box<EventId> },
+ /// Parse and print a PDU from a JSON
+ ParsePdu,
+ /// Retrieve and print a PDU by ID from the Conduit database
+ GetPdu { event_id: Box<EventId> },
+ /// Print database memory usage statistics
+ DatabaseMemoryUsage,
}
-const HELP_TEXT: &'static str = r#"
-<p>The following commands are available:</p>
-<ul>
-<li><code>register_appservice</code>: Register a bridge using its registration YAML</li>
-<li><code>unregister_appservice</code>: Unregister a bridge using its ID</li>
-<li><code>list_appservices</code>: List all the currently registered bridges</li>
-<li><code>get_auth_chain</code>: Get the `auth_chain` of a PDU</li>
-<li><code>parse_pdu</code>: Parse and print a PDU from a JSON</li>
-<li><code>get_pdu</code>: Retrieve and print a PDU by ID from the Conduit database</li>
-<li><code>database_memory_usage</code>: Print database memory usage statistics</li>
-<ul>
-"#;
-
pub fn try_parse_admin_command(
db: &Database,
- command: &str,
- args: Vec<&str>,
+ mut argv: Vec<&str>,
body: Vec<&str>,
) -> Result<AdminCommand> {
- let command = match command {
- "help" => AdminCommand::SendMessage(RoomMessageEventContent::text_html(
- html_to_markdown(HELP_TEXT),
- HELP_TEXT,
- )),
- "register_appservice" => {
+ argv.insert(0, "@conduit:example.com:");
+ let command = match AdminCommands::from_iter_safe(argv) {
+ Ok(command) => command,
+ Err(error) => {
+ println!("Before:\n{}\n", error.to_string());
+ let markdown_message = usage_to_markdown(&error.to_string())
+ .replace("example.com", db.globals.server_name().as_str());
+ let html_message = markdown_to_html(&markdown_message);
+
+ return Ok(AdminCommand::SendMessage(
+ RoomMessageEventContent::text_html(markdown_message, html_message),
+ ));
+ }
+ };
+
+ let admin_command = match command {
+ AdminCommands::RegisterAppservice => {
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" {
let appservice_config = body[1..body.len() - 1].join("\n");
let parsed_config = serde_yaml::from_str::<serde_yaml::Value>(&appservice_config);
@@ -233,47 +255,35 @@ pub fn try_parse_admin_command(
))
}
}
- "unregister_appservice" => {
- if args.len() == 1 {
- AdminCommand::UnregisterAppservice(args[0].to_owned())
+ AdminCommands::UnregisterAppservice {
+ appservice_identifier,
+ } => AdminCommand::UnregisterAppservice(appservice_identifier),
+ AdminCommands::ListAppservices => AdminCommand::ListAppservices,
+ AdminCommands::GetAuthChain { event_id } => {
+ let event_id = Arc::<EventId>::from(event_id);
+ if let Some(event) = db.rooms.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 = server_server::get_auth_chain(room_id, vec![event_id], db)?.count();
+ let elapsed = start.elapsed();
+ return Ok(AdminCommand::SendMessage(
+ RoomMessageEventContent::text_plain(format!(
+ "Loaded auth chain with length {} in {:?}",
+ count, elapsed
+ )),
+ ));
} else {
- AdminCommand::SendMessage(RoomMessageEventContent::text_plain(
- "Missing appservice identifier",
- ))
+ AdminCommand::SendMessage(RoomMessageEventContent::text_plain("Event not found."))
}
}
- "list_appservices" => AdminCommand::ListAppservices,
- "get_auth_chain" => {
- if args.len() == 1 {
- if let Ok(event_id) = EventId::parse_arc(args[0]) {
- if let Some(event) = db.rooms.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 =
- server_server::get_auth_chain(room_id, vec![event_id], db)?.count();
- let elapsed = start.elapsed();
- return Ok(AdminCommand::SendMessage(
- RoomMessageEventContent::text_plain(format!(
- "Loaded auth chain with length {} in {:?}",
- count, elapsed
- )),
- ));
- }
- }
- }
-
- AdminCommand::SendMessage(RoomMessageEventContent::text_plain(
- "Usage: get_auth_chain <event-id>",
- ))
- }
- "parse_pdu" => {
+ AdminCommands::ParsePdu => {
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" {
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
@@ -312,59 +322,83 @@ pub fn try_parse_admin_command(
))
}
}
- "get_pdu" => {
- if args.len() == 1 {
- if let Ok(event_id) = EventId::parse(args[0]) {
- let mut outlier = false;
- let mut pdu_json = db.rooms.get_non_outlier_pdu_json(&event_id)?;
- if pdu_json.is_none() {
- outlier = true;
- pdu_json = db.rooms.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");
- AdminCommand::SendMessage(
- RoomMessageEventContent::text_html(
- format!("{}\n```json\n{}\n```",
- if outlier {
- "PDU is outlier"
- } else { "PDU was accepted"}, json_text),
- format!("<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
- if outlier {
- "PDU is outlier"
- } else { "PDU was accepted"}, RawStr::new(&json_text).html_escape())
- ),
- )
- }
- None => AdminCommand::SendMessage(RoomMessageEventContent::text_plain(
- "PDU not found.",
- )),
- }
- } else {
- AdminCommand::SendMessage(RoomMessageEventContent::text_plain(
- "Event ID could not be parsed.",
+ AdminCommands::GetPdu { event_id } => {
+ let mut outlier = false;
+ let mut pdu_json = db.rooms.get_non_outlier_pdu_json(&event_id)?;
+ if pdu_json.is_none() {
+ outlier = true;
+ pdu_json = db.rooms.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");
+ AdminCommand::SendMessage(RoomMessageEventContent::text_html(
+ format!(
+ "{}\n```json\n{}\n```",
+ if outlier {
+ "PDU is outlier"
+ } else {
+ "PDU was accepted"
+ },
+ json_text
+ ),
+ format!(
+ "<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
+ if outlier {
+ "PDU is outlier"
+ } else {
+ "PDU was accepted"
+ },
+ RawStr::new(&json_text).html_escape()
+ ),
))
}
- } else {
- AdminCommand::SendMessage(RoomMessageEventContent::text_plain(
- "Usage: get_pdu <eventid>",
- ))
+ None => {
+ AdminCommand::SendMessage(RoomMessageEventContent::text_plain("PDU not found."))
+ }
}
}
- "database_memory_usage" => AdminCommand::ShowMemoryUsage,
- _ => {
- let message = format!(
- "Unrecognized command <code>{}</code>, try <code>help</code> for a list of commands.",
- command,
- );
- AdminCommand::SendMessage(RoomMessageEventContent::text_html(
- html_to_markdown(&message),
- message,
- ))
- }
+ AdminCommands::DatabaseMemoryUsage => AdminCommand::ShowMemoryUsage,
};
- Ok(command)
+ Ok(admin_command)
+}
+
+fn usage_to_markdown(text: &str) -> String {
+ // For the conduit admin room, subcommands become main commands
+ let text = text.replace("SUBCOMMAND", "COMMAND");
+ let text = text.replace("subcommand", "command");
+
+ // Put the first line (command name and version text) on its own paragraph
+ let re = Regex::new("^(.*?)\n").expect("Regex compilation should not fail");
+ let text = re.replace_all(&text, "*$1*\n\n");
+
+ // Wrap command names in backticks
+ // (?m) enables multi-line mode for ^ and $
+ let re = Regex::new("(?m)^ ([a-z-]+) +(.*)$").expect("Regex compilation should not fail");
+ let text = re.replace_all(&text, " `$1`: $2");
+
+ // Add * to list items
+ let re = Regex::new("(?m)^ (.*)$").expect("Regex compilation should not fail");
+ let text = re.replace_all(&text, "* $1");
+
+ // Turn section names to headings
+ let re = Regex::new("(?m)^([A-Z-]+):$").expect("Regex compilation should not fail");
+ let text = re.replace_all(&text, "#### $1");
+
+ text.to_string()
+}
+
+fn markdown_to_html(text: &str) -> String {
+ // CommonMark's spec allows HTML tags; however, CLI required arguments look
+ // very much like tags so escape them.
+ let text = text.replace("<", "&lt;").replace(">", "&gt;");
+
+ let mut html_output = String::new();
+
+ let parser = pulldown_cmark::Parser::new(&text);
+ pulldown_cmark::html::push_html(&mut html_output, parser);
+
+ html_output
}