@@ -1184,6 +1184,96 @@ impl MisskeyClient {
11841184 let message: ChatMessage = serde_json:: from_value ( data) ?;
11851185 Ok ( message)
11861186 }
1187+
1188+ // --- Server Discovery (unauthenticated) ---
1189+
1190+ /// Fetch nodeinfo via .well-known/nodeinfo.
1191+ /// Returns the parsed nodeinfo JSON.
1192+ pub async fn fetch_nodeinfo ( & self , host : & str ) -> Result < Value , NoteDeckError > {
1193+ let well_known_url = format ! ( "https://{host}/.well-known/nodeinfo" ) ;
1194+ let res = self
1195+ . client
1196+ . get ( & well_known_url)
1197+ . timeout ( Duration :: from_secs ( 10 ) )
1198+ . send ( )
1199+ . await ?;
1200+ if !res. status ( ) . is_success ( ) {
1201+ return Err ( NoteDeckError :: Api {
1202+ endpoint : ".well-known/nodeinfo" . to_string ( ) ,
1203+ status : res. status ( ) . as_u16 ( ) ,
1204+ message : "Failed to fetch well-known nodeinfo" . to_string ( ) ,
1205+ } ) ;
1206+ }
1207+ let text = Self :: read_body_limited ( res, ".well-known/nodeinfo" ) . await ?;
1208+ let well_known: Value = serde_json:: from_str ( & text) ?;
1209+
1210+ let nodeinfo_url = well_known[ "links" ]
1211+ . as_array ( )
1212+ . and_then ( |links| {
1213+ links. iter ( ) . find_map ( |link| {
1214+ let rel = link[ "rel" ] . as_str ( ) . unwrap_or ( "" ) ;
1215+ if rel. contains ( "nodeinfo" ) {
1216+ link[ "href" ] . as_str ( ) . map ( |s| s. to_string ( ) )
1217+ } else {
1218+ None
1219+ }
1220+ } )
1221+ } )
1222+ . ok_or_else ( || NoteDeckError :: Api {
1223+ endpoint : ".well-known/nodeinfo" . to_string ( ) ,
1224+ status : 0 ,
1225+ message : format ! ( "No nodeinfo URL found for {host}" ) ,
1226+ } ) ?;
1227+
1228+ // Validate URL to prevent SSRF: must be https://{host}/...
1229+ let expected_prefix = format ! ( "https://{host}/" ) ;
1230+ if !nodeinfo_url. starts_with ( & expected_prefix) {
1231+ return Err ( NoteDeckError :: Api {
1232+ endpoint : ".well-known/nodeinfo" . to_string ( ) ,
1233+ status : 0 ,
1234+ message : format ! ( "Nodeinfo URL host/scheme mismatch for {host}" ) ,
1235+ } ) ;
1236+ }
1237+
1238+ let res = self
1239+ . client
1240+ . get ( & nodeinfo_url)
1241+ . timeout ( Duration :: from_secs ( 10 ) )
1242+ . send ( )
1243+ . await ?;
1244+ if !res. status ( ) . is_success ( ) {
1245+ return Err ( NoteDeckError :: Api {
1246+ endpoint : "nodeinfo" . to_string ( ) ,
1247+ status : res. status ( ) . as_u16 ( ) ,
1248+ message : "Failed to fetch nodeinfo" . to_string ( ) ,
1249+ } ) ;
1250+ }
1251+ let text = Self :: read_body_limited ( res, "nodeinfo" ) . await ?;
1252+ let nodeinfo: Value = serde_json:: from_str ( & text) ?;
1253+ Ok ( nodeinfo)
1254+ }
1255+
1256+ /// Fetch server meta (icon URL) via /api/meta (unauthenticated).
1257+ pub async fn fetch_server_meta ( & self , host : & str ) -> Result < Value , NoteDeckError > {
1258+ let url = self . api_url ( host, "meta" ) ;
1259+ let res = self
1260+ . client
1261+ . post ( & url)
1262+ . json ( & json ! ( { } ) )
1263+ . timeout ( Duration :: from_secs ( 10 ) )
1264+ . send ( )
1265+ . await ?;
1266+ if !res. status ( ) . is_success ( ) {
1267+ return Err ( NoteDeckError :: Api {
1268+ endpoint : "meta" . to_string ( ) ,
1269+ status : res. status ( ) . as_u16 ( ) ,
1270+ message : "Failed to fetch server meta" . to_string ( ) ,
1271+ } ) ;
1272+ }
1273+ let text = Self :: read_body_limited ( res, "meta" ) . await ?;
1274+ let meta: Value = serde_json:: from_str ( & text) ?;
1275+ Ok ( meta)
1276+ }
11871277}
11881278
11891279#[ cfg( test) ]
0 commit comments