Improve instance detection
Instance detection is now based on NodeInfo, which is standardized and lighterweight.
This commit is contained in:
parent
6d4eca0131
commit
2ad24a91da
2 changed files with 146 additions and 182 deletions
146
src/pages/api/detect/[domain].ts
Normal file
146
src/pages/api/detect/[domain].ts
Normal file
|
@ -0,0 +1,146 @@
|
|||
/*!
|
||||
* © 2023 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
|
||||
import type { APIRoute } from "astro";
|
||||
import { FediverseProject } from "../../../constants";
|
||||
import { normalizeURL } from "../../../util";
|
||||
|
||||
interface FediverseProjectData {
|
||||
publishEndpoint: string;
|
||||
params: {
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
|
||||
const PROJECTS: Map<FediverseProject, FediverseProjectData> = new Map([
|
||||
[
|
||||
FediverseProject.Mastodon,
|
||||
{
|
||||
publishEndpoint: "share",
|
||||
params: {
|
||||
text: "text",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
FediverseProject.GNUSocial,
|
||||
{
|
||||
publishEndpoint: "/notice/new",
|
||||
params: {
|
||||
text: "status_textarea",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
FediverseProject.Pleroma,
|
||||
{
|
||||
publishEndpoint: "share",
|
||||
params: {
|
||||
text: "message",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
FediverseProject.Friendica,
|
||||
{
|
||||
publishEndpoint: "compose",
|
||||
params: {
|
||||
text: "body",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
FediverseProject.Hubzilla,
|
||||
{
|
||||
publishEndpoint: "rpost",
|
||||
params: {
|
||||
text: "body",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
FediverseProject.Misskey,
|
||||
{
|
||||
publishEndpoint: "share",
|
||||
params: {
|
||||
text: "text",
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
interface NodeInfoList {
|
||||
links: {
|
||||
rel: string;
|
||||
href: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface NodeInfo {
|
||||
[key: string]: unknown;
|
||||
software: {
|
||||
[key: string]: unknown;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
type NonEmptyArray<T> = [T, ...T[]];
|
||||
function isNotEmpty<T>(array: T[]): array is NonEmptyArray<T> {
|
||||
return array.length > 0;
|
||||
}
|
||||
|
||||
const checkNodeInfo = async (domain: string): Promise<FediverseProject> => {
|
||||
const nodeInfoListUrl = new URL(
|
||||
"/.well-known/nodeinfo",
|
||||
normalizeURL(domain),
|
||||
);
|
||||
const nodeInfoListResponse = await fetch(nodeInfoListUrl);
|
||||
const nodeInfoList = (await nodeInfoListResponse.json()) as NodeInfoList;
|
||||
|
||||
if (isNotEmpty(nodeInfoList.links)) {
|
||||
const nodeInfoUrl = nodeInfoList.links[0].href;
|
||||
const nodeInfoResponse = await fetch(nodeInfoUrl);
|
||||
const nodeInfo = (await nodeInfoResponse.json()) as NodeInfo;
|
||||
|
||||
return nodeInfo.software.name as FediverseProject;
|
||||
} else {
|
||||
throw new Error(`No nodeinfo found for ${domain}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const get: APIRoute = async ({ params }) => {
|
||||
const domain = params.domain as string;
|
||||
|
||||
try {
|
||||
const projectId = await checkNodeInfo(domain);
|
||||
|
||||
if (!PROJECTS.has(projectId)) {
|
||||
throw new Error(`Unexpected project ID: ${projectId}`);
|
||||
}
|
||||
const projectData = PROJECTS.get(projectId) as FediverseProjectData;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
host: domain,
|
||||
project: projectId,
|
||||
publishEndpoint: projectData.publishEndpoint,
|
||||
params: projectData.params,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Cache-Control": "s-maxage=86400, max-age=86400, public",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: "Couldn't detect instance" }), {
|
||||
status: 404,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,182 +0,0 @@
|
|||
/*!
|
||||
* © 2023 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
|
||||
import type { APIRoute } from "astro";
|
||||
import { normalizeURL } from "../../../util";
|
||||
|
||||
interface FediverseProjectBasic {
|
||||
publishEndpoint: string;
|
||||
params: {
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface FediverseProjectCheckFunction extends FediverseProjectBasic {
|
||||
check: (url: string) => Promise<string>;
|
||||
}
|
||||
|
||||
interface FediverseProjectCheckUrl extends FediverseProjectBasic {
|
||||
checkUrl: string;
|
||||
}
|
||||
|
||||
type FediverseProject =
|
||||
| FediverseProjectCheckUrl
|
||||
| FediverseProjectCheckFunction;
|
||||
|
||||
const PROJECTS: Map<string, FediverseProject> = new Map([
|
||||
[
|
||||
"mastodon",
|
||||
{
|
||||
checkUrl: "/api/v1/instance/rules",
|
||||
publishEndpoint: "share",
|
||||
params: {
|
||||
text: "text",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"gnuSocial",
|
||||
{
|
||||
checkUrl: "/api/gnusocial/config.xml",
|
||||
publishEndpoint: "/notice/new",
|
||||
params: {
|
||||
text: "status_textarea",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"pleroma",
|
||||
{
|
||||
checkUrl: "/api/v1/pleroma/federation_status",
|
||||
publishEndpoint: "share",
|
||||
params: {
|
||||
text: "message",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"friendica",
|
||||
{
|
||||
checkUrl: "/api/statusnet/config",
|
||||
publishEndpoint: "compose",
|
||||
params: {
|
||||
text: "body",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"hubzilla",
|
||||
{
|
||||
check: async (url: string): Promise<string> => {
|
||||
const response = await fetch(url);
|
||||
const htmlBody = await response.text();
|
||||
console.debug(htmlBody);
|
||||
if (
|
||||
htmlBody.includes(
|
||||
'<meta name="application-name" content="hubzilla" />',
|
||||
)
|
||||
) {
|
||||
return "hubzilla";
|
||||
}
|
||||
throw new Error(`${url} doesn't host Hubzilla`);
|
||||
},
|
||||
checkUrl: "/.well-known/zot-info",
|
||||
publishEndpoint: "rpost",
|
||||
params: {
|
||||
text: "body",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"misskey",
|
||||
{
|
||||
check: async (url: string): Promise<string> => {
|
||||
const response = await fetch(new URL("/api/meta", url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
detail: false,
|
||||
}),
|
||||
});
|
||||
const metadata = await response.json();
|
||||
if (metadata.version) {
|
||||
return "misskey";
|
||||
}
|
||||
throw new Error(`${url} doesn't host Misskey`);
|
||||
},
|
||||
checkUrl: "/",
|
||||
publishEndpoint: "share",
|
||||
params: {
|
||||
text: "text",
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const checkProjectUrl = (
|
||||
urlToCheck: URL,
|
||||
projectId: string,
|
||||
): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(urlToCheck)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
resolve(projectId);
|
||||
} else {
|
||||
reject(urlToCheck);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error.toString());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const get: APIRoute = async ({ params }) => {
|
||||
const host = params.host as string;
|
||||
|
||||
const promises = [...PROJECTS.entries()].map(([service, project]) => {
|
||||
const url = normalizeURL(host);
|
||||
if (project.check !== undefined) {
|
||||
return project.check(url);
|
||||
}
|
||||
return checkProjectUrl(new URL(project.checkUrl, url), service);
|
||||
});
|
||||
try {
|
||||
const projectId = await Promise.any(promises);
|
||||
|
||||
if (!PROJECTS.has(projectId)) {
|
||||
throw new Error(`Unexpected project ID: ${projectId}`);
|
||||
}
|
||||
|
||||
const project = PROJECTS.get(projectId) as FediverseProject;
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
host,
|
||||
project: projectId,
|
||||
publishEndpoint: project.publishEndpoint,
|
||||
params: project.params,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Cache-Control": "s-maxage=86400, max-age=86400, public",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: "Couldn't detect instance" }), {
|
||||
status: 404,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
Reference in a new issue