96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 1 | use macltok::{ |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 2 | EngineConfig, Event, PasswordLogin, Retain, RoomMembership, Session, StoreConfig, |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 3 | TimelineContent, TimelineDiff, TimelineId, TimelineItem, imbl::Vector, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 4 | }; |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 5 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 6 | #[tokio::main] |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 7 | async fn main() -> Result<(), Box<dyn std::error::Error>> { |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 8 | let store_path = std::path::PathBuf::from(required("MATRIX_STORE_PATH")?); |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 9 | let passphrase = required("MATRIX_STORE_PASSPHRASE")?; |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 10 | // a real client keeps this somewhere safer, it holds the access token |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 11 | let record_path = store_path.join("session.json"); |
62fe32759529 | atagen | 2026-09-26 08:50 UTC | 12 | let store = StoreConfig::sqlite(&store_path, &passphrase); |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 13 | let session = match std::fs::read(&record_path) { |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 14 | Ok(saved) => Session::restore(serde_json::from_slice(&saved)?, store).await?, |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 15 | Err(_) => { |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 16 | let session = Session::login_password( |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 17 | PasswordLogin::new( |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 18 | required("MATRIX_HOMESERVER")?, |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 19 | required("MATRIX_USERNAME")?, |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 20 | required("MATRIX_PASSWORD")?, |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 21 | ) |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 22 | .with_device_display_name("macltok headless example"), |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 23 | store, |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 24 | ) |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 25 | .await?; |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 26 | std::fs::write(&record_path, serde_json::to_vec(&session.record())?)?; |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 27 | session |
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 => { |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 78 | std::fs::write(&record_path, serde_json::to_vec(&handle.session_record()?)?)?; |
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 | } |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 91 | match running.await??.retain() { |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 92 | Retain::RecordAndStore => {} |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 93 | Retain::Store => std::fs::remove_file(&record_path)?, |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 94 | Retain::Nothing => std::fs::remove_dir_all(&store_path)?, |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 95 | } |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 96 | Ok(()) |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 97 | } |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 98 | |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 99 | fn print_messages(items: &Vector<TimelineItem>) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 100 | for item in items { |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 101 | if let TimelineItem::Event(event) = item |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 102 | && let TimelineContent::Message(message) = &event.content |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 103 | { |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 104 | println!("{} {}", event.sender.name(), message.body); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 105 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 106 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 107 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 108 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 109 | fn required(name: &str) -> Result<String, Box<dyn std::error::Error>> { |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 110 | std::env::var(name).map_err(|_| format!("{name} is required").into()) |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 111 | } |