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