Initial commit of Collapse
This commit is contained in:
commit
cef0ce7615
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
|
||||
/.idea
|
||||
*.swp
|
File diff suppressed because it is too large
Load Diff
|
@ -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"] }
|
|
@ -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("&", "&").replace("<", "<").replace(">", ">")
|
||||
.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(¬if_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(())
|
||||
}
|
Loading…
Reference in New Issue