From 1c42fc675f51e77a39fdd88690abf51ea021381e Mon Sep 17 00:00:00 2001 From: hitalin Date: Sat, 9 May 2026 16:54:10 +0900 Subject: [PATCH] =?UTF-8?q?fix(chat):=20legacy=20messaging/unread=20?= =?UTF-8?q?=E3=82=92=20chat/history=20isRead=20=E9=9B=86=E8=A8=88=E3=81=AB?= =?UTF-8?q?=E7=BD=AE=E6=8F=9B=20(notedeck#469)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Misskey 新 Chat API ([#15686](https://github.com/misskey-dev/misskey/pull/15686), v2025) で legacy `messaging/unread` エンドポイントが完全廃止されたため、`chat/history` を `room=false` (DM) と `room=true` (room) で叩いて各 thread 最新 message の `isRead` フラグを集計するように置き換える。 シグネチャ拡張: `me_user_id: &str` を追加。自分送信メッセージの `isRead=false` (= 相手が未読) を自分の未読扱いにしないため。 戻り値は `bool` のまま (1 件でも未読あれば true)。Misskey 新 chat API には 件数を返す統一 API が無いため、現状の useUnreadChat (= 1/0) と互換維持。 テスト: 4 件追加 (other-user 未読 / self 除外 / all-read / 500 fallback)。 --- src/api.rs | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 5 deletions(-) diff --git a/src/api.rs b/src/api.rs index fc5316a..3f62453 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1311,11 +1311,39 @@ impl MisskeyClient { // --- Unread chat --- - pub async fn get_unread_chat(&self, host: &str, token: &str) -> Result { - let data = self - .request(host, token, "messaging/unread", json!({})) - .await?; - Ok(data.as_bool().unwrap_or(false)) + /// 未読チャットがあるかを返す。 + /// + /// Misskey 新 Chat API ([#15686](https://github.com/misskey-dev/misskey/pull/15686), v2025) では + /// legacy `messaging/unread` エンドポイントが廃止されたため、`chat/history` を + /// `room=false` (DM) と `room=true` (room) で叩いて各 thread 最新メッセージの + /// `isRead` フラグを集計する。`me_user_id` は自分送信メッセージを除外するために必要 + /// (自分送信メッセージで `isRead=false` でも自分の未読扱いにしないため)。 + pub async fn get_unread_chat( + &self, + host: &str, + token: &str, + me_user_id: &str, + ) -> Result { + for room in [false, true] { + let mut params = json!({ "limit": 100 }); + if room { + params["room"] = json!(true); + } + // 片方失敗しても他方を確認できるよう、エラーは握りつぶして次に進む + let data = match self.request(host, token, "chat/history", params).await { + Ok(d) => d, + Err(_) => continue, + }; + let Some(arr) = data.as_array() else { continue }; + for msg in arr { + let from = msg.get("fromUserId").and_then(|v| v.as_str()).unwrap_or(""); + let is_read = msg.get("isRead").and_then(|v| v.as_bool()).unwrap_or(true); + if from != me_user_id && !is_read { + return Ok(true); + } + } + } + Ok(false) } // --- Self (current user) --- @@ -3004,4 +3032,113 @@ mod tests { assert!(msgs[0].from_user.is_none()); assert!(msgs[0].to_user.is_none()); } + + // --- Unread chat (#469: messaging/unread → chat/history isRead 集計) --- + + fn unread_chat_history_msg( + id: &str, + from_user_id: &str, + is_read: bool, + is_room: bool, + ) -> Value { + let mut m = json!({ + "id": id, + "createdAt": "2025-01-01T00:00:00.000Z", + "fromUserId": from_user_id, + "fromUser": null, + "toUserId": null, + "toUser": null, + "toRoomId": null, + "toRoom": null, + "text": "hi", + "fileId": null, + "file": null, + "isRead": is_read, + "reactions": [] + }); + if is_room { + m["toRoomId"] = json!("r1"); + m["toRoom"] = json!({"id": "r1", "name": "R", "description": null}); + } else { + m["toUserId"] = json!("u_self"); + } + m + } + + #[tokio::test] + async fn get_unread_chat_returns_true_when_other_user_message_is_unread() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/chat/history")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!([ + unread_chat_history_msg("m1", "u_other", false, false), + ]))) + .mount(&server) + .await; + + let client = MisskeyClient::with_base_url(&server.uri()); + let unread = client + .get_unread_chat("h", "token", "u_self") + .await + .unwrap(); + assert!(unread); + } + + #[tokio::test] + async fn get_unread_chat_excludes_self_messages() { + let server = MockServer::start().await; + // 自分送信の DM が isRead=false (= 相手未読) でも自分の未読扱いにしない + Mock::given(method("POST")) + .and(path("/api/chat/history")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!([ + unread_chat_history_msg("m1", "u_self", false, false), + ]))) + .mount(&server) + .await; + + let client = MisskeyClient::with_base_url(&server.uri()); + let unread = client + .get_unread_chat("h", "token", "u_self") + .await + .unwrap(); + assert!(!unread); + } + + #[tokio::test] + async fn get_unread_chat_returns_false_when_all_read() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/chat/history")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!([ + unread_chat_history_msg("m1", "u_other", true, false), + unread_chat_history_msg("m2", "u_other", true, true), + ]))) + .mount(&server) + .await; + + let client = MisskeyClient::with_base_url(&server.uri()); + let unread = client + .get_unread_chat("h", "token", "u_self") + .await + .unwrap(); + assert!(!unread); + } + + #[tokio::test] + async fn get_unread_chat_swallows_errors_and_returns_false() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/chat/history")) + .respond_with(ResponseTemplate::new(500)) + .mount(&server) + .await; + + let client = MisskeyClient::with_base_url(&server.uri()); + let unread = client + .get_unread_chat("h", "token", "u_self") + .await + .unwrap(); + // 両方失敗 → false (panic せず正常 return) + assert!(!unread); + } }