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
This commit is contained in:
Mark Felder 2019-02-11 23:59:04 +00:00
parent 39548c3824
commit ac7ef0999d
3 changed files with 157 additions and 71 deletions

View File

@ -3,12 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.OpenGraph do defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
alias Pleroma.HTML
alias Pleroma.Formatter
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.Metadata alias Pleroma.Web.Metadata
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.Metadata.Providers.Provider alias Pleroma.Web.Metadata.Providers.Provider
alias Pleroma.Web.Metadata.Utils
@behaviour Provider @behaviour Provider
@ -19,7 +17,7 @@ def build_tags(%{
user: user user: user
}) do }) do
attachments = build_attachments(object) attachments = build_attachments(object)
scrubbed_content = scrub_html_and_truncate(object) scrubbed_content = Utils.scrub_html_and_truncate(object)
# Zero width space # Zero width space
content = content =
if scrubbed_content != "" and scrubbed_content != "\u200B" do if scrubbed_content != "" and scrubbed_content != "\u200B" do
@ -44,13 +42,14 @@ def build_tags(%{
{:meta, {:meta,
[ [
property: "og:description", property: "og:description",
content: "#{user_name_string(user)}" <> content content: "#{Utils.user_name_string(user)}" <> content
], []}, ], []},
{:meta, [property: "og:type", content: "website"], []} {:meta, [property: "og:type", content: "website"], []}
] ++ ] ++
if attachments == [] or Metadata.activity_nsfw?(object) do 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:width", content: 150], []},
{:meta, [property: "og:image:height", content: 150], []} {:meta, [property: "og:image:height", content: 150], []}
] ]
@ -61,17 +60,17 @@ def build_tags(%{
@impl Provider @impl Provider
def build_tags(%{user: user}) do 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, {:meta,
[ [
property: "og:title", 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:url", content: User.profile_url(user)], []},
{:meta, [property: "og:description", content: truncated_bio], []}, {:meta, [property: "og:description", content: truncated_bio], []},
{:meta, [property: "og:type", content: "website"], []}, {: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:width", content: 150], []},
{:meta, [property: "og:image:height", content: 150], []} {:meta, [property: "og:image:height", content: 150], []}
] ]
@ -93,13 +92,14 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do
case media_type do case media_type do
"audio" -> "audio" ->
[ [
{:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []} {:meta,
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}
| acc | acc
] ]
"image" -> "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:width", content: 150], []},
{:meta, [property: "og:image:height", content: 150], []} {:meta, [property: "og:image:height", content: 150], []}
@ -108,7 +108,8 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do
"video" -> "video" ->
[ [
{:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []} {:meta,
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}
| acc | acc
] ]
@ -120,37 +121,4 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do
acc ++ rendered_tags acc ++ rendered_tags
end) end)
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 end

View File

@ -3,44 +3,120 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.TwitterCard do defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
alias Pleroma.Web.Metadata.Providers.Provider alias Pleroma.User
alias Pleroma.Web.Metadata alias Pleroma.Web.Metadata
alias Pleroma.Web.Metadata.Providers.Provider
alias Pleroma.Web.Metadata.Utils
@behaviour Provider @behaviour Provider
@impl Provider @impl Provider
def build_tags(%{object: object}) do def build_tags(%{
if Metadata.activity_nsfw?(object) or object.data["attachment"] == [] do object: object,
build_tags(nil) 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 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)
end 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 end
@impl Provider @impl Provider
def build_tags(_) do def build_tags(%{user: user}) do
[{:meta, [property: "twitter:card", content: "summary"], []}] 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 end
def find_first_acceptable_media_type(%{data: %{"attachment" => attachment}}) do defp build_attachments(%{data: %{"attachment" => attachments}}) do
Enum.find_value(attachment, fn attachment -> Enum.reduce(attachments, [], fn attachment, acc ->
Enum.find_value(attachment["url"], fn url -> rendered_tags =
Enum.reduce(attachment["url"], [], fn url, acc ->
content_type = url["mediaType"]
media_type =
Enum.find(["image", "audio", "video"], fn media_type -> Enum.find(["image", "audio", "video"], fn media_type ->
String.starts_with?(url["mediaType"], media_type) String.starts_with?(url["mediaType"], media_type)
end) 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 end
end end

View File

@ -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