From bd172753fb4ef5d967b0ca94827616f70e9166ac Mon Sep 17 00:00:00 2001 From: asonix Date: Wed, 2 Nov 2022 17:58:52 -0500 Subject: [PATCH] Add basic administration via telegram --- Cargo.lock | 320 +++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 +- README.md | 6 + src/config.rs | 17 +++ src/main.rs | 5 + src/telegram.rs | 90 ++++++++++++++ 6 files changed, 440 insertions(+), 5 deletions(-) create mode 100644 src/telegram.rs diff --git a/Cargo.lock b/Cargo.lock index 27b0983..cd78fba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "ap-relay" -version = "0.3.28" +version = "0.3.29" dependencies = [ "activitystreams", "activitystreams-ext", @@ -311,6 +311,7 @@ dependencies = [ "sha2", "signature", "sled", + "teloxide", "thiserror", "tokio", "toml", @@ -325,6 +326,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "aquamarine" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941c39708478e8eea39243b5983f1c42d2717b3620ee91f4a52115fd02ac43f" +dependencies = [ + "itertools 0.9.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "arc-swap" version = "1.5.1" @@ -590,6 +604,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "clap" version = "4.0.18" @@ -754,6 +778,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "5.4.0" @@ -814,6 +873,15 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dptree" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81175dab5ec79c30e0576df2ed2c244e1721720c302000bb321b107e82e265c" +dependencies = [ + "futures", +] + [[package]] name = "either" version = "1.8.0" @@ -829,6 +897,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "erasable" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f11890ce181d47a64e5d1eb4b6caba0e7bae911a356723740d058a5d0340b7d" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -903,6 +981,7 @@ checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1183,6 +1262,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-timeout" version = "0.4.1" @@ -1195,6 +1287,12 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.3.0" @@ -1224,6 +1322,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + [[package]] name = "iri-string" version = "0.5.6" @@ -1233,6 +1337,15 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1415,6 +1528,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1448,6 +1571,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "never" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" + [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -1977,7 +2106,7 @@ checksum = "7f835c582e6bd972ba8347313300219fed5bfa52caf175298d860b61ff6069bb" dependencies = [ "bytes", "heck", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "multimap", @@ -1996,7 +2125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7345d5f0e08c0536d7ac7229952590239e77abf0a0100a1b1d890add6ea96364" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn", @@ -2051,6 +2180,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rc-box" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0690759eabf094030c2cdabc25ade1395bac02210d920d655053c1d49583fd8" +dependencies = [ + "erasable", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -2095,6 +2233,47 @@ dependencies = [ "winapi", ] +[[package]] +name = "reqwest" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -2180,7 +2359,7 @@ checksum = "85517cd381cf0c34694881d8aaf173107c6af7670e66cec18d7a1a8bfce3b758" dependencies = [ "base64", "bytecount", - "itertools", + "itertools 0.10.5", "md5", "mime", "nom", @@ -2218,6 +2397,15 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64", +] + [[package]] name = "ryu" version = "1.0.11" @@ -2289,6 +2477,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.5" @@ -2457,6 +2657,87 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "takecell" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e" + +[[package]] +name = "teloxide" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94734a391eb4f3b6172b285fc10593192f9bdb4c8a377075cff063d967f0e43b" +dependencies = [ + "aquamarine", + "bytes", + "derive_more", + "dptree", + "futures", + "log", + "mime", + "pin-project", + "serde", + "serde_json", + "serde_with_macros", + "teloxide-core", + "teloxide-macros", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "teloxide-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9243a720aa9bddda324a7f90b4ab42887425524bf4d5d24a56b50bccb984b7c4" +dependencies = [ + "bitflags", + "bytes", + "chrono", + "derive_more", + "either", + "futures", + "log", + "mime", + "never", + "once_cell", + "pin-project", + "rc-box", + "reqwest", + "serde", + "serde_json", + "serde_with_macros", + "take_mut", + "takecell", + "thiserror", + "tokio", + "tokio-util", + "url", + "uuid", +] + +[[package]] +name = "teloxide-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a5fc46d9004706ee23e3b73a0f53518f28498f7297813fa9a505a29638ffd6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" version = "3.3.0" @@ -2888,6 +3169,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.8" @@ -2924,6 +3214,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -2995,6 +3286,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.83" @@ -3152,6 +3455,15 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index ce6c944..935d62d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ap-relay" description = "A simple activitypub relay" -version = "0.3.28" +version = "0.3.29" authors = ["asonix "] license = "AGPL-3.0" readme = "README.md" @@ -48,6 +48,11 @@ serde_json = "1.0" sha2 = { version = "0.10", features = ["oid"] } signature = "1.6.4" sled = "0.34.7" +teloxide = { version = "0.11.1", default-features = false, features = [ + "ctrlc_handler", + "macros", + "rustls", +] } thiserror = "1.0" tracing = "0.1" tracing-awc = "0.1.6" diff --git a/README.md b/README.md index 1dabcbe..fe79fd2 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ PRETTY_LOG=false PUBLISH_BLOCKS=true SLED_PATH=./sled/db-0.34 OPENTELEMETRY_URL=localhost:4317 +TELEGRAM_TOKEN=secret +TELEGRAM_ADMIN_HANDLE=your_handle ``` #### Descriptions @@ -116,6 +118,10 @@ Where to store the on-disk database of connected servers. This defaults to `./sl The URL to the source code for the relay. This defaults to `https://git.asonix.dog/asonix/relay`, but should be changed if you're running a fork hosted elsewhere. ##### `OPENTELEMETRY_URL` A URL for exporting opentelemetry spans. This is mostly useful for debugging. There is no default, since most people probably don't run an opentelemetry collector. +##### `TELEGRAM_TOKEN` +A Telegram Bot Token for running the relay administration bot. There is no default. +##### `TELEGRAM_ADMIN_HANDLE` +The handle of the telegram user allowed to administer the relay. There is no default. ### Subscribing Mastodon admins can subscribe to this relay by adding the `/inbox` route to their relay settings. diff --git a/src/config.rs b/src/config.rs index dc030b9..e42b755 100644 --- a/src/config.rs +++ b/src/config.rs @@ -30,6 +30,8 @@ pub(crate) struct ParsedConfig { sled_path: PathBuf, source_repo: IriString, opentelemetry_url: Option, + telegram_token: Option, + telegram_admin_handle: Option, } #[derive(Clone)] @@ -45,6 +47,8 @@ pub struct Config { sled_path: PathBuf, source_repo: IriString, opentelemetry_url: Option, + telegram_token: Option, + telegram_admin_handle: Option, } #[derive(Debug)] @@ -78,6 +82,8 @@ impl std::fmt::Debug for Config { "opentelemetry_url", &self.opentelemetry_url.as_ref().map(|url| url.to_string()), ) + .field("telegram_token", &"[redacted]") + .field("telegram_admin_handle", &self.telegram_admin_handle) .finish() } } @@ -96,6 +102,8 @@ impl Config { .set_default("sled_path", "./sled/db-0-34")? .set_default("source_repo", "https://git.asonix.dog/asonix/relay")? .set_default("opentelemetry_url", None as Option<&str>)? + .set_default("telegram_token", None as Option<&str>)? + .set_default("telegram_admin_handle", None as Option<&str>)? .add_source(Environment::default()) .build()?; @@ -116,6 +124,8 @@ impl Config { sled_path: config.sled_path, source_repo: config.source_repo, opentelemetry_url: config.opentelemetry_url, + telegram_token: config.telegram_token, + telegram_admin_handle: config.telegram_admin_handle, }) } @@ -225,6 +235,13 @@ impl Config { self.opentelemetry_url.as_ref() } + pub(crate) fn telegram_info(&self) -> Option<(&str, &str)> { + self.telegram_token.as_deref().and_then(|token| { + let handle = self.telegram_admin_handle.as_deref()?; + Some((token, handle)) + }) + } + pub(crate) fn generate_url(&self, kind: UrlKind) -> IriString { self.do_generate_url(kind).expect("Generated valid IRI") } diff --git a/src/main.rs b/src/main.rs index b1cb715..b35c5f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ mod jobs; mod middleware; mod requests; mod routes; +mod telegram; use self::{ args::Args, @@ -118,6 +119,10 @@ async fn main() -> Result<(), anyhow::Error> { let (manager, job_server) = create_workers(state.clone(), actors.clone(), media.clone(), config.clone()); + if let Some((token, admin_handle)) = config.telegram_info() { + telegram::start(admin_handle.to_owned(), db.clone(), token); + } + let bind_address = config.bind_address(); HttpServer::new(move || { App::new() diff --git a/src/telegram.rs b/src/telegram.rs new file mode 100644 index 0000000..9c0c2c0 --- /dev/null +++ b/src/telegram.rs @@ -0,0 +1,90 @@ +use crate::db::Db; +use activitystreams::iri_string::types::IriString; +use std::sync::Arc; +use teloxide::{prelude::*, utils::command::BotCommands}; + +#[derive(BotCommands, Clone)] +#[command( + rename_rule = "lowercase", + description = "These commands are for administering AodeRelay" +)] +enum Command { + #[command(description = "Display this text.")] + Help, + + #[command(description = "Block a domain from the relay.")] + Block { domain: IriString }, + + #[command(description = "Unblock a domain from the relay.")] + Unblock { domain: IriString }, + + #[command(description = "Allow a domain to connect to the relay (for RESTRICTED_MODE)")] + Allow { domain: IriString }, + + #[command(description = "Disallow a domain to connect to the relay (for RESTRICTED_MODE)")] + Disallow { domain: IriString }, +} + +pub(crate) fn start(admin_handle: String, db: Db, token: &str) { + let bot = Bot::new(token); + let admin_handle = Arc::new(admin_handle); + + actix_rt::spawn(async move { + teloxide::repl(bot, move |bot: Bot, msg: Message, cmd: Command| { + let admin_handle = admin_handle.clone(); + let db = db.clone(); + + async move { + if !is_admin(&admin_handle, &msg) { + return Ok(()); + } + + answer(bot, msg, cmd, db).await + } + }) + .await; + }); +} + +fn is_admin(admin_handle: &str, message: &Message) -> bool { + message + .from() + .and_then(|user| user.username.as_deref()) + .map(|username| username == admin_handle) + .unwrap_or(false) +} + +async fn answer(bot: Bot, msg: Message, cmd: Command, db: Db) -> ResponseResult<()> { + match cmd { + Command::Help => { + bot.send_message(msg.chat.id, Command::descriptions().to_string()) + .await?; + } + Command::Block { domain } => { + if db.add_blocks(vec![domain.to_string()]).await.is_ok() { + bot.send_message(msg.chat.id, format!("{} has been blocked", domain)) + .await?; + } + } + Command::Unblock { domain } => { + if db.remove_blocks(vec![domain.to_string()]).await.is_ok() { + bot.send_message(msg.chat.id, format!("{} has been unblocked", domain)) + .await?; + } + } + Command::Allow { domain } => { + if db.add_allows(vec![domain.to_string()]).await.is_ok() { + bot.send_message(msg.chat.id, format!("{} has been allowed", domain)) + .await?; + } + } + Command::Disallow { domain } => { + if db.remove_allows(vec![domain.to_string()]).await.is_ok() { + bot.send_message(msg.chat.id, format!("{} has been disallwoed", domain)) + .await?; + } + } + } + + Ok(()) +}