Skip to main content

example_custom_events/
main.rs

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