Skip to main content

example_getting_started/
main.rs

1#![recursion_limit = "256"]
2
3///
4///  This is an example showcasing how to build a very simple bot using the
5/// matrix-sdk. To try it, you need a rust build setup, then you can run:
6/// `cargo run -p example-getting-started -- <homeserver_url> <user> <password>`
7///
8/// Use a second client to open a DM to your bot or invite them into some room.
9/// You should see it automatically join. Then post `!party` to see the client
10/// in action.
11///
12/// Below the code has a lot of inline documentation to help you understand the
13/// various parts and what they do
14// The imports we need
15use std::{env, process::exit};
16
17use matrix_sdk::{
18    Client, Room, RoomState,
19    config::SyncSettings,
20    ruma::events::room::{
21        member::StrippedRoomMemberEvent,
22        message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
23    },
24};
25use tokio::time::{Duration, sleep};
26
27/// This is the starting point of the app. `main` is called by rust binaries to
28/// run the program in this case, we use tokio (a reactor) to allow us to use
29/// an `async` function run.
30#[tokio::main]
31async fn main() -> anyhow::Result<()> {
32    // set up some simple stderr logging. You can configure it by changing the
33    // env var `RUST_LOG`
34    tracing_subscriber::fmt::init();
35
36    // parse the command line for homeserver, username and password
37    let (Some(homeserver_url), Some(username), Some(password)) =
38        (env::args().nth(1), env::args().nth(2), env::args().nth(3))
39    else {
40        eprintln!("Usage: {} <homeserver_url> <username> <password>", env::args().next().unwrap());
41        // exit if missing
42        exit(1)
43    };
44
45    // our actual runner
46    login_and_sync(homeserver_url, &username, &password).await?;
47    Ok(())
48}
49
50// The core sync loop we have running.
51async fn login_and_sync(
52    homeserver_url: String,
53    username: &str,
54    password: &str,
55) -> anyhow::Result<()> {
56    // First, we set up the client.
57
58    // Note that when encryption is enabled, you should use a persistent store
59    // to be able to restore the session with a working encryption setup.
60    // See the `persist_session` example.
61    let client = Client::builder()
62        // We use the convenient client builder to set our custom homeserver URL on it.
63        .homeserver_url(homeserver_url)
64        .build()
65        .await?;
66
67    // Then let's log that client in
68    client
69        .matrix_auth()
70        .login_username(username, password)
71        .initial_device_display_name("getting started bot")
72        .await?;
73
74    // It worked!
75    println!("logged in as {username}");
76
77    // Now, we want our client to react to invites. Invites sent us stripped
78    // member state events so we want to react to them. We add the event
79    // handler before the sync, so this happens also for older messages. All
80    // rooms we've already entered won't have stripped states anymore and
81    // thus won't fire
82    client.add_event_handler(on_stripped_state_member);
83
84    // An initial sync to set up state and so our bot doesn't respond to old
85    // messages. If the `StateStore` finds saved state in the location given the
86    // initial sync will be skipped in favor of loading state from the store
87    let sync_token = client.sync_once(SyncSettings::default()).await.unwrap().next_batch;
88
89    // now that we've synced, let's attach a handler for incoming room messages,
90    // so we can react on it
91    client.add_event_handler(on_room_message);
92
93    // since we called `sync_once` before we entered our sync loop we must pass
94    // that sync token to `sync`
95    let settings = SyncSettings::default().token(sync_token);
96    // this keeps state from the server streaming in to the bot via the
97    // EventHandler trait
98    client.sync(settings).await?; // this essentially loops until we kill the bot
99
100    Ok(())
101}
102
103// Whenever we see a new stripped room member event, we've asked our client to
104// call this function. So what exactly are we doing then?
105async fn on_stripped_state_member(
106    room_member: StrippedRoomMemberEvent,
107    client: Client,
108    room: Room,
109) {
110    if room_member.state_key != client.user_id().unwrap() {
111        // the invite we've seen isn't for us, but for someone else. ignore
112        return;
113    }
114
115    // The event handlers are called before the next sync begins, but
116    // methods that change the state of a room (joining, leaving a room)
117    // wait for the sync to return the new room state so we need to spawn
118    // a new task for them.
119    tokio::spawn(async move {
120        println!("Autojoining room {}", room.room_id());
121        let mut delay = 2;
122
123        while let Err(err) = room.join().await {
124            // retry autojoin due to synapse sending invites, before the
125            // invited user can join for more information see
126            // https://github.com/matrix-org/synapse/issues/4345
127            eprintln!("Failed to join room {} ({err:?}), retrying in {delay}s", room.room_id());
128
129            sleep(Duration::from_secs(delay)).await;
130            delay *= 2;
131
132            if delay > 3600 {
133                eprintln!("Can't join room {} ({err:?})", room.room_id());
134                break;
135            }
136        }
137        println!("Successfully joined room {}", room.room_id());
138    });
139}
140
141// This fn is called whenever we see a new room message event. You notice that
142// the difference between this and the other function that we've given to the
143// handler lies only in their input parameters. However, that is enough for the
144// rust-sdk to figure out which one to call and only do so, when the parameters
145// are available.
146async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
147    // First, we need to unpack the message: We only want messages from rooms we
148    // are still in and that are regular text messages - ignoring everything
149    // else.
150    if room.state() != RoomState::Joined {
151        return;
152    }
153    let MessageType::Text(text_content) = event.content.msgtype else { return };
154
155    // here comes the actual "logic": when the bot see's a `!party` in the
156    // message, it responds
157    if text_content.body.contains("!party") {
158        let content = RoomMessageEventContent::text_plain("🎉🎊🥳 let's PARTY!! 🥳🎊🎉");
159
160        println!("sending");
161
162        // send our message to the room we found the "!party" command in
163        room.send(content).await.unwrap();
164
165        println!("message sent");
166    }
167}