atagen

macltok

public
Star0
Watch0
fe4133dbcd47e9263b8915f2406de5b2cab7e65a
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
examples/headless.rsRust111 lines4.5 KB
1
use macltok::{
2
    EngineConfig, Event, PasswordLogin, Retain, RoomMembership, Session, StoreConfig,
3
    TimelineContent, TimelineId, TimelineItem,
4
};
5
6
#[tokio::main]
7
async fn main() -> Result<(), Box<dyn std::error::Error>> {
8
    let store_path = std::path::PathBuf::from(required("MATRIX_STORE_PATH")?);
9
    let passphrase = required("MATRIX_STORE_PASSPHRASE")?;
10
    // a real client keeps this somewhere safer, it holds the access token
11
    let record_path = store_path.join("session.json");
12
    let store = StoreConfig::sqlite(&store_path, &passphrase);
13
    let session = match std::fs::read(&record_path) {
14
        Ok(saved) => Session::restore(serde_json::from_slice(&saved)?, store).await?,
15
        Err(_) => {
16
            let session = Session::login_password(
17
                PasswordLogin::new(
18
                    required("MATRIX_HOMESERVER")?,
19
                    required("MATRIX_USERNAME")?,
20
                    required("MATRIX_PASSWORD")?,
21
                )
22
                .with_device_display_name("macltok headless example"),
23
                store,
24
            )
25
            .await?;
26
            std::fs::write(&record_path, serde_json::to_vec(&session.record())?)?;
27
            session
28
        }
29
    };
30
    let (engine, handle, mut events) = session.build(EngineConfig::default()).await?;
31
    let running = tokio::spawn(engine.run());
32
    let mut opened: Option<TimelineId> = None;
33
    let mut items = Vec::new();
34
    let mut stale = false;
35
36
    loop {
37
        tokio::select! {
38
            event = events.recv() => {
39
                let Some(event) = event else { break };
40
                match event {
41
                    Event::RoomsChanged if opened.is_none() => {
42
                        let Some(room) = handle
43
                            .rooms()
44
                            .await
45
                            .into_iter()
46
                            .find(|room| room.membership == RoomMembership::Joined)
47
                        else {
48
                            continue;
49
                        };
50
                        opened = Some(room.room_id.clone().into());
51
                        // answers arrive on their own, the event loop keeps draining meanwhile
52
                        let handle = handle.clone();
53
                        tokio::spawn(async move {
54
                            if let Err(error) = handle.open_room(room.room_id).await {
55
                                eprintln!("could not open {error}");
56
                            }
57
                        });
58
                    }
59
                    Event::TimelineReset { timeline_id, items: snapshot }
60
                        if opened.as_ref() == Some(&timeline_id) =>
61
                    {
62
                        items = snapshot;
63
                        stale = false;
64
                        print_messages(&items);
65
                    }
66
                    Event::TimelineChanged { timeline_id, diffs }
67
                        if !stale && opened.as_ref() == Some(&timeline_id) =>
68
                    {
69
                        if diffs.into_iter().any(|diff| diff.try_apply(&mut items).is_err()) {
70
                            stale = true;
71
                            let handle = handle.clone();
72
                            tokio::spawn(async move { handle.resync(timeline_id).await });
73
                            continue;
74
                        }
75
                        print_messages(&items);
76
                    }
77
                    Event::SessionTokensChanged => {
78
                        std::fs::write(&record_path, serde_json::to_vec(&handle.session_record()?)?)?;
79
                    }
80
                    Event::Stopped => break,
81
                    _ => {}
82
                }
83
            }
84
            result = tokio::signal::ctrl_c() => {
85
                result?;
86
                let handle = handle.clone();
87
                tokio::spawn(async move { handle.shutdown().await });
88
            }
89
        }
90
    }
91
    match running.await??.retain() {
92
        Retain::RecordAndStore => {}
93
        Retain::Store => std::fs::remove_file(&record_path)?,
94
        Retain::Nothing => std::fs::remove_dir_all(&store_path)?,
95
    }
96
    Ok(())
97
}
98
99
fn print_messages(items: &[TimelineItem]) {
100
    for item in items {
101
        if let TimelineItem::Event(event) = item
102
            && let TimelineContent::Message(message) = &event.content
103
        {
104
            println!("{}  {}", event.sender.name(), message.body);
105
        }
106
    }
107
}
108
109
fn required(name: &str) -> Result<String, Box<dyn std::error::Error>> {
110
    std::env::var(name).map_err(|_| format!("{name} is required").into())
111
}