use std::{env, process::exit}; use schmfy::schmfy; use rand::Rng; use matrix_sdk::{ config::SyncSettings, room::Room, ruma::events::{room::{ member::StrippedRoomMemberEvent, message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent, ForwardThread}, }, relation::Annotation, reaction::ReactionEventContent}, Client, }; use tokio::time::{sleep, Duration}; #[tokio::main] async fn main() -> anyhow::Result<()> { 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: {} ", env::args().next().unwrap() ); // exist if missing exit(1) } }; // our actual runner login_and_sync(homeserver_url, &username, &password).await?; Ok(()) } // The core sync loop we have running. async fn login_and_sync( homeserver_url: String, username: &str, password: &str, ) -> anyhow::Result<()> { // First, we set up the client. // Note that when encryption is enabled, you should use a persistent store to be // able to restore the session with a working encryption setup. // See the `persist_session` example. let client = Client::builder() // We use the convenient client builder to set our custom homeserver URL on it. .homeserver_url(homeserver_url) .build() .await?; // Then let's log that client in client .matrix_auth() .login_username(username, password) .initial_device_display_name("getting started bot") .await?; // It worked! println!("logged in as {username}"); // Now, we want our client to react to invites. Invites sent us stripped member // state events so we want to react to them. We add the event handler before // the sync, so this happens also for older messages. All rooms we've // already entered won't have stripped states anymore and thus won't fire client.add_event_handler(on_stripped_state_member); // An initial sync to set up state and so our bot doesn't respond to old // messages. If the `StateStore` finds saved state in the location given the // initial sync will be skipped in favor of loading state from the store let sync_token = client.sync_once(SyncSettings::default()).await.unwrap().next_batch; // now that we've synced, let's attach a handler for incoming room messages, so // we can react on it client.add_event_handler(on_room_message); // since we called `sync_once` before we entered our sync loop we must pass // that sync token to `sync` let settings = SyncSettings::default().token(sync_token); // this keeps state from the server streaming in to the bot via the // EventHandler trait client.sync(settings).await?; // this essentially loops until we kill the bot Ok(()) } // 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; } // looks like the room is an invited room, let's attempt to join then if let Room::Invited(room) = room { // The event handlers are called before the next sync begins, but // methods that change the state of a room (joining, leaving a room) // wait for the sync to return the new room state so we need to spawn // a new task for them. tokio::spawn(async move { println!("Autojoining room {}", room.room_id()); 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 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()); }); } } fn schmfy_strip_reply(txt: &str) -> String { txt.split("\n") .filter(|line| line.starts_with("> ")) .map(|line| { let shortened = line.split(">") .enumerate() .filter(|(i,_)| *i > 1) .map(|(_,txt)| txt) .collect::>() .join(">"); schmfy(shortened.as_str()) }) .collect::>() .join("\n") } // This fn is called whenever we see a new room message event. You notice that // the difference between this and the other function that we've given to the // handler lies only in their input parameters. However, that is enough for the // rust-sdk to figure out which one to call one and only do so, when // the parameters are available. async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) { // First, we need to unpack the message: We only want messages from rooms we are // still in and that are regular text messages - ignoring everything else. let Room::Joined(room) = room else { return }; let MessageType::Text(text_content) = event.clone().content.msgtype else { return }; // full event for e.g. replies let full_event = event.clone().into_full_event(room.room_id().to_owned()); // here comes the actual "logic": when the bot see's a `!party` in the message, // it responds if text_content.body.contains("!party") { let content = RoomMessageEventContent::text_plain("🎉🎊🥳 let's PARTY!! 🥳🎊🎉"); println!("sending"); room.send(content, None).await.unwrap(); println!("message sent"); } if text_content.body.contains("!schmfy") && is_allowed_user(event.sender.as_str()) { match event.content.relates_to { Some(_) => { let plain = schmfy_strip_reply(text_content.body.as_str()); let formatted = match text_content.formatted { Some(formatted) => { schmfy_strip_reply(formatted.body.as_str()) }, None => { String::from("") } }; let content = RoomMessageEventContent::text_html(plain, formatted) .make_reply_to(&full_event, ForwardThread::Yes); room.send(content, None).await.unwrap(); }, None => { // react on invalid message let reaction = ReactionEventContent::new( Annotation::new( event.event_id.to_owned(), "WRONG".to_owned() ) ); room.send(reaction, None).await.unwrap(); } } } if is_allowed_room(room.name()) { if text_content.body.to_lowercase().contains("timo") { let reaction = ReactionEventContent::new( Annotation::new( event.event_id.to_owned(), "TIMO".to_owned() ) ); room.send(reaction, None).await.unwrap(); } if text_content.body.to_lowercase().contains("jan") { let reaction = ReactionEventContent::new( Annotation::new( event.event_id.to_owned(), "JAN".to_owned() ) ); room.send(reaction, None).await.unwrap(); } if text_content.body.to_lowercase().contains("fabian") { let reaction = ReactionEventContent::new( Annotation::new( event.event_id.to_owned(), "FABIAN".to_owned() ) ); room.send(reaction, None).await.unwrap(); } if text_content.body.to_lowercase().contains("second") || text_content.body.to_lowercase().contains("dennis") { let reaction = ReactionEventContent::new( Annotation::new( event.event_id.to_owned(), "SECOND".to_owned() ) ); room.send(reaction, None).await.unwrap(); } if event.sender.as_str().contains("conduit.rs") { let reaction = ReactionEventContent::new( Annotation::new( event.event_id.to_owned(), "⚡️".to_owned() ) ); room.send(reaction, None).await.unwrap(); } if text_content.body.contains("\\") { let reaction = ReactionEventContent::new( Annotation::new( event.event_id.to_owned(), text_content.body.replace("\\", "λ").replace("!lambda ","").to_owned() ) ); room.send(reaction, None).await.unwrap(); } if text_content.body.contains("!lambda") { let reaction = ReactionEventContent::new( Annotation::new( event.event_id.to_owned(), "FuPro".to_owned() ) ); room.send(reaction, None).await.unwrap(); } } if is_allowed_room(room.name()) && is_allowed_user(event.sender.as_str()){ if text_content.body.split_whitespace().count() < 2 { let reaction = ReactionEventContent::new( Annotation::new( event.event_id.to_owned(), schmfy(text_content.body.to_lowercase().as_str()) ) ); room.send(reaction, None).await.unwrap(); } else { /* let msg = text_content.body .split_terminator("\n") .map(|line| { line .split_whitespace() .map(|x| { if {let mut rng = rand::thread_rng(); rng.gen_range(0..200)} <= x.len()^2 { schmfy(x.to_lowercase().as_str()) } else { x.to_lowercase() } }) .collect::>() .join(" ") }) .collect::>() .join("\n"); let content = RoomMessageEventContent::text_plain(msg) .make_reply_to(&full_event, ForwardThread::Yes); room.send(content, None).await.unwrap(); */ } } } fn is_allowed_user(user: &str) -> bool { return !( user.contains("bot_") ) } fn is_allowed_room(name: Option) -> bool { let room_name = match name { Some(name) => { name }, _ => { String::from("") } }; if room_name.to_lowercase().contains("spam") || room_name.to_lowercase().contains("bot") { return true } false }