Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "notecli"
version = "0.5.0"
version = "0.6.0"
edition = "2021"
description = "Headless Misskey client — CLI & library"
repository = "https://github.com/notedeck-dev/notecli"
Expand Down
18 changes: 18 additions & 0 deletions migrations/V5__server_detections.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- V5: servers (アプリ側で計算した software/features のキャッシュ) を
-- server_detections (生の検出結果キャッシュ) に置き換える。
--
-- 旧 servers は「フロントで解決した software 名 + features 判定結果」を保存して
-- いたため、判定ロジックの更新が既存キャッシュに反映されない鮮度問題があった
-- (notedeck#782)。生の nodeinfo / meta を保存し、解決はアプリ側で読取時に行う。
-- 旧データは捨てる (次回アクセス時に再検出される 24h キャッシュのため損失は軽微)。

CREATE TABLE IF NOT EXISTS server_detections (
host TEXT PRIMARY KEY,
software_name TEXT NOT NULL,
software_version TEXT NOT NULL,
software_repository TEXT,
meta_json TEXT NOT NULL,
updated_at INTEGER NOT NULL
);

DROP TABLE IF EXISTS servers;
108 changes: 64 additions & 44 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::{Mutex, MutexGuard};

use crate::error::NoteDeckError;
use crate::models::{
Account, ChatMessage, ChatMessageReaction, ChatReactionUser, NormalizedNote, StoredServer,
Account, ChatMessage, ChatMessageReaction, ChatReactionUser, NormalizedNote, ServerDetection,
};

mod embedded {
Expand Down Expand Up @@ -391,37 +391,42 @@ impl Database {
Ok(count)
}

// --- Servers ---
// --- Server detections ---

pub fn load_servers(&self) -> Result<Vec<StoredServer>, NoteDeckError> {
pub fn load_server_detections(&self) -> Result<Vec<ServerDetection>, NoteDeckError> {
let conn = self.lock_read()?;
let mut stmt = conn.prepare_cached(
"SELECT host, software, version, features_json, updated_at FROM servers",
"SELECT host, software_name, software_version, software_repository, meta_json, updated_at FROM server_detections",
)?;
let rows = stmt.query_map([], |row| {
Ok(StoredServer {
Ok(ServerDetection {
host: row.get(0)?,
software: row.get(1)?,
version: row.get(2)?,
features_json: row.get(3)?,
updated_at: row.get(4)?,
software_name: row.get(1)?,
software_version: row.get(2)?,
software_repository: row.get(3)?,
meta_json: row.get(4)?,
updated_at: row.get(5)?,
})
})?;
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
}

pub fn get_server(&self, host: &str) -> Result<Option<StoredServer>, NoteDeckError> {
pub fn get_server_detection(
&self,
host: &str,
) -> Result<Option<ServerDetection>, NoteDeckError> {
let conn = self.lock_read()?;
let mut stmt = conn.prepare_cached(
"SELECT host, software, version, features_json, updated_at FROM servers WHERE host = ?1",
"SELECT host, software_name, software_version, software_repository, meta_json, updated_at FROM server_detections WHERE host = ?1",
)?;
let mut rows = stmt.query_map(params![host], |row| {
Ok(StoredServer {
Ok(ServerDetection {
host: row.get(0)?,
software: row.get(1)?,
version: row.get(2)?,
features_json: row.get(3)?,
updated_at: row.get(4)?,
software_name: row.get(1)?,
software_version: row.get(2)?,
software_repository: row.get(3)?,
meta_json: row.get(4)?,
updated_at: row.get(5)?,
})
})?;
match rows.next() {
Expand Down Expand Up @@ -900,22 +905,25 @@ impl Database {
Ok(())
}

pub fn upsert_server(&self, server: &StoredServer) -> Result<(), NoteDeckError> {
pub fn upsert_server_detection(&self, det: &ServerDetection) -> Result<(), NoteDeckError> {
let conn = self.lock_write()?;
conn.execute(
"INSERT INTO servers (host, software, version, features_json, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5)
"INSERT INTO server_detections
(host, software_name, software_version, software_repository, meta_json, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(host) DO UPDATE SET
software = excluded.software,
version = excluded.version,
features_json = excluded.features_json,
software_name = excluded.software_name,
software_version = excluded.software_version,
software_repository = excluded.software_repository,
meta_json = excluded.meta_json,
updated_at = excluded.updated_at",
params![
server.host,
server.software,
server.version,
server.features_json,
server.updated_at,
det.host,
det.software_name,
det.software_version,
det.software_repository,
det.meta_json,
det.updated_at,
],
)?;
Ok(())
Expand Down Expand Up @@ -1289,7 +1297,7 @@ impl Database {
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Account, NormalizedNote, NormalizedUser, StoredServer};
use crate::models::{Account, NormalizedNote, NormalizedUser, ServerDetection};
use std::collections::HashMap;

fn temp_db() -> (tempfile::TempDir, Database) {
Expand All @@ -1316,7 +1324,9 @@ mod tests {
.unwrap();

assert!(tables.contains(&"accounts".to_string()));
assert!(tables.contains(&"servers".to_string()));
assert!(tables.contains(&"server_detections".to_string()));
// V5 で旧 servers テーブルは削除済み
assert!(!tables.contains(&"servers".to_string()));
assert!(tables.contains(&"notes_cache".to_string()));
assert!(tables.contains(&"ogp_cache".to_string()));
assert!(tables.contains(&"chat_messages_cache".to_string()));
Expand Down Expand Up @@ -1465,35 +1475,45 @@ mod tests {

// --- Server CRUD tests ---

fn sample_server() -> StoredServer {
StoredServer {
fn sample_detection() -> ServerDetection {
ServerDetection {
host: "misskey.io".to_string(),
software: "misskey".to_string(),
version: "2025.3.0".to_string(),
features_json: "{}".to_string(),
software_name: "misskey".to_string(),
software_version: "2025.3.0".to_string(),
software_repository: Some("https://github.com/misskey-dev/misskey".to_string()),
meta_json: "{}".to_string(),
updated_at: 1700000000,
}
}

#[test]
fn server_upsert_and_load() {
fn server_detection_upsert_and_load() {
let (_dir, db) = temp_db();
db.upsert_server(&sample_server()).unwrap();
db.upsert_server_detection(&sample_detection()).unwrap();

let servers = db.load_servers().unwrap();
assert_eq!(servers.len(), 1);
assert_eq!(servers[0].host, "misskey.io");
let dets = db.load_server_detections().unwrap();
assert_eq!(dets.len(), 1);
assert_eq!(dets[0].host, "misskey.io");
}

#[test]
fn server_get_by_host() {
fn server_detection_get_by_host_and_update() {
let (_dir, db) = temp_db();
db.upsert_server(&sample_server()).unwrap();
db.upsert_server_detection(&sample_detection()).unwrap();

let d = db.get_server_detection("misskey.io").unwrap().unwrap();
assert_eq!(d.software_version, "2025.3.0");

let s = db.get_server("misskey.io").unwrap().unwrap();
assert_eq!(s.version, "2025.3.0");
// upsert は同 host を上書きする
let mut newer = sample_detection();
newer.software_version = "2025.4.0".to_string();
newer.updated_at = 1700001000;
db.upsert_server_detection(&newer).unwrap();
let d = db.get_server_detection("misskey.io").unwrap().unwrap();
assert_eq!(d.software_version, "2025.4.0");
assert_eq!(d.updated_at, 1700001000);

assert!(db.get_server("nonexistent").unwrap().is_none());
assert!(db.get_server_detection("nonexistent").unwrap().is_none());
}

// --- Notes cache tests ---
Expand Down
6 changes: 2 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod http_server;
pub(crate) mod insecure;
pub mod keychain;
pub mod models;
pub mod server_info;
pub mod streaming;

use db::Database;
Expand Down Expand Up @@ -38,10 +39,7 @@ pub fn get_credentials(db: &Database, account_id: &str) -> Result<(String, Strin
// Try lazy migration to keychain; verify before clearing DB
if keychain::is_persistent()
&& keychain::store_token(account_id, &db_token).is_ok()
&& keychain::get_token(account_id)
.ok()
.flatten()
.is_some()
&& keychain::get_token(account_id).ok().flatten().is_some()
{
let _ = db.clear_token(account_id);
}
Expand Down
36 changes: 24 additions & 12 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,24 @@ impl From<Account> for AccountPublic {
}
}

/// サーバー検出結果の生キャッシュ (notedeck#782)。
///
/// nodeinfo の software 情報と /api/meta の生 JSON をそのまま保存する。
/// フォーク解決 (software 名 → ServerSoftware) と feature 判定はアプリ側が
/// 読取時に行う — 判定ロジックの更新が古いキャッシュに埋まらないようにする。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[serde(rename_all = "camelCase")]
pub struct StoredServer {
pub struct ServerDetection {
pub host: String,
pub software: String,
pub version: String,
pub features_json: String,
/// nodeinfo `software.name` (例: "misskey")
pub software_name: String,
/// nodeinfo `software.version`
pub software_version: String,
/// nodeinfo 2.1 `software.repository` (例: "https://github.com/misskey-dev/misskey")
pub software_repository: Option<String>,
/// /api/meta (detail: true) の生 JSON。取得失敗時は "{}"
pub meta_json: String,
pub updated_at: i64,
}

Expand Down Expand Up @@ -2348,17 +2358,19 @@ mod tests {
}

#[test]
fn stored_server_serde_roundtrip() {
let server = StoredServer {
fn server_detection_serde_roundtrip() {
let det = ServerDetection {
host: "misskey.io".into(),
software: "misskey".into(),
version: "2024.1.0".into(),
features_json: r#"{"miAuth":true}"#.into(),
software_name: "misskey".into(),
software_version: "2024.1.0".into(),
software_repository: Some("https://github.com/misskey-dev/misskey".into()),
meta_json: r#"{"iconUrl":"/icon.png"}"#.into(),
updated_at: 1700000000,
};
let json = serde_json::to_string(&server).unwrap();
let back: StoredServer = serde_json::from_str(&json).unwrap();
let json = serde_json::to_string(&det).unwrap();
assert!(json.contains("softwareName"));
let back: ServerDetection = serde_json::from_str(&json).unwrap();
assert_eq!(back.host, "misskey.io");
assert_eq!(back.version, "2024.1.0");
assert_eq!(back.software_version, "2024.1.0");
}
}
Loading
Loading