Files
matrix-rust-sdk/examples/getting_started/src/main.rs
T
Benjamin Kampmann e8278b5d68 feat: add Result return value to sync_* (#1037)
Inspired by the changes in #1013 I was thinking about the use case for `sync_*` and how we handle error cases. Most notably while we give the callback the option to stop the loop, we don't really give an indication to the outside, how to interpret that cancellation: was there a failure? should we restart?

Take e.g. a connectivity issue on the wire, we'd constantly loop and just `warn`, what you might or might not see. Even if you handle that in the `sync_with_result_callback` and thus break the loop, the outer caller now still doesn't know whether everything is honky dory or whether they should restart. 

This Changes reworks that area by having all the `sync` return `Result<(), Error>`, where `()` means it was ended by the inner callback (which in `sync()` never occurs) or `Error` is the error either the inner `result_callback` found or the that was coming from the `send` in the first place. Thus allowing us to e.g. back down to sync as it was a dead wire or restart it if there was only a temporary problem. Making all that a just a bit more "rust-y".
2022-09-26 17:14:15 +02:00

183 lines
7.1 KiB
Rust

///
/// 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 -- <homeserver_url> <user> <password>`
///
/// 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: {} <homeserver_url> <username> <password>",
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);
// 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);
// 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 {
// The event handlers are called before the next sync begins, but
// methods that change the state of a room (joining, leaving a room)
// wait for the sync to return the new room state so we need to spawn
// a new task for them.
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());
});
}
}
// 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");
}
}
}