From 2f6fccf0183c6fc8d6555b55bdc3116141944b07 Mon Sep 17 00:00:00 2001 From: Johanna Larsson Date: Sun, 5 Jul 2026 17:21:21 +0100 Subject: [PATCH] Expose public document rendering Mostly for fun, but since we're rendering docs in the app, we can render any doc from public routes too! Will need to add rate limits and stuff won't I. --- lib/annot_at/atproto/identity.ex | 10 +++- lib/annot_at/atproto/standard_site.ex | 33 +++++++++++++ .../components/document_components.ex | 4 +- lib/annot_at_web/components/layouts.ex | 13 +++++ .../controllers/live/posts_live.ex | 2 +- .../controllers/public_post_controller.ex | 45 +++++++++++++++++ .../controllers/public_post_html.ex | 6 +++ .../public_post_html/show.html.heex | 3 ++ lib/annot_at_web/router.ex | 6 +++ .../public_post_controller_test.exs | 49 +++++++++++++++++++ 10 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 lib/annot_at_web/controllers/public_post_controller.ex create mode 100644 lib/annot_at_web/controllers/public_post_html.ex create mode 100644 lib/annot_at_web/controllers/public_post_html/show.html.heex create mode 100644 test/annot_at_web/controllers/public_post_controller_test.exs diff --git a/lib/annot_at/atproto/identity.ex b/lib/annot_at/atproto/identity.ex index 911d193..62e607c 100644 --- a/lib/annot_at/atproto/identity.ex +++ b/lib/annot_at/atproto/identity.ex @@ -54,13 +54,19 @@ defmodule AnnotAt.Atproto.Identity do with :ok <- validate_handle(handle), {:ok, did} <- handle_to_did(handle), :ok <- validate_did(did), - {:ok, document} <- did_to_document(did), - {:ok, parsed} <- DIDDocument.parse(document, did), + {:ok, parsed} <- resolve_did(did), :ok <- confirm_bidirectional(parsed, handle) do {:ok, %__MODULE__{did: did, handle: handle, pds_endpoint: parsed.pds_endpoint}} end end + @spec resolve_did(String.t()) :: {:ok, DIDDocument.t()} | {:error, term()} + def resolve_did(did) when is_binary(did) do + with {:ok, doc} <- did_to_document(did) do + DIDDocument.parse(doc, did) + end + end + defp validate_handle(handle) do if Handle.valid?(handle) do :ok diff --git a/lib/annot_at/atproto/standard_site.ex b/lib/annot_at/atproto/standard_site.ex index ae93045..2e14356 100644 --- a/lib/annot_at/atproto/standard_site.ex +++ b/lib/annot_at/atproto/standard_site.ex @@ -6,6 +6,7 @@ defmodule AnnotAt.Atproto.StandardSite do alias AnnotAt.Accounts alias AnnotAt.Atproto.HTTP + alias AnnotAt.Atproto.Identity alias AnnotAt.Atproto.OAuth.Client alias AnnotAt.Atproto.StandardSite.Document alias AnnotAt.Atproto.StandardSite.Publication @@ -145,6 +146,24 @@ defmodule AnnotAt.Atproto.StandardSite do "at://#{did}/site.standard.document/#{rkey}" end + def get_public_document(did, rkey), do: get_public_record(did, @document, rkey) + + def get_public_publication("at://" <> _ = site_uri) do + with {:ok, did, rkey} <- parse_aturi(site_uri, @publication) do + get_public_record(did, @publication, rkey) + end + end + + def get_public_publication(_site), do: {:error, :no_publication} + + def get_public_record(did, collection, rkey) do + with {:ok, did_doc} <- Identity.resolve_did(did), + url = record_url(did_doc.pds_endpoint, did, collection, rkey), + {:ok, %{"value" => value}} <- HTTP.get_json(url) do + {:ok, value, did_doc} + end + end + defp fetch_user(user_id) do case Accounts.get_user(user_id) do nil -> {:error, :no_session} @@ -229,4 +248,18 @@ defmodule AnnotAt.Atproto.StandardSite do String.slice(text, 0, max) end end + + defp record_url(pds, did, collection, rkey) do + query = URI.encode_query(repo: did, collection: collection, rkey: rkey) + "#{pds}/xrpc/com.atproto.repo.getRecord?#{query}" + end + + defp parse_aturi("at://" <> rest, collection) do + case String.split(rest, "/") do + [did, ^collection, rkey] -> {:ok, did, rkey} + _ -> {:error, :invalid} + end + end + + defp parse_aturi(_uri, _collection), do: {:error, :invalid} end diff --git a/lib/annot_at_web/components/document_components.ex b/lib/annot_at_web/components/document_components.ex index f4863ff..2add987 100644 --- a/lib/annot_at_web/components/document_components.ex +++ b/lib/annot_at_web/components/document_components.ex @@ -17,7 +17,7 @@ defmodule AnnotAtWeb.DocumentComponents do :if={@cover_url} src={@cover_url} alt={@doc["title"]} - class="aspect-[1.91/1] w-full rounded-2dxl border-2 border-ink object-cover" + class="aspect-[1.91/1] w-full rounded-2xl border-2 border-ink object-cover" />

{@doc["title"]}

@@ -30,7 +30,7 @@ defmodule AnnotAtWeb.DocumentComponents do /> {@author.display_name || @author.handle} - +
diff --git a/lib/annot_at_web/components/layouts.ex b/lib/annot_at_web/components/layouts.ex index b553e3b..61978ec 100644 --- a/lib/annot_at_web/components/layouts.ex +++ b/lib/annot_at_web/components/layouts.ex @@ -250,4 +250,17 @@ defmodule AnnotAtWeb.Layouts do
""" end + + slot :inner_block, required: true + + def public(assigns) do + ~H""" +
+
+ <.link navigate={~p"/"} class="font-display text-xl font-bold tracking-tight">annot.at +
+
{render_slot(@inner_block)}
+
+ """ + end end diff --git a/lib/annot_at_web/controllers/live/posts_live.ex b/lib/annot_at_web/controllers/live/posts_live.ex index 87bc10f..a668aaa 100644 --- a/lib/annot_at_web/controllers/live/posts_live.ex +++ b/lib/annot_at_web/controllers/live/posts_live.ex @@ -106,7 +106,7 @@ defmodule AnnotAtWeb.PostsLive do {if publishing?(@publishing, entry), do: "Publishing…", else: "Publish"} No - date + date diff --git a/lib/annot_at_web/controllers/public_post_controller.ex b/lib/annot_at_web/controllers/public_post_controller.ex new file mode 100644 index 0000000..35e81f8 --- /dev/null +++ b/lib/annot_at_web/controllers/public_post_controller.ex @@ -0,0 +1,45 @@ +defmodule AnnotAtWeb.PublicPostController do + use AnnotAtWeb, :controller + + alias AnnotAt.Atproto + alias AnnotAt.Atproto.StandardSite + + require Logger + + def show(conn, %{"did" => did, "rkey" => rkey}) do + case StandardSite.get_public_document(did, rkey) do + {:ok, doc, did_doc} -> + render(conn, :show, + doc: doc, + author: author(doc, did_doc), + cover_url: blob(did_doc.pds_endpoint, did, doc["coverImage"]), + page_title: doc["title"] + ) + + {:error, error} -> + Logger.warning("PublicPost: failed to get public doc", reason: error) + + conn + |> put_status(:not_found) + |> put_view(html: AnnotAtWeb.ErrorHTML) + |> render(:"404") + end + end + + defp author(doc, did_doc) do + case StandardSite.get_public_publication(doc["site"]) do + {:ok, pub, pub_doc} -> + %{ + avatar_url: blob(pub_doc.pds_endpoint, pub_doc.did, pub["icon"]), + display_name: pub["name"], + handle: did_doc.handle + } + + {:error, _} -> + %{display_name: nil, handle: did_doc.handle, avatar_url: nil} + end + end + + defp blob(_pds, _did, nil), do: nil + defp blob(pds, did, blob), do: Atproto.blob_url(pds, did, blob) +end diff --git a/lib/annot_at_web/controllers/public_post_html.ex b/lib/annot_at_web/controllers/public_post_html.ex new file mode 100644 index 0000000..92b9be2 --- /dev/null +++ b/lib/annot_at_web/controllers/public_post_html.ex @@ -0,0 +1,6 @@ +defmodule AnnotAtWeb.PublicPostHTML do + use AnnotAtWeb, :html + import AnnotAtWeb.DocumentComponents + + embed_templates "public_post_html/*" +end diff --git a/lib/annot_at_web/controllers/public_post_html/show.html.heex b/lib/annot_at_web/controllers/public_post_html/show.html.heex new file mode 100644 index 0000000..29bf97b --- /dev/null +++ b/lib/annot_at_web/controllers/public_post_html/show.html.heex @@ -0,0 +1,3 @@ + + <.document doc={@doc} author={@author} cover_url={@cover_url} /> + diff --git a/lib/annot_at_web/router.ex b/lib/annot_at_web/router.ex index 6e34fc4..1ab95cf 100644 --- a/lib/annot_at_web/router.ex +++ b/lib/annot_at_web/router.ex @@ -17,6 +17,12 @@ defmodule AnnotAtWeb.Router do plug :accepts, ["json"] end + scope "/", AnnotAtWeb do + pipe_through :browser + + get "/p/:did/:rkey", PublicPostController, :show + end + scope "/", AnnotAtWeb do pipe_through [:browser, :redirect_if_user_is_authenticated] diff --git a/test/annot_at_web/controllers/public_post_controller_test.exs b/test/annot_at_web/controllers/public_post_controller_test.exs new file mode 100644 index 0000000..4fed0bd --- /dev/null +++ b/test/annot_at_web/controllers/public_post_controller_test.exs @@ -0,0 +1,49 @@ +defmodule AnnotAtWeb.PublicPostControllerTest do + use AnnotAtWeb.ConnCase, async: true + use Mimic + + alias AnnotAt.Atproto.DIDDocument + alias AnnotAt.Atproto.StandardSite + + @did "did:plc:ewvi7nxzyoun6zhxrhs64oiz" + @rkey "3mlhhbujc22gw" + + test "GET /p/:did/rkey renders the document", %{conn: conn} do + did_doc = %DIDDocument{did: @did, handle: "jola.dev", pds_endpoint: "https://pds.example"} + + doc = %{ + "title" => "Running local models on M4", + "publishedAt" => "2026-03-23T12:00:00Z", + "site" => "at://#{@did}/site.standard.publication/pub1", + "tags" => ["ai"], + "content" => %{ + "$type" => "org.wordpress.html", + "html" => "

Helloworld.

" + }, + "textContent" => "Hello world." + } + + pub = %{"name" => "jola.dev blog", "icon" => nil} + + expect(StandardSite, :get_public_document, fn @did, @rkey -> {:ok, doc, did_doc} end) + expect(StandardSite, :get_public_publication, fn "at://" <> _ -> {:ok, pub, did_doc} end) + + html = + conn + |> get(~p"/p/#{@did}/#{@rkey}") + |> html_response(200) + + assert html =~ "Running local models on M4" + assert html =~ "world" + assert html =~ "jola.dev blog" + end + + test "GET /p/:did/:rkey 404s when the record can't be loaded", %{conn: conn} do + expect(StandardSite, :get_public_document, fn + @did, @rkey -> {:error, {:http_status, 404}} + end) + + conn = get(conn, ~p"/p/#{@did}/#{@rkey}") + assert response(conn, 404) + end +end -- 2.51.2