WIP: Fix Twitter Cards
authorMark Felder <feld@FreeBSD.org>
Mon, 11 Feb 2019 23:59:04 +0000 (23:59 +0000)
committerMark Felder <feld@FreeBSD.org>
Mon, 11 Feb 2019 23:59:04 +0000 (23:59 +0000)
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
lib/pleroma/web/metadata/twitter_card.ex
lib/pleroma/web/metadata/utils.ex [new file with mode: 0644]

index 190377767ddf4c534cecbcc4be0c2a9759ddfb20..4d6639c842960853265653830bcaf0f962d57a91 100644 (file)
@@ -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 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
         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 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
       {: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 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
 
   @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 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph 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 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph 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 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph 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/<br\s?\/?>/, " ")
-    |> 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/<br\s?\/?>/, " ")
-    |> 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
index 32b979357b30bd93d5097954bae903efdb3e8a3d..0365e464752978f60180420cde00db6c4c9a19ce 100644 (file)
 # 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 (file)
index 0000000..a166800
--- /dev/null
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright \xc2\xa9 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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/<br\s?\/?>/, " ")
+    |> 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/<br\s?\/?>/, " ")
+    |> 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