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:
parent
39548c3824
commit
ac7ef0999d
@ -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
|
||||||
|
@ -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
|
||||||
|
42
lib/pleroma/web/metadata/utils.ex
Normal file
42
lib/pleroma/web/metadata/utils.ex
Normal 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
|
Loading…
Reference in New Issue
Block a user