use tokio::time::{delay_for, Duration};
pub mod handler;
use crate::handler::{HandleResult, MessageHandler};
use matrix_sdk::{
self, async_trait,
events::{
room::{member::MemberEventContent, message::MessageEventContent},
StrippedStateEvent, SyncMessageEvent,
},
Client, ClientConfig, EventEmitter, JsonStore, SyncRoom, SyncSettings,
};
use url::Url;
pub struct MatrixBot {
pub client: Client,
handlers: Vec<Box<dyn MessageHandler + Send + Sync>>,
}
impl MatrixBot {
pub async fn new(
homeserver: &str,
username: &str,
password: &str,
) -> Result<Self, matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let mut home = dirs::home_dir().expect("no home directory found");
home.push("testbot");
let store = JsonStore::open(&home)?;
let client_config = ClientConfig::new().state_store(Box::new(store));
let homeserver = Url::parse(&homeserver).expect("Couldn't parse the homeserver URL");
let client = Client::new_with_config(homeserver, client_config).unwrap();
client
.login(&username, &password, None, Some("testbot"))
.await
.unwrap();
println!("logged in as {}", username);
Ok(Self {
client,
handlers: vec![],
})
}
pub async fn run(self) -> Result<(), matrix_sdk::Error> {
let mut client = self.client.clone();
client.sync_once(SyncSettings::default()).await.unwrap();
client.add_event_emitter(Box::new(self)).await;
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
client.sync(settings).await;
Ok(())
}
pub fn add_handler<M>(&mut self, handler: M)
where
M: handler::MessageHandler + 'static + Send + Sync,
{
self.handlers.push(Box::new(handler));
}
}
#[async_trait]
impl EventEmitter for MatrixBot {
async fn on_stripped_state_member(
&self,
room: SyncRoom,
room_member: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
if room_member.state_key != self.client.user_id().await.unwrap() {
return;
}
if let SyncRoom::Invited(room) = room {
let room = room.read().await;
println!("Autojoining room {}", room.room_id);
let mut delay: u64 = 2;
while let Err(err) = self.client.join_room_by_id(&room.room_id).await {
eprintln!(
"Failed to join room {} ({:?}), retrying in {}s",
room.room_id, err, delay
);
delay_for(Duration::from_secs(delay)).await;
delay *= 2;
if delay > 3600 {
eprintln!("Can't join room {} ({:?})", room.room_id, err);
break;
}
}
println!("Successfully joined room {}", room.room_id);
}
}
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
for handler in self.handlers.iter() {
let val = handler.handle_message(&self, &room, event).await;
match val {
HandleResult::Continue => continue,
HandleResult::Stop => break,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use matrix_sdk_test::test_json;
use mockito::mock;
#[tokio::test]
async fn login() {
let homeserver = mockito::server_url();
let _m = mock("POST", "/_matrix/client/r0/login")
.with_status(200)
.with_body(test_json::LOGIN.to_string())
.create();
let bot = MatrixBot::new(&homeserver, "user", "password")
.await
.unwrap();
let logged_in = bot.client.logged_in().await;
assert!(logged_in, "Bot should be logged in");
}
}