From 7438471f840180bdd975ebee0375f52f56252af1 Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Tue, 23 May 2023 21:48:38 +0200 Subject: [PATCH] simple ping bot functionality --- Cargo.lock | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 8 ++- src/main.rs | 120 +++++++++++++++++++++++++++++++- src/ping.rs | 97 ++++++++++++++++++++++++++ 4 files changed, 417 insertions(+), 5 deletions(-) create mode 100644 src/ping.rs diff --git a/Cargo.lock b/Cargo.lock index 474f387..417071b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,6 +22,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + [[package]] name = "anymap2" version = "0.13.0" @@ -153,6 +168,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "num-integer", + "num-traits", + "winapi", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -162,6 +189,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cron" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff76b51e4c068c52bfd2866e1567bee7c567ae8f24ada09fd4307019e25eab7" +dependencies = [ + "chrono", + "nom", + "once_cell", +] + [[package]] name = "darling" version = "0.14.4" @@ -437,6 +481,15 @@ dependencies = [ "ahash", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "http" version = "0.2.9" @@ -508,6 +561,29 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -627,8 +703,12 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" name = "matrix-federation-notifier" version = "0.1.0" dependencies = [ + "anyhow", + "js_int", "matrix-sdk", + "serde", "tokio", + "tokio-cron-scheduler", ] [[package]] @@ -637,6 +717,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbeafb4809f33f377165f2fbcf10e0613053ad206762194c3050a727fd3abcb2" dependencies = [ + "anyhow", "anymap2", "async-once-cell", "async-stream", @@ -717,6 +798,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "0.8.6" @@ -729,6 +816,56 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 1.0.109", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg 1.1.0", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg 1.1.0", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -746,6 +883,16 @@ dependencies = [ "parking_lot_core 0.8.6", ] +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.7", +] + [[package]] name = "parking_lot_core" version = "0.8.6" @@ -1281,6 +1428,15 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.8" @@ -1396,11 +1552,41 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", + "parking_lot 0.12.1", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-cron-scheduler" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2c1fd54a857b29c6cd1846f31903d0ae8e28175615c14a277aed45c58d8e27" +dependencies = [ + "chrono", + "cron", + "num-derive", + "num-traits", + "tokio", + "tracing", + "uuid 1.3.3", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", +] + [[package]] name = "tokio-rustls" version = "0.24.0" @@ -1653,7 +1839,7 @@ checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" dependencies = [ "futures", "js-sys", - "parking_lot", + "parking_lot 0.11.2", "pin-utils", "wasm-bindgen", "wasm-bindgen-futures", @@ -1717,6 +1903,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index bf6b2e7..ea3b820 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -matrix-sdk = { version = "0.6.2", default-features = false, features = ["rustls-tls"] } -tokio = "1.28.1" +anyhow = "1.0.71" +js_int = { version = "0.2.2", features = ["serde"] } +matrix-sdk = { version = "0.6.2", default-features = false, features = ["rustls-tls", "anyhow"] } +serde = { version = "1.0.163", features = ["derive"] } +tokio = { version = "1.28.1", features = ["full"] } +tokio-cron-scheduler = "0.9.4" diff --git a/src/main.rs b/src/main.rs index e7a11a9..65a6a0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,119 @@ -fn main() { - println!("Hello, world!"); +// Parts of this file was lifted from https://github.com/matrix-org/matrix-rust-sdk/commit/3db90fbe026c222167000fcfdee8b35b44ae5694 +// And are thus licensed under Apache + +use matrix_sdk::{ + config::SyncSettings, + room::Room, + ruma::events::room::message::{MessageType, OriginalSyncRoomMessageEvent}, + ruma::events::{macros::EventContent, room::member::StrippedRoomMemberEvent}, + Client, +}; +use serde::{Deserialize, Serialize}; +use std::{env, process::exit}; +use tokio::time::{sleep, Duration}; + +mod ping; + +async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) { + if let Room::Joined(room) = room { + let MessageType::Text(text_content) = &event.content.msgtype else { + return; + }; + + if text_content.body.starts_with("!ping ") { + ping::ping(event, room).await; + } + } +} + +async fn on_stripped_state_member( + room_member: StrippedRoomMemberEvent, + client: Client, + room: Room, +) { + if room_member.state_key != client.user_id().unwrap() { + return; + } + + if let Room::Invited(room) = room { + tokio::spawn(async move { + println!("Autojoining room {}", room.room_id()); + let mut delay = 2; + + while let Err(err) = room.accept_invitation().await { + // retry autojoin due to synapse sending invites, before the + // invited user can join for more information see + // https://github.com/matrix-org/synapse/issues/4345 + eprintln!( + "Failed to join room {} ({err:?}), retrying in {delay}s", + room.room_id() + ); + + sleep(Duration::from_secs(delay)).await; + delay *= 2; + + if delay > 3600 { + eprintln!("Can't join room {} ({err:?})", room.room_id()); + break; + } + } + println!("Successfully joined room {}", room.room_id()); + }); + } +} + +async fn login_and_sync( + homeserver_url: String, + username: String, + password: String, +) -> anyhow::Result<()> { + // Note that when encryption is enabled, you should use a persistent store to be + // able to restore the session with a working encryption setup. + // See the `persist_session` example. + let client = Client::builder() + .homeserver_url(homeserver_url) + .build() + .await + .unwrap(); + client + .login_username(&username, &password) + .initial_device_display_name("ping bot") + .send() + .await?; + + println!("logged in as {username}"); + + // An initial sync to set up state and so our bot doesn't respond to old + // messages. + let response = client.sync_once(SyncSettings::default()).await.unwrap(); + + println!("Finished initial sync"); + + client.add_event_handler(on_room_message); + client.add_event_handler(on_stripped_state_member); + + // since we called `sync_once` before we entered our sync loop we must pass + // that sync token to `sync` + let settings = SyncSettings::default().token(response.next_batch); + + client.sync(settings).await?; + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let (homeserver_url, username, password) = + match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) { + (Some(a), Some(b), Some(c)) => (a, b, c), + _ => { + eprintln!( + "Usage: {} ", + env::args().next().unwrap() + ); + exit(1) + } + }; + + login_and_sync(homeserver_url, username, password).await?; + Ok(()) } diff --git a/src/ping.rs b/src/ping.rs new file mode 100644 index 0000000..5edfb1c --- /dev/null +++ b/src/ping.rs @@ -0,0 +1,97 @@ +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use matrix_sdk::{ + room::Joined, + ruma::{ + events::room::message::OriginalSyncRoomMessageEvent, exports::ruma_macros::EventContent, + OwnedEventId, UInt, + }, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "rel_type", rename = "xyz.maubot.pong")] +struct PongRelation { + event_id: OwnedEventId, + from: String, + ms: UInt, +} + +impl PongRelation { + fn new(event: &OriginalSyncRoomMessageEvent, diff: &Duration) -> Self { + let millis = diff.as_millis(); + let clamped_ms: u64 = if millis < js_int::MAX_SAFE_UINT as u128 { + millis as u64 + } else { + js_int::MAX_SAFE_UINT + }; + PongRelation { + event_id: event.event_id.to_owned(), + from: event.sender.server_name().to_string(), + ms: UInt::new_saturating(clamped_ms), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct PongMixin { + from: String, + ms: UInt, + ping: OwnedEventId, +} + +impl PongMixin { + fn new(event: &OriginalSyncRoomMessageEvent, diff: &Duration) -> Self { + let millis = diff.as_millis(); + let clamped_ms: u64 = if millis < js_int::MAX_SAFE_UINT as u128 { + millis as u64 + } else { + js_int::MAX_SAFE_UINT + }; + PongMixin { + ping: event.event_id.to_owned(), + from: event.sender.server_name().to_string(), + ms: UInt::new_saturating(clamped_ms), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[ruma_event(type = "m.room.message", kind = MessageLike)] +struct AckEventContent { + body: String, + msgtype: String, + #[serde(rename = "m.relates_to")] + relates_to: PongRelation, + pong: PongMixin, +} + +impl AckEventContent { + fn new(event: &OriginalSyncRoomMessageEvent, diff: &Duration) -> Self { + let plain = format!("ping took {:?} to arrive", &diff); + + Self { + body: plain, + msgtype: "m.notice".to_string(), + relates_to: PongRelation::new(&event, &diff), + pong: PongMixin::new(&event, &diff), + } + } +} + +pub async fn ping(event: OriginalSyncRoomMessageEvent, room: Joined) { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("timetravel"); + let then = event + .origin_server_ts + .to_system_time() + .expect("timestamp was not a real time") + .duration_since(UNIX_EPOCH) + .expect("timetravel"); + let diff = now - then; + + let content = AckEventContent::new(&event, &diff); + + room.send(content, None).await.unwrap(); +}