-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Detect AT URI from http and html alternate links (#190)
* Detect AT URI from http and html alternate links * Add comment * Add a uri decode * Use a proper link parser * Abort the fetch, add user agent * Factor out into shared function
- Loading branch information
1 parent
d1afd44
commit 8c38876
Showing
6 changed files
with
222 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,14 @@ | ||
"use server"; | ||
|
||
import { getAtUriPath } from "@/lib/util"; | ||
import { AtUri, isValidHandle } from "@atproto/syntax"; | ||
import { redirect } from "next/navigation"; | ||
import { navigateAtUri } from "@/lib/navigation"; | ||
|
||
export async function navigateUri(_state: unknown, formData: FormData) { | ||
export async function navigateUriAction(_state: unknown, formData: FormData) { | ||
const uriInput = formData.get("uri") as string; | ||
const handle = parseHandle(uriInput); | ||
|
||
if (handle) { | ||
redirect(getAtUriPath(new AtUri(`at://${handle}`))); | ||
} | ||
const result = await navigateAtUri(uriInput); | ||
|
||
let uri; | ||
try { | ||
uri = new AtUri(uriInput); | ||
} catch (_) { | ||
return { | ||
error: `Invalid URI: ${uriInput}`, | ||
}; | ||
if ("error" in result) { | ||
return result; | ||
} | ||
|
||
redirect(getAtUriPath(uri)); | ||
} | ||
|
||
function parseHandle(input: string): string | null { | ||
if (!input.startsWith("@")) return null; | ||
const handle = input.slice(1); | ||
if (!isValidHandle(handle)) return null; | ||
return handle; | ||
throw new Error("Should have redirected"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,14 @@ | ||
import { getAtUriPath } from "@/lib/util"; | ||
import { AtUri } from "@atproto/syntax"; | ||
import { redirect } from "next/navigation"; | ||
import { navigateAtUri } from "@/lib/navigation"; | ||
|
||
export function GET(request: Request) { | ||
export async function GET(request: Request) { | ||
const searchParams = new URL(request.url).searchParams; | ||
console.log(searchParams.get("u")); | ||
const uri = new AtUri(searchParams.get("u")!); | ||
redirect(getAtUriPath(uri)); | ||
const u = searchParams.get("u"); | ||
if (!u) { | ||
return new Response("Missing u parameter", { status: 400 }); | ||
} | ||
const result = await navigateAtUri(u); | ||
if ("error" in result) { | ||
return new Response(result.error, { status: 400 }); | ||
} | ||
throw new Error("Should have redirected"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import "server-only"; | ||
|
||
import { getAtUriPath } from "./util"; | ||
import { AtUri, isValidHandle } from "@atproto/syntax"; | ||
import { redirect } from "next/navigation"; | ||
import { parse as parseHtml } from "node-html-parser"; | ||
import { parse as parseLinkHeader } from "http-link-header"; | ||
|
||
export async function navigateAtUri(input: string) { | ||
const handle = parseHandle(input); | ||
|
||
if (handle) { | ||
redirect(getAtUriPath(new AtUri(`at://${handle}`))); | ||
} | ||
|
||
const result = | ||
input.startsWith("http://") || input.startsWith("https://") | ||
? await getAtUriFromHttp(input) | ||
: parseUri(input); | ||
|
||
if ("error" in result) { | ||
return result; | ||
} | ||
|
||
redirect(getAtUriPath(result.uri)); | ||
} | ||
|
||
type UriParseResult = | ||
| { | ||
error: string; | ||
} | ||
| { uri: AtUri }; | ||
|
||
async function getAtUriFromHttp(url: string): Promise<UriParseResult> { | ||
const controller = new AbortController(); | ||
const response = await fetch(url, { | ||
headers: { | ||
"User-Agent": "atproto-browser.vercel.app", | ||
}, | ||
signal: controller.signal, | ||
}); | ||
if (!response.ok) { | ||
controller.abort(); | ||
return { error: `Failed to fetch ${url}` }; | ||
} | ||
|
||
const linkHeader = response.headers.get("Link"); | ||
if (linkHeader) { | ||
const ref = parseLinkHeader(linkHeader).refs.find( | ||
(ref) => ref.rel === "alternate" && ref.uri.startsWith("at://"), | ||
); | ||
const result = ref ? parseUri(ref.uri) : null; | ||
if (result && "uri" in result) { | ||
controller.abort(); | ||
redirect(getAtUriPath(result.uri)); | ||
} | ||
} | ||
|
||
const html = await response.text(); | ||
let doc; | ||
try { | ||
doc = parseHtml(html); | ||
} catch (_) { | ||
return { | ||
error: `Failed to parse HTML from ${url}`, | ||
}; | ||
} | ||
|
||
const alternates = doc.querySelectorAll('link[rel="alternate"]'); | ||
// Choose the first AT URI found in the alternates, there's not really a better way to choose the right one | ||
const atUriAlternate = alternates.find((link) => | ||
link.getAttribute("href")?.startsWith("at://"), | ||
); | ||
if (atUriAlternate) { | ||
const result = parseUri(atUriAlternate.getAttribute("href")!); | ||
if ("uri" in result) { | ||
return result; | ||
} | ||
} | ||
|
||
return { | ||
error: `No AT URI found in ${url}`, | ||
}; | ||
} | ||
|
||
function parseUri(input: string): UriParseResult { | ||
try { | ||
return { uri: new AtUri(input) }; | ||
} catch (_) { | ||
return { | ||
error: `Invalid URI: ${input}`, | ||
}; | ||
} | ||
} | ||
|
||
function parseHandle(input: string): string | null { | ||
if (!input.startsWith("@")) return null; | ||
const handle = input.slice(1); | ||
if (!isValidHandle(handle)) return null; | ||
return handle; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.