From 378351a5d8deb0a740ae6d3b803984ffb93eb8df Mon Sep 17 00:00:00 2001 From: Nikita Karamov Date: Sat, 18 Mar 2023 00:28:16 +0100 Subject: [PATCH] Add host detection This re-implements the posting that was achieved in v3. --- api/share.js | 102 --------------------------------- src/pages/api/detect/[host].ts | 78 +++++++++++++++++++++++++ src/pages/api/share.ts | 24 +++++--- 3 files changed, 94 insertions(+), 110 deletions(-) delete mode 100644 api/share.js create mode 100644 src/pages/api/detect/[host].ts diff --git a/api/share.js b/api/share.js deleted file mode 100644 index 42af326..0000000 --- a/api/share.js +++ /dev/null @@ -1,102 +0,0 @@ -/*! - share2fedi - Instance-agnostic share page for the Fediverse. - Copyright (C) 2020-2023 Nikita Karamov - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - - SPDX-License-Identifier: AGPL-3.0-or-later -*/ - -import http from "http"; -import https from "https"; - -const pathsMap = { - mastodon: { - checkUrl: "/api/v1/instance/rules", - postUrl: "share", - textParam: "text", - }, - gnuSocial: { - checkUrl: "/api/statusnet/version.xml", - postUrl: "/notice/new", - textParam: "status_textarea", - }, - pleroma: { - checkUrl: "/api/v1/pleroma/federation_status", - postUrl: "share", - textParam: "message", - }, -}; - -const queryUrl = (url, service) => { - return new Promise((resolve, reject) => { - const get = url.protocol === "https:" ? https.get : http.get; - get(url, ({ statusCode }) => { - if (statusCode === 200) { - console.debug(url.href, "is", service); - resolve(service); - } else { - reject(url); - } - }).on("error", (error) => { - reject(error); - }); - }); -}; - -const detectService = async (instanceURL) => { - const checkPromises = Object.entries(pathsMap).map( - ([service, { checkUrl }]) => - queryUrl(new URL(checkUrl, instanceURL), service), - ); - - return await Promise.any(checkPromises); -}; - -const requestListener = async (request, response) => { - if (request.method !== "POST") { - response.writeHead(405).end(); - return; - } - - let data = ""; - request.on("data", (chunk) => { - data += chunk.toString(); - }); - - request.on("end", () => { - const requestBody = new URLSearchParams(data); - const postText = requestBody.get("text") || ""; - const instanceURL = - requestBody.get("instance") || "https://mastodon.social/"; - - detectService(instanceURL) - .then((service) => { - const publishUrl = new URL(pathsMap[service].postUrl, instanceURL); - publishUrl.search = new URLSearchParams([ - [pathsMap[service].textParam, postText], - ]); - response.writeHead(303, { Location: publishUrl.toString() }).end(); - }) - .catch((error) => { - response.writeHead(400).end(error); - }); - }); -}; - -if (!import.meta.env || import.meta.env.PROD) { - http.createServer(requestListener).listen(8080); -} - -export const viteNodeApp = requestListener; diff --git a/src/pages/api/detect/[host].ts b/src/pages/api/detect/[host].ts new file mode 100644 index 0000000..1e93ca4 --- /dev/null +++ b/src/pages/api/detect/[host].ts @@ -0,0 +1,78 @@ +import { APIRoute } from "astro"; +import { normalizeURL } from "../../../util"; + +const PROJECTS = { + mastodon: { + checkUrl: "/api/v1/instance/rules", + publishEndpoint: "share", + params: { + text: "text", + }, + }, + gnuSocial: { + checkUrl: "/api/statusnet/version.xml", + publishEndpoint: "/notice/new", + params: { + text: "status_textarea", + }, + }, + pleroma: { + checkUrl: "/api/v1/pleroma/federation_status", + publishEndpoint: "share", + params: { + text: "message", + }, + }, +}; + +const checkProjectUrl = ( + urlToCheck: URL, + projectId: string, +): Promise => { + 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; + + const promises = Object.entries(PROJECTS).map(([service, { checkUrl }]) => + checkProjectUrl(new URL(checkUrl, normalizeURL(host)), service), + ); + try { + const project = await Promise.any(promises); + return new Response( + JSON.stringify({ + host, + project, + publishEndpoint: PROJECTS[project].publishEndpoint, + params: PROJECTS[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: 400, + headers: { + "Content-Type": "application/json", + }, + }); + } +}; diff --git a/src/pages/api/share.ts b/src/pages/api/share.ts index 813a0ae..61c213d 100644 --- a/src/pages/api/share.ts +++ b/src/pages/api/share.ts @@ -1,16 +1,24 @@ import { APIRoute } from "astro"; -export const post: APIRoute = async ({ redirect, request }) => { +export const post: APIRoute = async ({ redirect, request, url }) => { const formData = await request.formData(); const text = (formData.get("text") as string) || ""; - const instanceDomain = + const instanceHost = (formData.get("instance") as string) || "mastodon.social"; - const publishUrl = new URL("/share", `https://${instanceDomain}/`); - publishUrl.search = new URLSearchParams({ - text, - }).toString(); - - return redirect(publishUrl.toString(), 303); + try { + const response = await fetch(new URL(`/api/detect/${instanceHost}`, url)); + const { host, publishEndpoint, params } = await response.json(); + const publishUrl = new URL(publishEndpoint, `https://${host}/`); + publishUrl.search = new URLSearchParams([[params.text, text]]).toString(); + return redirect(publishUrl.toString(), 303); + } catch { + return new Response(JSON.stringify({ error: "Couldn't detect instance" }), { + status: 400, + headers: { + "Content-Type": "application/json", + }, + }); + } };