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