Fix Twitter Cards See merge request pleroma/pleroma!815tags/v1.1.4
@@ -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,14 +92,15 @@ 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], []} | |||
| acc | |||
@@ -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 |
@@ -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, tag_attrs} = | |||
case type do | |||
"audio" <> _ -> {:audio, []} | |||
"video" <> _ -> {:video, [loop: true]} | |||
end | |||
content_tag( | |||
tag_type, | |||
[ | |||
tag(:source, src: href, type: type), | |||
"Your browser does not support #{type} playback." | |||
], | |||
[controls: true] ++ tag_attrs | |||
) | |||
end | |||
end |
@@ -3,44 +3,122 @@ | |||
# 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(%{ | |||
activity_id: id, | |||
object: object, | |||
user: user | |||
}) do | |||
attachments = build_attachments(id, 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(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 -> | |||
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:width", content: "480"], []}, | |||
{:meta, [property: "twitter:player:height", content: "80"], []}, | |||
{:meta, [property: "twitter:player", content: player_url(id)], []} | |||
| acc | |||
] | |||
"image" -> | |||
[ | |||
{:meta, [property: "twitter:card", content: "summary_large_image"], []}, | |||
{:meta, | |||
[ | |||
property: "twitter:player", | |||
content: Utils.attachment_url(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: player_url(id)], []}, | |||
{:meta, [property: "twitter:player:width", content: "480"], []}, | |||
{:meta, [property: "twitter:player:height", content: "480"], []} | |||
| acc | |||
] | |||
_ -> | |||
acc | |||
end | |||
end) | |||
end) | |||
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 |
@@ -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 |
@@ -156,6 +156,7 @@ defmodule Pleroma.Web.OStatus.OStatusController 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( | |||
@@ -187,6 +188,30 @@ defmodule Pleroma.Web.OStatus.OStatusController do | |||
end | |||
end | |||
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes. | |||
def notice_player(conn, %{"id" => id}) do | |||
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), | |||
true <- ActivityPub.is_public?(activity), | |||
%Object{} = object <- Object.normalize(activity.data["object"]), | |||
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, | |||
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do | |||
conn | |||
|> put_layout(:metadata_player) | |||
|> put_resp_header("x-frame-options", "ALLOW") | |||
|> put_resp_header( | |||
"content-security-policy", | |||
"default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" | |||
) | |||
|> put_view(Pleroma.Web.Metadata.PlayerView) | |||
|> render("player.html", url) | |||
else | |||
_error -> | |||
conn | |||
|> put_status(404) | |||
|> Fallback.RedirectController.redirector(nil, 404) | |||
end | |||
end | |||
defp represent_activity( | |||
conn, | |||
"activity+json", | |||
@@ -505,6 +505,7 @@ defmodule Pleroma.Web.Router do | |||
get("/objects/:uuid", OStatus.OStatusController, :object) | |||
get("/activities/:uuid", OStatus.OStatusController, :activity) | |||
get("/notice/:id", OStatus.OStatusController, :notice) | |||
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player) | |||
get("/users/:nickname/feed", OStatus.OStatusController, :feed) | |||
get("/users/:nickname", OStatus.OStatusController, :feed_redirect) | |||
@@ -0,0 +1,16 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<body> | |||
<style type="text/css"> | |||
video, audio { | |||
width:100%; | |||
max-width:600px; | |||
height: auto; | |||
} | |||
</style> | |||
<%= render @view_module, @view_template, assigns %> | |||
</body> | |||
</html> |