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