1///
2/// This is an example showcasing how to build a very simple bot with custom
3/// events using the matrix-sdk. To try it, you need a rust build setup, then
4/// you can run: `cargo run -p example-custom-events -- <homeserver_url> <user>
5/// <password>`
6///
7/// Use a second client to open a DM to your bot or invite them into some room.
8/// You should see it automatically join. Then post `!ping` and observe the log
9/// of the bot. You will see that it sends the `Ping` event and upon receiving
10/// it responds with the `Ack` event send to the room. You won't see that in
11/// most regular clients, unless you activate showing of unknown events.
12use std::{
13 env,
14 process::exit,
15 sync::{
16 atomic::{AtomicU64, Ordering},
17 Arc,
18 },
19};
2021use matrix_sdk::{
22 config::SyncSettings,
23 event_handler::Ctx,
24 ruma::{
25 events::{
26 macros::EventContent,
27 room::{
28 member::StrippedRoomMemberEvent,
29 message::{MessageType, OriginalSyncRoomMessageEvent},
30 },
31 },
32 OwnedEventId,
33 },
34 Client, Room, RoomState,
35};
36use serde::{Deserialize, Serialize};
37use tokio::time::{sleep, Duration};
3839// We use ruma to define our custom events. Just declare the events content
40// by deriving from `EventContent` and define `ruma_events` for the metadata
4142#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
43#[ruma_event(type = "rs.matrix-sdk.example.ping", kind = MessageLike)]
44pub struct PingEventContent {}
4546#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
47#[ruma_event(type = "rs.matrix-sdk.example.ack", kind = MessageLike)]
48pub struct AckEventContent {
49// the event ID of the ping.
50ping_id: OwnedEventId,
51}
5253// We're going to create a small struct which will count how many times we have
54// been pinged.
55#[derive(Debug, Default, Clone)]
56pub struct CustomContext {
57 ping_counter: Arc<AtomicU64>,
58}
5960// Deriving `EventContent` generates a few types and aliases,
61// like wrapping the content into full-blown events: for `PingEventContent` this
62// generates us `PingEvent` and `SyncPingEvent`, which have redaction support
63// and contain all the other event-metadata like event_id and room_id. We will
64// use that for `on_ping_event`.
6566// we want to start the ping-ack-flow on "!ping" messages.
67async fn on_regular_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
68if room.state() != RoomState::Joined {
69return;
70 }
71let MessageType::Text(text_content) = event.content.msgtype else { return };
7273if text_content.body.contains("!ping") {
74let content = PingEventContent {};
7576println!("sending ping");
77 room.send(content).await.unwrap();
78println!("ping sent");
79 }
80}
8182// call this on any PingEvent we receive
83async fn on_ping_event(event: SyncPingEvent, room: Room, context: Ctx<CustomContext>) {
84if room.state() != RoomState::Joined {
85return;
86 }
8788let event_id = event.event_id().to_owned();
89let ping_number = context.ping_counter.fetch_add(1, Ordering::SeqCst);
9091// Send an ack with the event_id of the ping, as our 'protocol' demands
92let content = AckEventContent { ping_id: event_id };
93println!("sending ack for ping no {ping_number}");
94 room.send(content).await.unwrap();
9596println!("ack sent");
97}
9899// once logged in, this is called where we configure the handlers
100// and run the client
101async fn sync_loop(client: Client) -> anyhow::Result<()> {
102// invite acceptance as in the getting-started-client
103client.add_event_handler(on_stripped_state_member);
104let response = client.sync_once(SyncSettings::default()).await.unwrap();
105106// our customisation:
107 // - send `PingEvent` on `!ping` in any room
108client.add_event_handler(on_regular_room_message);
109// - send `AckEvent` on `PingEvent` in any room
110client.add_event_handler(on_ping_event);
111// Add our context so we can increment and print the ping count.
112client.add_event_handler_context(CustomContext::default());
113114let settings = SyncSettings::default().token(response.next_batch);
115 client.sync(settings).await?; // this essentially loops until we kill the bot
116117Ok(())
118}
119120// ------ below is mainly like the getting-started example, see that for docs.
121122async fn login_and_sync(
123 homeserver_url: String,
124 username: &str,
125 password: &str,
126) -> anyhow::Result<()> {
127let client = Client::builder().homeserver_url(homeserver_url).build().await?;
128 client
129 .matrix_auth()
130 .login_username(username, password)
131 .initial_device_display_name("getting started bot")
132 .await?;
133134// it worked!
135println!("logged in as {username}");
136 sync_loop(client).await
137}
138139// Whenever we see a new stripped room member event, we've asked our client to
140// call this function. So what exactly are we doing then?
141async fn on_stripped_state_member(
142 room_member: StrippedRoomMemberEvent,
143 client: Client,
144 room: Room,
145) {
146if room_member.state_key != client.user_id().unwrap() {
147// the invite we've seen isn't for us, but for someone else. ignore
148return;
149 }
150151 tokio::spawn(async move {
152println!("Autojoining room {}", room.room_id());
153let mut delay = 2;
154155while let Err(err) = room.join().await {
156// retry autojoin due to synapse sending invites, before the
157 // invited user can join for more information see
158 // https://github.com/matrix-org/synapse/issues/4345
159eprintln!("Failed to join room {} ({err:?}), retrying in {delay}s", room.room_id());
160161 sleep(Duration::from_secs(delay)).await;
162 delay *= 2;
163164if delay > 3600 {
165eprintln!("Can't join room {} ({err:?})", room.room_id());
166break;
167 }
168 }
169170println!("Successfully joined room {}", room.room_id());
171 });
172}
173174#[tokio::main]
175async fn main() -> anyhow::Result<()> {
176// set up some simple stderr logging. You can configure it by changing the env
177 // var `RUST_LOG`
178tracing_subscriber::fmt::init();
179180// parse the command line for homeserver, username and password
181let (homeserver_url, username, password) =
182match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
183 (Some(a), Some(b), Some(c)) => (a, b, c),
184_ => {
185eprintln!(
186"Usage: {} <homeserver_url> <username> <password>",
187 env::args().next().unwrap()
188 );
189// exist if missing
190exit(1)
191 }
192 };
193194// our actual runner
195login_and_sync(homeserver_url, &username, &password).await?;
196Ok(())
197}