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