auto-join invites, auto-create room/people notes with display names

This commit is contained in:
Damocles 2026-04-30 20:44:34 +02:00
parent 50e2695e93
commit 84bb5165ef

View file

@ -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,10 +284,103 @@ 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) {
loop { loop {