diff --git a/js/public/index.html b/js/public/index.html index 86e3871f2..346854e2f 100644 --- a/js/public/index.html +++ b/js/public/index.html @@ -8,6 +8,7 @@ mobilizon + diff --git a/js/vue.config.js b/js/vue.config.js index b76081558..b3363d711 100644 --- a/js/vue.config.js +++ b/js/vue.config.js @@ -19,4 +19,19 @@ module.exports = { ], }, }, + chainWebpack: config => { + config + .plugin('html') + .tap(args => { + args[0].minify = { + collapseWhitespace: true, + removeComments: false, + removeRedundantAttributes: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true, + useShortDoctype: true + }; + return args + }); + } }; diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 5ce7c0db1..9cae6b29e 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -378,6 +378,12 @@ defmodule Mobilizon.Actors.Actor do end end + def display_name_and_username(%Actor{name: nil} = actor), do: actor_acct_from_actor(actor) + def display_name_and_username(%Actor{name: ""} = actor), do: actor_acct_from_actor(actor) + + def display_name_and_username(%Actor{name: name} = actor), + do: name <> " (" <> actor_acct_from_actor(actor) <> ")" + def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do Cachex.del(:activity_pub, "actor_" <> preferred_username) end diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 92a4e40af..181e61137 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -467,6 +467,20 @@ defmodule Mobilizon.Actors do |> Repo.preload(:organized_events) end + @doc """ + Returns a cached local actor by username + """ + @spec get_cached_local_actor_by_name(String.t()) :: + {:ok, Actor.t()} | {:commit, Actor.t()} | {:ignore, any()} + def get_cached_local_actor_by_name(name) do + Cachex.fetch(:activity_pub, "actor_" <> name, fn "actor_" <> name -> + case get_local_actor_by_name(name) do + nil -> {:ignore, nil} + %Actor{} = actor -> {:commit, actor} + end + end) + end + @doc """ Getting an actor from url, eventually creating it """ diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 6bf3ea1ec..f9fd63770 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -154,6 +154,16 @@ defmodule Mobilizon.Events do |> Repo.one() end + def get_cached_event_full_by_uuid(uuid) do + Cachex.fetch(:activity_pub, "event_" <> uuid, fn "event_" <> uuid -> + with %Event{} = event <- get_event_full_by_uuid(uuid) do + {:commit, event} + else + _ -> {:ignore, nil} + end + end) + end + @doc """ Gets a single event, with all associations loaded. """ @@ -1018,6 +1028,16 @@ defmodule Mobilizon.Events do end end + def get_cached_comment_full_by_uuid("comment_" <> uuid) do + Cachex.fetch(:activity_pub, "comment_" <> uuid, fn "comment_" <> uuid -> + with %Comment{} = comment <- Events.get_comment_full_from_uuid(uuid) do + {:commit, comment} + else + _ -> {:ignore, nil} + end + end) + end + def get_comment_from_url(url), do: Repo.get_by(Comment, url: url) def get_comment_from_url!(url), do: Repo.get_by!(Comment, url: url) diff --git a/lib/mobilizon_web/controllers/activity_pub_controller.ex b/lib/mobilizon_web/controllers/activity_pub_controller.ex index 76670b69b..870554108 100644 --- a/lib/mobilizon_web/controllers/activity_pub_controller.ex +++ b/lib/mobilizon_web/controllers/activity_pub_controller.ex @@ -16,101 +16,55 @@ defmodule MobilizonWeb.ActivityPubController do action_fallback(:errors) - @activity_pub_headers [ - "application/activity+json", - "application/activity+json, application/ld+json" - ] - @doc """ - Show an Actor's ActivityPub representation + Renders an Actor ActivityPub's representation """ - @spec actor(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec actor(Plug.Conn.t(), String.t()) :: Plug.Conn.t() def actor(conn, %{"name" => name}) do - if conn |> get_req_header("accept") |> is_ap_header() do - render_cached_actor(conn, name) - else + with {status, %Actor{} = actor} when status in [:ok, :commit] <- + Actors.get_cached_local_actor_by_name(name) do conn - |> put_resp_content_type("text/html") - |> send_file(200, "priv/static/index.html") - end - end - - @spec render_cached_actor(Plug.Conn.t(), String.t()) :: Plug.Conn.t() - defp render_cached_actor(conn, name) do - case Cachex.fetch(:activity_pub, "actor_" <> name, &get_local_actor_by_name/1) do - {status, %Actor{} = actor} when status in [:ok, :commit] -> - conn - |> put_resp_header("content-type", "application/activity+json") - |> json(ActorView.render("actor.json", %{actor: actor})) - + |> put_resp_header("content-type", "application/activity+json") + |> json(ActorView.render("actor.json", %{actor: actor})) + else {:ignore, _} -> {:error, :not_found} end end - defp get_local_actor_by_name("actor_" <> name) do - case Actors.get_local_actor_by_name(name) do - nil -> {:ignore, nil} - %Actor{} = actor -> {:commit, actor} - end - end - - # Test if the request has an AP header - defp is_ap_header(ap_headers) do - length(@activity_pub_headers -- ap_headers) < 2 - end - @doc """ Renders an Event ActivityPub's representation """ @spec event(Plug.Conn.t(), map()) :: Plug.Conn.t() def event(conn, %{"uuid" => uuid}) do - case Cachex.fetch(:activity_pub, "event_" <> uuid, &get_event_full_by_uuid/1) do - {status, %Event{} = event} when status in [:ok, :commit] -> - conn - |> put_resp_header("content-type", "application/activity+json") - |> json(ObjectView.render("event.json", %{event: event |> Utils.make_event_data()})) - + with {status, %Event{}} = event when status in [:ok, :commit] <- + Events.get_cached_event_full_by_uuid(uuid), + true <- event.visibility in [:public, :unlisted] do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(ObjectView.render("event.json", %{event: event |> Utils.make_event_data()})) + else {:ignore, _} -> {:error, :not_found} end end - defp get_event_full_by_uuid("event_" <> uuid) do - with %Event{} = event <- Events.get_event_full_by_uuid(uuid), - true <- event.visibility in [:public, :unlisted] do - {:commit, event} - else - _ -> {:ignore, nil} - end - end - @doc """ Renders a Comment ActivityPub's representation """ @spec comment(Plug.Conn.t(), map()) :: Plug.Conn.t() def comment(conn, %{"uuid" => uuid}) do - case Cachex.fetch(:activity_pub, "comment_" <> uuid, &get_comment_full_by_uuid/1) do - {status, %Comment{} = comment} when status in [:ok, :commit] -> - conn - |> put_resp_header("content-type", "application/activity+json") - |> json( - ObjectView.render("comment.json", %{comment: comment |> Utils.make_comment_data()}) - ) - - {:ignore, _} -> - {:error, :not_found} - end - end - - defp get_comment_full_by_uuid("comment_" <> uuid) do - with %Comment{} = comment <- Events.get_comment_full_from_uuid(uuid) do + with {status, %Comment{} = comment} when status in [:ok, :commit] <- + Events.get_cached_comment_full_by_uuid(uuid) do # Comments are always public for now # TODO : Make comments maybe restricted # true <- comment.public do - {:commit, comment} + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(ObjectView.render("comment.json", %{comment: comment |> Utils.make_comment_data()})) else - _ -> {:ignore, nil} + {:ignore, _} -> + {:error, :not_found} end end diff --git a/lib/mobilizon_web/controllers/page_controller.ex b/lib/mobilizon_web/controllers/page_controller.ex index c68f79bf6..a3e755a2e 100644 --- a/lib/mobilizon_web/controllers/page_controller.ex +++ b/lib/mobilizon_web/controllers/page_controller.ex @@ -3,6 +3,11 @@ defmodule MobilizonWeb.PageController do Controller to load our webapp """ use MobilizonWeb, :controller + alias Mobilizon.Service.Metadata + alias Mobilizon.Actors + alias Mobilizon.Actors.Actor + alias Mobilizon.Events + alias Mobilizon.Events.{Event, Comment} plug(:put_layout, false) @@ -11,4 +16,71 @@ defmodule MobilizonWeb.PageController do |> put_resp_content_type("text/html") |> send_file(200, "priv/static/index.html") end + + def actor(conn, %{"name" => name}) do + case get_format(conn) do + "html" -> + with {status, %Actor{} = actor} when status in [:ok, :commit] <- + Actors.get_cached_local_actor_by_name(name) do + render_with_meta(conn, actor) + end + + "activity+json" -> + MobilizonWeb.ActivityPubController.call(conn, :actor) + + _ -> + {:error, :not_found} + end + end + + def event(conn, %{"uuid" => uuid}) do + case get_format(conn) do + "html" -> + with {status, %Event{} = event} when status in [:ok, :commit] <- + Events.get_cached_event_full_by_uuid(uuid), + true <- event.visibility in [:public, :unlisted] do + render_with_meta(conn, event) + end + + "activity+json" -> + MobilizonWeb.ActivityPubController.call(conn, :event) + + _ -> + {:error, :not_found} + end + end + + def comment(conn, %{"uuid" => uuid}) do + case get_format(conn) do + "html" -> + with {status, %Comment{} = comment} when status in [:ok, :commit] <- + Events.get_cached_comment_full_by_uuid(uuid) do + # Comments are always public for now + # TODO : Make comments maybe restricted + # true <- comment.public do + render_with_meta(conn, comment) + end + + "activity+json" -> + MobilizonWeb.ActivityPubController.call(conn, :comment) + + _ -> + {:error, :not_found} + end + end + + # Inject OpenGraph information + defp render_with_meta(conn, object) do + {:ok, index_content} = File.read(index_file_path()) + tags = Metadata.build_tags(object) + response = String.replace(index_content, "", tags) + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, response) + end + + defp index_file_path() do + Path.join(Application.app_dir(:mobilizon, "priv/static/"), "index.html") + end end diff --git a/lib/mobilizon_web/router.ex b/lib/mobilizon_web/router.ex index e41a0cdb0..73d996004 100644 --- a/lib/mobilizon_web/router.ex +++ b/lib/mobilizon_web/router.ex @@ -19,6 +19,10 @@ defmodule MobilizonWeb.Router do end pipeline :activity_pub do + plug(:accepts, ["activity-json"]) + end + + pipeline :activity_pub_and_html do plug(:accepts, ["activity-json", "html"]) end @@ -61,15 +65,19 @@ defmodule MobilizonWeb.Router do get("/@:name/feed/:format", FeedController, :actor) end + scope "/", MobilizonWeb do + pipe_through(:activity_pub_and_html) + get("/@:name", PageController, :actor) + get("/events/:uuid", PageController, :event) + get("/comments/:uuid", PageController, :comment) + end + scope "/", MobilizonWeb do pipe_through(:activity_pub) - get("/@:name", ActivityPubController, :actor) get("/@:name/outbox", ActivityPubController, :outbox) get("/@:name/following", ActivityPubController, :following) get("/@:name/followers", ActivityPubController, :followers) - get("/events/:uuid", ActivityPubController, :event) - get("/comments/:uuid", ActivityPubController, :comment) end scope "/", MobilizonWeb do diff --git a/lib/service/metadata.ex b/lib/service/metadata.ex new file mode 100644 index 000000000..bd3ba99b1 --- /dev/null +++ b/lib/service/metadata.ex @@ -0,0 +1,6 @@ +defprotocol Mobilizon.Service.Metadata do + @doc """ + Build tags + """ + def build_tags(entity) +end diff --git a/lib/service/metadata/actor.ex b/lib/service/metadata/actor.ex new file mode 100644 index 000000000..4a51e2f96 --- /dev/null +++ b/lib/service/metadata/actor.ex @@ -0,0 +1,25 @@ +defimpl Mobilizon.Service.Metadata, for: Mobilizon.Actors.Actor do + alias Phoenix.HTML + alias Phoenix.HTML.Tag + alias Mobilizon.Actors.Actor + require Logger + + def build_tags(%Actor{} = actor) do + actor + |> do_build_tags() + |> Enum.map(&HTML.safe_to_string/1) + |> Enum.reduce("", fn tag, acc -> acc <> tag end) + end + + defp do_build_tags(%Actor{} = actor) do + [ + Tag.tag(:meta, property: "og:title", content: Actor.display_name_and_username(actor)), + Tag.tag(:meta, property: "og:url", content: actor.url), + Tag.tag(:meta, property: "og:description", content: actor.summary), + Tag.tag(:meta, property: "og:type", content: "profile"), + Tag.tag(:meta, property: "profile:username", content: actor.preferred_username), + Tag.tag(:meta, property: "og:image", content: actor.avatar_url), + Tag.tag(:meta, property: "twitter:card", content: "summary") + ] + end +end diff --git a/lib/service/metadata/comment.ex b/lib/service/metadata/comment.ex new file mode 100644 index 000000000..dfa504579 --- /dev/null +++ b/lib/service/metadata/comment.ex @@ -0,0 +1,22 @@ +defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Comment do + alias Phoenix.HTML + alias Phoenix.HTML.Tag + alias Mobilizon.Events.Comment + + def build_tags(%Comment{} = comment) do + comment + |> do_build_tags() + |> Enum.map(&HTML.safe_to_string/1) + |> Enum.reduce("", fn tag, acc -> acc <> tag end) + end + + defp do_build_tags(%Comment{} = comment) do + [ + Tag.tag(:meta, property: "og:title", content: comment.actor.preferred_username), + Tag.tag(:meta, property: "og:url", content: comment.url), + Tag.tag(:meta, property: "og:description", content: comment.text), + Tag.tag(:meta, property: "og:type", content: "website"), + Tag.tag(:meta, property: "twitter:card", content: "summary") + ] + end +end diff --git a/lib/service/metadata/event.ex b/lib/service/metadata/event.ex new file mode 100644 index 000000000..8a63d7b84 --- /dev/null +++ b/lib/service/metadata/event.ex @@ -0,0 +1,24 @@ +defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do + alias Phoenix.HTML + alias Phoenix.HTML.Tag + alias Mobilizon.Events.Event + + def build_tags(%Event{} = event) do + event + |> do_build_tags() + |> Enum.map(&HTML.safe_to_string/1) + |> Enum.reduce("", fn tag, acc -> acc <> tag end) + end + + defp do_build_tags(%Event{} = event) do + [ + Tag.tag(:meta, property: "og:title", content: event.title), + Tag.tag(:meta, property: "og:url", content: event.url), + Tag.tag(:meta, property: "og:description", content: event.description), + Tag.tag(:meta, property: "og:type", content: "website"), + Tag.tag(:meta, property: "og:image", content: event.thumbnail), + Tag.tag(:meta, property: "og:image", content: event.large_image), + Tag.tag(:meta, property: "twitter:card", content: "summary_large_image") + ] + end +end