use matrix_sdk::{ config::SyncSettings, room::Room, ruma::{ events::room::{ member::StrippedRoomMemberEvent, message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent}, }, OwnedUserId, }, Client, }; use miette::{miette, Context, IntoDiagnostic, Result}; use dotenv::dotenv; use reqwest::Url; use serde_json::Value; use std::env; #[tokio::main] async fn main() -> Result<()> { dotenv().ok(); let hs = env::var("MATRIX_HOMESERVER") .into_diagnostic() .wrap_err("Variable: MATRIX_HOMESERVER")?; let username = env::var("MATRIX_USERNAME") .into_diagnostic() .wrap_err("Variable: MATRIX_USERNAME")?; let password = env::var("MATRIX_PASSWORD") .into_diagnostic() .wrap_err("Variable: MATRIX_PASSWORD")?; let hs_url = Url::parse(&hs).into_diagnostic()?; let client = Client::new(hs_url.clone()).await.into_diagnostic()?; client .login_username(&username, &password) .initial_device_display_name("RIL100 Bot") .send() .await .into_diagnostic()?; let response = client .sync_once(SyncSettings::default()) .await .into_diagnostic()?; println!("Logged in as RIL100 bot"); let user_id = client .user_id() .ok_or_else(|| return miette!("Client does not have a User Id"))? .to_owned(); client.add_event_handler(on_stripped_state_member); client.add_event_handler(move |ev, room| on_room_message(ev, room, user_id.clone())); let settings = SyncSettings::default().token(response.next_batch); client.sync(settings).await.into_diagnostic()?; Ok(()) } async fn on_room_message( event: OriginalSyncRoomMessageEvent, room: Room, current_user: OwnedUserId, ) -> Result<()> { // Make sure room is joined let Room::Joined(room) = room else { return Ok(()) }; // Check if message has text content let MessageType::Text(text) = event.clone().content.msgtype else { return Ok(()) }; // Do not reply to own messages if event.sender == current_user { return Ok(()); }; let user_uri = ¤t_user.matrix_to_uri().to_string(); let user_uri = urlencoding::decode(user_uri) .into_diagnostic()? .into_owned(); // Only reply to mentions if text.formatted.map_or(false, |v| { v.body.contains(&format!("")) }) { // Drop the mention let query = text .body .split_once(": ") .ok_or_else(|| { return miette!("Message both contains and doesn't contain a mention 🤨"); })? .1 .trim() .to_string(); let response = if query.chars().all(char::is_uppercase) { let body = reqwest::get(format!("https://v6.db.transport.rest/stations/{query}")) .await .into_diagnostic()? .json::() .await .into_diagnostic()?; let name = body .get("name") .and_then(|v| v.as_str()) .unwrap_or("unknown"); format!("{query} is {name}") } else { let body = reqwest::get(format!( "https://v6.db.transport.rest/stations?query={query}&results=1" )) .await .into_diagnostic()? .json::() .await .into_diagnostic()?; // Get first object field let body = body.as_object().and_then(|v| v.values().next()); let code = body .and_then(|v| v.get("ril100")) .and_then(|v| v.as_str()) .unwrap_or("unknown"); let station = body .and_then(|v| v.get("name")) .and_then(|v| v.as_str()) .unwrap_or(&query); format!("{station} is {code}") }; room.send( RoomMessageEventContent::text_html( response.replace("", "").replace("", ""), response, ) .make_reply_to(&event.into_full_event(room.room_id().into())), None, ) .await .into_diagnostic()?; }; Ok(()) } async fn on_stripped_state_member( room_member: StrippedRoomMemberEvent, client: Client, room: Room, ) -> Result<()> { if room_member.state_key != client .user_id() .ok_or_else(|| return miette!("Client does not have a User Id"))? { return Ok(()); } if let Room::Invited(room) = room { tokio::spawn(async move { let mut delay = 2; while let Err(err) = room.accept_invitation().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 miette!(format!( "Failed to join room {} ({err:?}), retrying in {delay}s", room.room_id() )); tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await; delay *= 2; if delay > 3600 { miette!(format!("Can't join room {} ({err:?})", room.room_id())); break; } } }); } Ok(()) }