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