From bd172753fb4ef5d967b0ca94827616f70e9166ac Mon Sep 17 00:00:00 2001
From: asonix <asonix@asonix.dog>
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 <asonix@asonix.dog>"]
 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<IriString>,
+    telegram_token: Option<String>,
+    telegram_admin_handle: Option<String>,
 }
 
 #[derive(Clone)]
@@ -45,6 +47,8 @@ pub struct Config {
     sled_path: PathBuf,
     source_repo: IriString,
     opentelemetry_url: Option<IriString>,
+    telegram_token: Option<String>,
+    telegram_admin_handle: Option<String>,
 }
 
 #[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(())
+}