From 9538596fbbc208dd9361d8dfa79e89bc993d830a Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 3 Aug 2022 13:04:28 +0200 Subject: [PATCH] doc: Add getting-started example autojoin & command bot with plenty of source docs --- examples/getting_started/Cargo.toml | 14 +++ examples/getting_started/src/main.rs | 178 +++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 examples/getting_started/Cargo.toml create mode 100644 examples/getting_started/src/main.rs diff --git a/examples/getting_started/Cargo.toml b/examples/getting_started/Cargo.toml new file mode 100644 index 000000000..4f23dbdf6 --- /dev/null +++ b/examples/getting_started/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "example-getting-started" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +anyhow = "1" +dirs = "4.0.0" +tokio = { version = "1.20.1", features = ["full"] } +tracing-subscriber = "0.3.15" + +[dependencies.matrix-sdk] +path = "../../crates/matrix-sdk" diff --git a/examples/getting_started/src/main.rs b/examples/getting_started/src/main.rs new file mode 100644 index 000000000..d7718f288 --- /dev/null +++ b/examples/getting_started/src/main.rs @@ -0,0 +1,178 @@ +/// +/// This is an example showcasing how to build a very simple bot using the +/// matrix-sdk. +/// To try it, you need a rust build setup, then you can run: +/// `cargo run -p example-getting-started -- +/// ` +/// +/// Use a second client to open a DM to your bot or invite them into some room. +/// You should see it automatically join. Then post `!party` to see the client +/// in action. +/// +/// Below the code has a lot of inline documentation to help you understand the +/// various parts and what they do +// The imports we need +use std::{env, process::exit}; + +use matrix_sdk::{ + config::SyncSettings, + room::Room, + ruma::events::room::{ + member::StrippedRoomMemberEvent, + message::{ + MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent, + TextMessageEventContent, + }, + }, + Client, +}; +use tokio::time::{sleep, Duration}; + +/// This is the starting point of the app. `main` is called by rust binaries to +/// run the program in this case, we use tokio (a reactor) to allow us to use +/// an `async` function run. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // set up some simple stderr logging. You can configure it by changing the env + // var `RUST_LOG` + tracing_subscriber::fmt::init(); + + // parse the command line for homeserver, username and password + 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() + ); + // exist if missing + exit(1) + } + }; + + // our actual runner + login_and_sync(homeserver_url, &username, &password).await?; + Ok(()) +} + +// The core sync loop we have running. +async fn login_and_sync( + homeserver_url: String, + username: &str, + password: &str, +) -> anyhow::Result<()> { + // first, we set up the client. We use the convenient client builder to set our + // custom homeserver URL on it + #[allow(unused_mut)] + let mut client_builder = Client::builder().homeserver_url(homeserver_url); + + // Matrix-SDK has support for pluggable, configurable state and crypto-store + // support we use the default sled-store (enabled by default on native + // architectures), to configure a local cache and store for our crypto keys + let home = dirs::data_dir().expect("no home directory found").join("getting_started"); + client_builder = client_builder.sled_store(home, None)?; + + // alright, let's make that into a client + let client = client_builder.build().await?; + + // then let's log that client in + client + .login_username(username, password) + .initial_device_display_name("getting started bot") + .send() + .await?; + + // it worked! + println!("logged in as {username}"); + + // Now, we want our client to react to invites. Invites sent us stripped member + // state events so we want to react to them. We add the event handler before + // the sync, so this happens also for older messages. All rooms we've + // already entered won't have stripped states anymore and thus won't fire + client.add_event_handler(on_stripped_state_member).await; + + // An initial sync to set up state and so our bot doesn't respond to old + // messages. If the `StateStore` finds saved state in the location given the + // initial sync will be skipped in favor of loading state from the store + client.sync_once(SyncSettings::default()).await.unwrap(); + + // now that we've synced, let's attach a handler for incoming room messages, so + // we can react on it + client.add_event_handler(on_room_message).await; + + // since we called `sync_once` before we entered our sync loop we must pass + // that sync token to `sync` + let settings = SyncSettings::default().token(client.sync_token().await.unwrap()); + // this keeps state from the server streaming in to the bot via the + // EventHandler trait + client.sync(settings).await; // this essentially loops until we kill the bot + + Ok(()) +} + +// Whenever we see a new stripped room member event, we've asked our client to +// call this function. So what exactly are we doing then? +async fn on_stripped_state_member( + room_member: StrippedRoomMemberEvent, + client: Client, + room: Room, +) { + if room_member.state_key != client.user_id().unwrap() { + // the invite we've seen isn't for us, but for someone else. ignore + return; + } + + // looks like the room is an invited room, let's attempt to join then + if let Room::Invited(room) = room { + 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()); + } +} + +// This fn is called whenever we see a new room message event. You notice that +// the difference between this and the other function that we've given to the +// handler lies only in their input parameters. However, that is enough for the +// rust-sdk to figure out which one to call one and only do so, when +// the parameters are available. +async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) { + // First, we need to unpack the message: We only want messages from rooms we are + // still in and that are regular text messages - ignoring everything else. + if let Room::Joined(room) = room { + let msg_body = match event.content.msgtype { + MessageType::Text(TextMessageEventContent { body, .. }) => body, + _ => return, + }; + + // here comes the actual "logic": when the bot see's a `!party` in the message, + // it responds + if msg_body.contains("!party") { + let content = RoomMessageEventContent::text_plain("🎉🎊🥳 let's PARTY!! 🥳🎊🎉"); + + println!("sending"); + + // send our message to the room we found the "!party" command in + // the last parameter is an optional transaction id which we don't + // care about. + room.send(content, None).await.unwrap(); + + println!("message sent"); + } + } +}