auto-join invites, auto-create room/people notes with display names
This commit is contained in:
parent
50e2695e93
commit
84bb5165ef
1 changed files with 139 additions and 7 deletions
144
src/main.rs
144
src/main.rs
|
|
@ -10,10 +10,11 @@ use matrix_sdk::{
|
||||||
authentication::matrix::MatrixSession,
|
authentication::matrix::MatrixSession,
|
||||||
config::SyncSettings,
|
config::SyncSettings,
|
||||||
ruma::{
|
ruma::{
|
||||||
OwnedEventId, OwnedRoomId, OwnedUserId,
|
OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
|
||||||
api::client::filter::FilterDefinition,
|
api::client::filter::FilterDefinition,
|
||||||
events::room::message::{
|
events::room::{
|
||||||
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
|
member::StrippedRoomMemberEvent,
|
||||||
|
message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -208,11 +209,47 @@ async fn sync(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.add_event_handler(on_stripped_state_member);
|
||||||
|
|
||||||
client.sync(sync_settings).await?;
|
client.sync(sync_settings).await?;
|
||||||
|
|
||||||
bail!("sync loop exited unexpectedly")
|
bail!("sync loop exited unexpectedly")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn on_stripped_state_member(event: StrippedRoomMemberEvent, client: Client, room: Room) {
|
||||||
|
let Some(my_id) = client.user_id() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if event.state_key != my_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let room_id = room.room_id().to_owned();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tracing::info!(room = %room_id, "auto-joining invite");
|
||||||
|
let mut delay = 2u64;
|
||||||
|
loop {
|
||||||
|
match room.join().await {
|
||||||
|
Ok(()) => {
|
||||||
|
tracing::info!(room = %room_id, "joined");
|
||||||
|
if let Err(e) = ensure_room_notes(&room).await {
|
||||||
|
tracing::warn!(room = %room_id, "failed to write room notes: {e}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(room = %room_id, "join failed, retry in {delay}s: {e}");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(delay)).await;
|
||||||
|
delay = (delay * 2).min(300);
|
||||||
|
if delay >= 300 {
|
||||||
|
tracing::error!(room = %room_id, "giving up on auto-join");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async fn persist_sync_token(session_file: &Path, sync_token: String) -> anyhow::Result<()> {
|
async fn persist_sync_token(session_file: &Path, sync_token: String) -> anyhow::Result<()> {
|
||||||
let data = fs::read_to_string(session_file).await?;
|
let data = fs::read_to_string(session_file).await?;
|
||||||
let mut session: PersistedSession = serde_json::from_str(&data)?;
|
let mut session: PersistedSession = serde_json::from_str(&data)?;
|
||||||
|
|
@ -234,8 +271,10 @@ async fn on_room_message(
|
||||||
};
|
};
|
||||||
|
|
||||||
let room_id = room.room_id().to_owned();
|
let room_id = room.room_id().to_owned();
|
||||||
let mut state = state.lock().await;
|
let is_self = {
|
||||||
let is_self = event.sender == state.own_user_id;
|
let state = state.lock().await;
|
||||||
|
event.sender == state.own_user_id
|
||||||
|
};
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
room = %room_id,
|
room = %room_id,
|
||||||
|
|
@ -245,9 +284,102 @@ async fn on_room_message(
|
||||||
text_content.body
|
text_content.body
|
||||||
);
|
);
|
||||||
|
|
||||||
if !is_self && !state.pending_rooms.contains(&room_id) {
|
if let Err(e) = ensure_room_notes(&room).await {
|
||||||
|
tracing::warn!(room = %room_id, "failed to ensure room notes: {e}");
|
||||||
|
}
|
||||||
|
if !is_self {
|
||||||
|
if let Err(e) = ensure_person_notes(&room, &event.sender).await {
|
||||||
|
tracing::warn!(sender = %event.sender, "failed to ensure person notes: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_self {
|
||||||
|
let mut state = state.lock().await;
|
||||||
|
if !state.pending_rooms.contains(&room_id) {
|
||||||
state.pending_rooms.push(room_id);
|
state.pending_rooms.push(room_id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create state/rooms/<room_id>/notes.md if it doesn't exist.
|
||||||
|
async fn ensure_room_notes(room: &Room) -> anyhow::Result<()> {
|
||||||
|
let room_id = room.room_id();
|
||||||
|
let dir = paths::state_dir().join("rooms").join(room_id.as_str());
|
||||||
|
let file = dir.join("notes.md");
|
||||||
|
if file.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
fs::create_dir_all(&dir).await?;
|
||||||
|
|
||||||
|
let display_name = room
|
||||||
|
.display_name()
|
||||||
|
.await
|
||||||
|
.map(|n| n.to_string())
|
||||||
|
.unwrap_or_else(|_| room_id.to_string());
|
||||||
|
let now = chrono_now();
|
||||||
|
|
||||||
|
let body = format!("# {room_id}\n\nDisplay name: {display_name}\n\nFirst joined: {now}\n",);
|
||||||
|
fs::write(&file, body).await?;
|
||||||
|
tracing::info!(room = %room_id, "created room notes");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create state/people/<user_id>/notes.md if it doesn't exist.
|
||||||
|
/// Pre-fill with display name (from this room's member info) and the room as
|
||||||
|
/// "first met in".
|
||||||
|
async fn ensure_person_notes(room: &Room, user_id: &UserId) -> anyhow::Result<()> {
|
||||||
|
let dir = paths::state_dir().join("people").join(user_id.as_str());
|
||||||
|
let file = dir.join("notes.md");
|
||||||
|
if file.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
fs::create_dir_all(&dir).await?;
|
||||||
|
|
||||||
|
let display_name = room
|
||||||
|
.get_member_no_sync(user_id)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|m| m.display_name().map(ToOwned::to_owned))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let display_line = if display_name.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("Display name: {display_name}\n")
|
||||||
|
};
|
||||||
|
let room_id = room.room_id();
|
||||||
|
let now = chrono_now();
|
||||||
|
|
||||||
|
let body = format!("# {user_id}\n\n{display_line}First met in: {room_id} on {now}\n",);
|
||||||
|
fs::write(&file, body).await?;
|
||||||
|
tracing::info!(user = %user_id, "created person notes");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chrono_now() -> String {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
let secs = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs() as i64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
// Simple ISO-ish without pulling chrono. YYYY-MM-DD only is fine for notes.
|
||||||
|
let days = secs / 86400;
|
||||||
|
let (y, m, d) = days_to_ymd(days);
|
||||||
|
format!("{y:04}-{m:02}-{d:02}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert days-since-1970-01-01 to (year, month, day). Civil-date algorithm.
|
||||||
|
fn days_to_ymd(z: i64) -> (i64, u32, u32) {
|
||||||
|
let z = z + 719_468;
|
||||||
|
let era = z.div_euclid(146_097);
|
||||||
|
let doe = z.rem_euclid(146_097) as u32;
|
||||||
|
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
|
||||||
|
let y = yoe as i64 + era * 400;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
let mp = (5 * doy + 2) / 153;
|
||||||
|
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||||
|
(if m <= 2 { y + 1 } else { y }, m, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
|
async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue