Initial commit of Collapse

This commit is contained in:
Olivier 'reivilibre' 2022-12-17 12:20:31 +00:00
commit cef0ce7615
4 changed files with 3388 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
/.idea
*.swp

3200
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "collapse"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
appdirs = "0.2.0"
eyre = "0.6.8"
matrix-sdk = { version = "0.6.2", features = ["eyre"] }
serde = "1.0.147"
serde_json = "1.0.87"
notify-rust = "4.5.10"
tokio = { version = "1.21.2", features = ["full"] }

169
src/bin/collapse.rs Normal file
View File

@ -0,0 +1,169 @@
use std::time::{Duration, Instant};
use eyre::{Context, ContextCompat};
use matrix_sdk::{Client, Session};
use matrix_sdk::config::SyncSettings;
use matrix_sdk::room::Room;
use matrix_sdk::ruma::events::room::message::MessageType;
use matrix_sdk::ruma::UserId;
use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent;
use notify_rust::{Hint, Notification, Timeout};
use tokio::io::{AsyncBufReadExt, BufReader, stdin};
use tokio::task::spawn_blocking;
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
let appdir = appdirs::user_data_dir(Some("collapse_matrix"), None, true)
.unwrap();
if ! appdir.exists() {
tokio::fs::create_dir(&appdir).await?;
}
let session_path = appdir.join("session.json");
let store_path = appdir.join("matrix-sdk-sled");
let client = if session_path.exists() {
let session_json = tokio::fs::read(&session_path).await.context("Failed to read session")?;
let session: Session = serde_json::from_slice(&session_json).context("Failed to deserialise session")?;
let client = Client::builder()
.server_name(session.user_id.server_name())
.sled_store(&store_path, None)
.context("Failed to make/open Matrix Store")?
.build().await
.context("Failed to create new client")?;
client.restore_login(session).await.context("Failed to restore session")?;
println!("Restored login.");
client
} else {
let mut br = BufReader::new(stdin());
let mut buf = String::new();
println!("You need to log in to use Collapse.");
println!("MXID: ");
br.read_line(&mut buf).await?;
let user_id = <&UserId>::try_from(buf.trim()).context("Bad MXID")?.to_owned();
buf.clear();
println!("Password: ");
br.read_line(&mut buf).await?;
let password = buf.trim().to_owned();
let client = Client::builder()
.server_name(user_id.server_name())
.sled_store(&store_path, None)
.context("Failed to make/open Matrix Store")?
.build().await
.context("Failed to create new client")?;
client.login_username(&user_id, &password)
.initial_device_display_name("Collapse")
.send()
.await
.context("Login failed")?;
let session_json = serde_json::to_vec(&client.session().context("No session after login")?).context("Failed to serialise session.")?;
tokio::fs::write(session_path, &session_json).await.context("Failed to write session")?;
client
};
println!("Initial syncing now...");
// Sync once so we don't process old messages
for attempt in 0..3 {
match client.sync_once(SyncSettings::default()).await
.context("Failed initial sync") {
Ok(_) => {
break;
}
Err(err) => {
if attempt == 2 {
return Err(err);
}
eprintln!("{err:?}");
}
}
}
println!("Initial sync completed.");
client.add_event_handler(on_room_message);
let mut last_fail = None;
loop {
if let Err(err) = client.sync(SyncSettings::default()).await {
eprintln!("Sync failed: {:?}", err);
let this_fail = Instant::now();
if let Some(last_fail) = last_fail {
if this_fail.duration_since(last_fail) < Duration::from_secs(3) {
Notification::new()
.appname("Collapse")
.summary("Collapse has collapsed!")
.body(&format!("Sync failed multiple times: {:?}", err))
.timeout(Timeout::Never)
.icon("error")
.show()
.context("Failed to show notification")?
.wait_for_action(|_| {});
break;
}
}
last_fail = Some(this_fail);
}
}
Ok(())
}
async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) -> eyre::Result<()> {
let Room::Joined(room) = room else { return Ok(()); };
let MessageType::Text(text) = event.content.msgtype else { return Ok(()); };
if ! text.body.contains("reivilibre") { return Ok(()); }
let escaped_body = text.body.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace("reivilibre", "<b>reivilibre</b>");
let room_name = room.display_name().await.context("Failed to calculate name of room.")?.to_string();
let member = room.get_member(&event.sender).await?.context("No room member")?;
let user_name = member.name();
let notif_name = format!("{user_name} ({room_name})");
let notif = Notification::new()
.appname("Collapse")
.summary(&notif_name)
.body(&escaped_body)
.hint(Hint::Category("chat".to_owned()))
.timeout(10_000)
.show()
.context("Couldn't show notification")?;
tokio::spawn(async move {
let action = spawn_blocking(move || {
let mut action = None;
notif.wait_for_action(|action_arg| {
action = Some(action_arg.to_owned());
});
action
}).await;
match action {
Ok(Some(ref action)) => {
match action.as_str() {
"__close" => {
return;
}
other => {
eprintln!("action: {:?}", other);
}
}
},
Ok(None) => return,
Err(err) => {
eprintln!("Error: {err:?}");
}
}
});
Ok(())
}