instance_info API

This commit is contained in:
CenTdemeern1 2025-01-12 10:14:17 +01:00
parent 3af231ca38
commit 6ea7343d22
9 changed files with 1042 additions and 6 deletions

893
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2021"
[dependencies]
rocket = "0.5.1"
reqwest = "0.12.12"
rocket = { version = "0.5.1", features = ["json"] }
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.135"
url = "2.5.4"

View file

@ -2,6 +2,7 @@
"software": {
"misskey": {
"name": "Misskey",
"nodeinfoName": "misskey",
"aliases": [
"misskey",
"mk"
@ -13,6 +14,7 @@
},
"sharkey": {
"name": "Sharkey",
"nodeinfoName": "sharkey",
"aliases": [
"sharkey",
"sk"
@ -25,6 +27,7 @@
},
"iceshrimp-js": {
"name": "Iceshrimp-JS",
"nodeinfoName": "iceshrimp",
"aliases": [
"iceshrimp-js"
],
@ -37,6 +40,7 @@
},
"iceshrimp-dotnet": {
"name": "Iceshrimp.NET",
"nodeinfoName": "Iceshrimp.NET",
"aliases": [
"iceshrimp-dotnet"
],
@ -49,6 +53,7 @@
},
"firefish": {
"name": "Firefish",
"nodeinfoName": "firefish",
"aliases": [
"firefish",
"calckey"

119
src/api/instance_info.rs Normal file
View file

@ -0,0 +1,119 @@
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::known_software::KNOWN_SOFTWARE_NODEINFO_NAMES;
#[derive(Serialize)]
pub struct InstanceInfo {
name: String,
software: String,
#[serde(rename = "iconURL")]
icon_url: Option<String>,
}
#[derive(Deserialize)]
struct InstanceManifest {
name: Option<String>,
short_name: Option<String>,
icons: Option<Vec<InstanceIcon>>,
}
#[derive(Deserialize)]
struct InstanceIcon {
src: String,
sizes: String,
}
impl InstanceIcon {
fn get_size(&self) -> Option<usize> {
let (x, y) = self.sizes.split_once("x")?;
let x: usize = x.parse().ok()?;
let y: usize = y.parse().ok()?;
Some(x.max(y))
}
}
#[derive(Deserialize)]
struct NodeInfoDiscovery {
links: Vec<NodeInfoLink>,
}
#[derive(Deserialize)]
struct NodeInfoLink {
href: String,
}
#[derive(Deserialize)]
struct NodeInfo {
software: NodeInfoSoftware,
metadata: Option<NodeInfoMetadata>,
}
#[derive(Deserialize)]
struct NodeInfoSoftware {
name: String,
}
#[derive(Deserialize)]
struct NodeInfoMetadata {
#[serde(rename = "nodeName")]
name: Option<String>,
}
async fn get_info_from_manifest(url: Url) -> Option<[Option<String>; 3]> {
// FIXME: Iceshrimp.NET doesn't have a manifest...
let response = reqwest::get(url.clone()).await.ok()?.text().await.ok()?;
let manifest: InstanceManifest = serde_json::from_str(&response).ok()?;
Some([
manifest.name,
manifest.short_name,
manifest
.icons
.as_ref()
.and_then(|icons| icons.iter().min_by_key(|icon| icon.get_size()))
.map(|icon| icon.src.to_owned()),
])
}
#[get("/instance_info/<secure>/<host>")]
pub async fn instance_info(secure: bool, host: &str) -> Option<Json<InstanceInfo>> {
let mut url = Url::parse(&format!(
"http{}://{host}/manifest.json",
if secure { "s" } else { "" }
))
.ok()?;
// I'm not sure if you can sneak in a path, but better safe than sorry
// I don't really care about username/password/port, those are fine
if url.path() != "/manifest.json" {
return None;
}
let [name, short_name, icon_url] = get_info_from_manifest(url.clone())
.await
.unwrap_or_default();
let icon_url = icon_url
.and_then(|i| url.join(&i).ok())
.map(|u| u.to_string());
// FIXME: Iceshrimp.NET doesn't have a nodeinfo discovery file either.............
url.set_path("/.well-known/nodeinfo");
let response = reqwest::get(url.clone()).await.ok()?.text().await.ok()?;
let nodeinfo_discovery: NodeInfoDiscovery = serde_json::from_str(&response).ok()?;
let response = reqwest::get(&nodeinfo_discovery.links.first()?.href)
.await
.ok()?
.text()
.await
.ok()?;
let nodeinfo: NodeInfo = serde_json::from_str(&response).ok()?;
let software = KNOWN_SOFTWARE_NODEINFO_NAMES
.get(&nodeinfo.software.name)?
.to_owned();
Some(Json(InstanceInfo {
name: name
.or(short_name)
.or(nodeinfo.metadata.and_then(|m| m.name))
.unwrap_or(url.host_str().unwrap().to_owned()),
software,
icon_url,
}))
}

7
src/api/mod.rs Normal file
View file

@ -0,0 +1,7 @@
use rocket::Route;
pub mod instance_info;
pub fn get_routes() -> Vec<Route> {
routes![instance_info::instance_info]
}

View file

@ -9,11 +9,14 @@ pub static KNOWN_SOFTWARE: LazyLock<KnownSoftware> =
LazyLock::new(|| serde_json::from_str(include_str!("../known-software.json")).unwrap());
pub static KNOWN_SOFTWARE_NAMES: LazyLock<HashMap<String, String>> =
LazyLock::new(|| KNOWN_SOFTWARE.get_name_map());
pub static KNOWN_SOFTWARE_NODEINFO_NAMES: LazyLock<HashMap<String, String>> =
LazyLock::new(|| KNOWN_SOFTWARE.get_nodeinfo_name_map());
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Software {
name: String,
nodeinfo_name: String,
aliases: HashSet<String>,
groups: HashSet<String>,
fork_of: Option<String>,
@ -46,6 +49,17 @@ impl KnownSoftware {
});
map
}
fn get_nodeinfo_name_map(&self) -> HashMap<String, String> {
let mut map = HashMap::new();
self.software.iter().for_each(|(name, software)| {
assert_eq!(
map.insert(software.nodeinfo_name.to_owned(), name.to_owned()),
None
);
});
map
}
}
pub struct KnownInstanceSoftware<'r> {

View file

@ -8,6 +8,7 @@ use rocket::{
};
use std::path::PathBuf;
mod api;
mod known_software;
#[get("/known-software.json")]
@ -39,7 +40,7 @@ fn route_for_unknown_instance_software(instance: &str, route: PathBuf) -> (Conte
fn rocket() -> _ {
rocket::build()
.mount("/static", FileServer::from("static").rank(0))
.mount("/api", routes![])
.mount("/api", api::get_routes())
.mount(
"/",
routes![

View file

@ -1,5 +1,6 @@
type Software = {
name: string,
nodeinfoName: string,
aliases: string[],
groups: string[],
forkOf?: string,

View file

@ -24,7 +24,7 @@ type Instance = {
*
* Make sure to sanitize this! Could lead to XSS
*/
iconURL: string,
iconURL?: string,
}
type LocalStorage = {