From ac7ef0999d70145ed5217258a4908eba609d68de Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 11 Feb 2019 23:59:04 +0000 Subject: [PATCH 1/6] WIP: Fix Twitter Cards Twitter cards were not passing any useful metadata. A few things were being handled on Twitter's end by trying to match OpenGraph tags with their own, but it wasn't working at all for media. This is an attempt to fix that. Common functions have been pulled out of opengraph and put into utils. Twitter's functionality was entirely replaced with a direct copy of Opengraph's and then modified as needed. Profiles are now represented as Summary Cards Posts with images are now represented as Summart with Large Image Cards Posts with video and audio attachments are represented as Player Cards. This now passes the Twitter Card Validator. Validator and Docs are below https://cards-dev.twitter.com/validator https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards --- lib/pleroma/web/metadata/opengraph.ex | 58 +++------- lib/pleroma/web/metadata/twitter_card.ex | 128 ++++++++++++++++++----- lib/pleroma/web/metadata/utils.ex | 42 ++++++++ 3 files changed, 157 insertions(+), 71 deletions(-) create mode 100644 lib/pleroma/web/metadata/utils.ex diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex index 190377767..4d6639c84 100644 --- a/lib/pleroma/web/metadata/opengraph.ex +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -3,12 +3,10 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Metadata.Providers.OpenGraph do - alias Pleroma.HTML - alias Pleroma.Formatter alias Pleroma.User alias Pleroma.Web.Metadata - alias Pleroma.Web.MediaProxy alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata.Utils @behaviour Provider @@ -19,7 +17,7 @@ def build_tags(%{ user: user }) do attachments = build_attachments(object) - scrubbed_content = scrub_html_and_truncate(object) + scrubbed_content = Utils.scrub_html_and_truncate(object) # Zero width space content = if scrubbed_content != "" and scrubbed_content != "\u200B" do @@ -44,13 +42,14 @@ def build_tags(%{ {:meta, [ property: "og:description", - content: "#{user_name_string(user)}" <> content + content: "#{Utils.user_name_string(user)}" <> content ], []}, {:meta, [property: "og:type", content: "website"], []} ] ++ if attachments == [] or Metadata.activity_nsfw?(object) do [ - {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []}, + {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], + []}, {:meta, [property: "og:image:width", content: 150], []}, {:meta, [property: "og:image:height", content: 150], []} ] @@ -61,17 +60,17 @@ def build_tags(%{ @impl Provider def build_tags(%{user: user}) do - with truncated_bio = scrub_html_and_truncate(user.bio || "") do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do [ {:meta, [ property: "og:title", - content: user_name_string(user) + content: Utils.user_name_string(user) ], []}, {:meta, [property: "og:url", content: User.profile_url(user)], []}, {:meta, [property: "og:description", content: truncated_bio], []}, {:meta, [property: "og:type", content: "website"], []}, - {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []}, + {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, {:meta, [property: "og:image:width", content: 150], []}, {:meta, [property: "og:image:height", content: 150], []} ] @@ -93,13 +92,14 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do case media_type do "audio" -> [ - {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []} + {:meta, + [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} | acc ] "image" -> [ - {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], + {:meta, [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}, {:meta, [property: "og:image:width", content: 150], []}, {:meta, [property: "og:image:height", content: 150], []} @@ -108,7 +108,8 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do "video" -> [ - {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []} + {:meta, + [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} | acc ] @@ -120,37 +121,4 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do acc ++ rendered_tags end) end - - defp scrub_html_and_truncate(%{data: %{"content" => content}} = object) do - content - # html content comes from DB already encoded, decode first and scrub after - |> HtmlEntities.decode() - |> String.replace(~r//, " ") - |> HTML.get_cached_stripped_html_for_object(object, __MODULE__) - |> Formatter.demojify() - |> Formatter.truncate() - end - - defp scrub_html_and_truncate(content) when is_binary(content) do - content - # html content comes from DB already encoded, decode first and scrub after - |> HtmlEntities.decode() - |> String.replace(~r//, " ") - |> HTML.strip_tags() - |> Formatter.demojify() - |> Formatter.truncate() - end - - defp attachment_url(url) do - MediaProxy.url(url) - end - - defp user_name_string(user) do - "#{user.name} " <> - if user.local do - "(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})" - else - "(@#{user.nickname})" - end - end end diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index 32b979357..0365e4647 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -3,44 +3,120 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Metadata.Providers.TwitterCard do - alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.User alias Pleroma.Web.Metadata + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata.Utils @behaviour Provider @impl Provider - def build_tags(%{object: object}) do - if Metadata.activity_nsfw?(object) or object.data["attachment"] == [] do - build_tags(nil) - else - case find_first_acceptable_media_type(object) do - "image" -> - [{:meta, [property: "twitter:card", content: "summary_large_image"], []}] - - "audio" -> - [{:meta, [property: "twitter:card", content: "player"], []}] - - "video" -> - [{:meta, [property: "twitter:card", content: "player"], []}] - - _ -> - build_tags(nil) + def build_tags(%{ + object: object, + user: user + }) do + attachments = build_attachments(object) + scrubbed_content = Utils.scrub_html_and_truncate(object) + # Zero width space + content = + if scrubbed_content != "" and scrubbed_content != "\u200B" do + "“" <> scrubbed_content <> "”" + else + "" + end + + [ + {:meta, + [ + property: "twitter:title", + content: Utils.user_name_string(user) + ], []}, + {:meta, + [ + property: "twitter:description", + content: content + ], []} + ] ++ + if attachments == [] or Metadata.activity_nsfw?(object) do + [ + {:meta, + [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []}, + {:meta, [property: "twitter:card", content: "summary_large_image"], []} + ] + else + attachments end - end end @impl Provider - def build_tags(_) do - [{:meta, [property: "twitter:card", content: "summary"], []}] + def build_tags(%{user: user}) do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do + [ + {:meta, + [ + property: "twitter:title", + content: Utils.user_name_string(user) + ], []}, + {:meta, [property: "twitter:description", content: truncated_bio], []}, + {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], + []}, + {:meta, [property: "twitter:card", content: "summary"], []} + ] + end end - def find_first_acceptable_media_type(%{data: %{"attachment" => attachment}}) do - Enum.find_value(attachment, fn attachment -> - Enum.find_value(attachment["url"], fn url -> - Enum.find(["image", "audio", "video"], fn media_type -> - String.starts_with?(url["mediaType"], media_type) + defp build_attachments(%{data: %{"attachment" => attachments}}) do + Enum.reduce(attachments, [], fn attachment, acc -> + rendered_tags = + Enum.reduce(attachment["url"], [], fn url, acc -> + content_type = url["mediaType"] + + media_type = + Enum.find(["image", "audio", "video"], fn media_type -> + String.starts_with?(url["mediaType"], media_type) + end) + + # TODO: Add additional properties to objects when we have the data available. + case media_type do + "audio" -> + [ + {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, [property: "twitter:player", content: Utils.attachment_url(url["href"])], + []} + | acc + ] + + "image" -> + [ + {:meta, [property: "twitter:card", content: "summary_large_image"], []}, + {:meta, + [ + property: "twitter:player", + content: + Utils.attachment_url( + Pleroma.Uploaders.Uploader.preview_url(content_type, url["href"]) + ) + ], []} + | acc + ] + + # TODO: Need the true width and height values here or Twitter renders an iFrame with a bad aspect ratio + "video" -> + [ + {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, [property: "twitter:player", content: Utils.attachment_url(url["href"])], + []}, + {:meta, [property: "twitter:player:width", content: "1280"], []}, + {:meta, [property: "twitter:player:height", content: "720"], []} + | acc + ] + + _ -> + acc + end end) - end) + + acc ++ rendered_tags end) end end diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex new file mode 100644 index 000000000..a166800d4 --- /dev/null +++ b/lib/pleroma/web/metadata/utils.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright \xc2\xa9 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Utils do + alias Pleroma.HTML + alias Pleroma.Formatter + alias Pleroma.Web.MediaProxy + + def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do + content + # html content comes from DB already encoded, decode first and scrub after + |> HtmlEntities.decode() + |> String.replace(~r//, " ") + |> HTML.get_cached_stripped_html_for_object(object, __MODULE__) + |> Formatter.demojify() + |> Formatter.truncate() + end + + def scrub_html_and_truncate(content) when is_binary(content) do + content + # html content comes from DB already encoded, decode first and scrub after + |> HtmlEntities.decode() + |> String.replace(~r//, " ") + |> HTML.strip_tags() + |> Formatter.demojify() + |> Formatter.truncate() + end + + def attachment_url(url) do + MediaProxy.url(url) + end + + def user_name_string(user) do + "#{user.name} " <> + if user.local do + "(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})" + else + "(@#{user.nickname})" + end + end +end From 4956ab5ea30e49147947ba321c1bf93d38d790bd Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 12 Feb 2019 00:25:12 +0000 Subject: [PATCH 2/6] Fix compile --- lib/pleroma/web/metadata/twitter_card.ex | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index 0365e4647..13fa22240 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -69,8 +69,6 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = Enum.reduce(attachment["url"], [], fn url, acc -> - content_type = url["mediaType"] - media_type = Enum.find(["image", "audio", "video"], fn media_type -> String.starts_with?(url["mediaType"], media_type) @@ -92,10 +90,7 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do {:meta, [ property: "twitter:player", - content: - Utils.attachment_url( - Pleroma.Uploaders.Uploader.preview_url(content_type, url["href"]) - ) + content: Utils.attachment_url(url["href"]) ], []} | acc ] From c984e8272a89276a805226be974af5d916a930ee Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 12 Feb 2019 00:37:22 +0000 Subject: [PATCH 3/6] Formatting --- lib/pleroma/web/metadata/opengraph.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex index 4d6639c84..cafb8134b 100644 --- a/lib/pleroma/web/metadata/opengraph.ex +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -99,8 +99,8 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do "image" -> [ - {:meta, [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], - []}, + {:meta, + [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}, {:meta, [property: "og:image:width", content: 150], []}, {:meta, [property: "og:image:height", content: 150], []} | acc From 10a11f083ca543022c61e24b1ec0cc83811d6d06 Mon Sep 17 00:00:00 2001 From: href Date: Tue, 19 Feb 2019 17:39:42 +0100 Subject: [PATCH 4/6] Embed player suitable for Twitter Cards --- lib/pleroma/web/metadata/player_view.ex | 21 +++++++++++++++++++ lib/pleroma/web/metadata/twitter_card.ex | 19 +++++++++++------ lib/pleroma/web/ostatus/ostatus_controller.ex | 20 ++++++++++++++++++ lib/pleroma/web/router.ex | 1 + .../templates/layout/metadata_player.html.eex | 16 ++++++++++++++ 5 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 lib/pleroma/web/metadata/player_view.ex create mode 100644 lib/pleroma/web/templates/layout/metadata_player.html.eex diff --git a/lib/pleroma/web/metadata/player_view.ex b/lib/pleroma/web/metadata/player_view.ex new file mode 100644 index 000000000..68b0a3507 --- /dev/null +++ b/lib/pleroma/web/metadata/player_view.ex @@ -0,0 +1,21 @@ +defmodule Pleroma.Web.Metadata.PlayerView do + use Pleroma.Web, :view + import Phoenix.HTML.Tag, only: [content_tag: 3, tag: 2] + + def render("player.html", %{"mediaType" => type, "href" => href}) do + tag_type = + case type do + "audio" <> _ -> :audio + "video" <> _ -> :video + end + + content_tag( + tag_type, + [ + tag(:source, src: href, type: type), + "Your browser does not support #{type} playback." + ], + controls: true + ) + end +end diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index 13fa22240..e7f5760a9 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -12,10 +12,11 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do @impl Provider def build_tags(%{ + activity_id: id, object: object, user: user }) do - attachments = build_attachments(object) + attachments = build_attachments(id, object) scrubbed_content = Utils.scrub_html_and_truncate(object) # Zero width space content = @@ -65,7 +66,9 @@ def build_tags(%{user: user}) do end end - defp build_attachments(%{data: %{"attachment" => attachments}}) do + defp build_attachments(id, z = %{data: %{"attachment" => attachments}}) do + IO.puts(inspect(z)) + Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = Enum.reduce(attachment["url"], [], fn url, acc -> @@ -79,8 +82,9 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do "audio" -> [ {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, [property: "twitter:player", content: Utils.attachment_url(url["href"])], - []} + {:meta, [property: "twitter:player:width", content: "480"], []}, + {:meta, [property: "twitter:player:height", content: "80"], []}, + {:meta, [property: "twitter:player", content: player_url(id)], []} | acc ] @@ -99,8 +103,7 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do "video" -> [ {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, [property: "twitter:player", content: Utils.attachment_url(url["href"])], - []}, + {:meta, [property: "twitter:player", content: player_url(id)], []}, {:meta, [property: "twitter:player:width", content: "1280"], []}, {:meta, [property: "twitter:player:height", content: "720"], []} | acc @@ -114,4 +117,8 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do acc ++ rendered_tags end) end + + defp player_url(id) do + Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id) + end end diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index db4c8f4da..e7bde28a6 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -153,6 +153,7 @@ def notice(conn, %{"id" => id}) do %Object{} = object = Object.normalize(activity.data["object"]) Fallback.RedirectController.redirector_with_meta(conn, %{ + activity_id: activity.id, object: object, url: Pleroma.Web.Router.Helpers.o_status_url( @@ -184,6 +185,25 @@ def notice(conn, %{"id" => id}) do end end + # Returns an HTML embedded