96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 1 | use macltok::{ |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 2 | EncryptedSessionStore, EngineConfig, Event, PasswordLogin, Restore, Retain, RoomMembership, |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 3 | Session, SessionStore, StoreConfig, TimelineContent, TimelineDiff, TimelineId, TimelineItem, |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 4 | imbl::Vector, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 5 | }; |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 6 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 7 | #[tokio::main] |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 8 | async fn main() -> Result<(), Box<dyn std::error::Error>> { |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 9 | let store_path = std::path::PathBuf::from(required("MATRIX_STORE_PATH")?); |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 10 | let passphrase = required("MATRIX_STORE_PASSPHRASE")?; |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 11 | let sessions = EncryptedSessionStore::file( |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 12 | store_path.with_extension("session"), |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 13 | StoreConfig::sqlite(&store_path, &passphrase), |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 14 | ); |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 15 | let session = match Session::restore(&sessions).await? { |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 16 | Some(Restore::Resumed(session)) => session, |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 17 | Some(Restore::SoftLoggedOut(soft)) => { |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 18 | Session::login_password(soft.password_login(required("MATRIX_PASSWORD")?), &sessions) |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 19 | .await? |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 20 | } |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 21 | None => { |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 22 | Session::login_password( |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 23 | PasswordLogin::new( |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 24 | required("MATRIX_HOMESERVER")?, |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 25 | required("MATRIX_USERNAME")?, |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 26 | required("MATRIX_PASSWORD")?, |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 27 | ) |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 28 | .with_device_display_name("macltok headless example"), |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 29 | &sessions, |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 30 | ) |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 31 | .await? |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 32 | } |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 33 | }; |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 34 | let (engine, handle, mut events) = session.build(EngineConfig::default()).await?; |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 35 | let running = tokio::spawn(engine.run()); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 36 | let mut opened: Option<TimelineId> = None; |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 37 | let mut items = Vector::new(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 38 | let mut stale = false; |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 39 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 40 | loop { |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 41 | tokio::select! { |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 42 | event = events.recv() => { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 43 | let Some(event) = event else { break }; |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 44 | match event { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 45 | Event::RoomsChanged if opened.is_none() => { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 46 | let Some(room) = handle |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 47 | .rooms() |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 48 | .await |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 49 | .into_iter() |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 50 | .find(|room| room.membership == RoomMembership::Joined) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 51 | else { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 52 | continue; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 53 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 54 | opened = Some(room.room_id.clone().into()); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 55 | // answers arrive on their own, the event loop keeps draining meanwhile |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 56 | let handle = handle.clone(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 57 | tokio::spawn(async move { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 58 | if let Err(error) = handle.open_room(room.room_id).await { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 59 | eprintln!("could not open {error}"); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 60 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 61 | }); |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 62 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 63 | Event::TimelineReset { timeline_id, items: snapshot } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 64 | if opened.as_ref() == Some(&timeline_id) => |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 65 | { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 66 | items = snapshot; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 67 | stale = false; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 68 | print_messages(&items); |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 69 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 70 | Event::TimelineChanged { timeline_id, diffs } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 71 | if !stale && opened.as_ref() == Some(&timeline_id) => |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 72 | { |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 73 | if TimelineDiff::try_apply_all(diffs, &mut items).is_err() { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 74 | stale = true; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 75 | let handle = handle.clone(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 76 | tokio::spawn(async move { handle.resync(timeline_id).await }); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 77 | continue; |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 78 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 79 | print_messages(&items); |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 80 | } |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 81 | Event::SessionTokensChanged => { |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 82 | sessions.save(&handle.session_record()?).await?; |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 83 | } |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 84 | Event::Stopped => break, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 85 | _ => {} |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 86 | } |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 87 | } |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 88 | result = tokio::signal::ctrl_c() => { |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 89 | result?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 90 | let handle = handle.clone(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 91 | tokio::spawn(async move { handle.shutdown().await }); |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 92 | } |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 93 | } |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 94 | } |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 95 | let termination = running.await??; |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 96 | drop((handle, events)); |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 97 | match termination.retain() { |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 98 | Retain::RecordAndStore => {} |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 99 | Retain::Store => sessions.soft_log_out().await?, |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 100 | Retain::Nothing => sessions.discard().await?, |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 101 | } |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 102 | Ok(()) |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 103 | } |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 104 | |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 105 | fn print_messages(items: &Vector<TimelineItem>) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 106 | for item in items { |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 107 | if let TimelineItem::Event(event) = item |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 108 | && let TimelineContent::Message(message) = &event.content |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 109 | { |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 110 | println!("{} {}", event.sender.name(), message.body); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 111 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 112 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 113 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 114 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 115 | fn required(name: &str) -> Result<String, Box<dyn std::error::Error>> { |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 116 | std::env::var(name).map_err(|_| format!("{name} is required").into()) |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 117 | } |