Merge branch 'feature/rich-media-part-2-electric-boogaloo' into 'develop'

Rich Media support, part 2.

See merge request pleroma/pleroma!719
This commit is contained in:
Haelwenn 2019-01-29 05:11:08 +00:00
commit ebb3496386
17 changed files with 161 additions and 133 deletions

View File

@ -69,7 +69,7 @@ def extract_first_external_url(object, content) do
|> Floki.attribute("a", "href") |> Floki.attribute("a", "href")
|> Enum.at(0) |> Enum.at(0)
{:commit, result} {:commit, {:ok, result}}
end) end)
end end
end end

View File

@ -88,6 +88,10 @@ def insert(map, local \\ true) when is_map(map) do
recipients: recipients recipients: recipients
}) })
Task.start(fn ->
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end)
Notification.create_notifications(activity) Notification.create_notifications(activity)
stream_out(activity) stream_out(activity)
{:ok, activity} {:ok, activity}

View File

@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.{Repo, Object, Activity, User, Notification, Stats} alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.HTML
alias Pleroma.Web.MastodonAPI.{ alias Pleroma.Web.MastodonAPI.{
StatusView, StatusView,
@ -500,7 +499,8 @@ def update_media(%{assigns: %{user: user}} = conn, data) do
def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <- with {:ok, object} <-
ActivityPub.upload(file, ActivityPub.upload(
file,
actor: User.ap_id(user), actor: User.ap_id(user),
description: Map.get(data, "description") description: Map.get(data, "description")
) do ) do
@ -1101,7 +1101,9 @@ def login(conn, %{"code" => code}) do
def login(conn, _) do def login(conn, _) do
with {:ok, app} <- get_or_make_app() do with {:ok, app} <- get_or_make_app() do
path = path =
o_auth_path(conn, :authorize, o_auth_path(
conn,
:authorize,
response_type: "code", response_type: "code",
client_id: app.client_id, client_id: app.client_id,
redirect_uri: ".", redirect_uri: ".",
@ -1342,27 +1344,20 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do
end end
end end
def get_status_card(status_id) do
with %Activity{} = activity <- Repo.get(Activity, status_id),
true <- ActivityPub.is_public?(activity),
%Object{} = object <- Object.normalize(activity.data["object"]),
page_url <- HTML.extract_first_external_url(object, object.data["content"]),
{:ok, rich_media} <- Pleroma.Web.RichMedia.Parser.parse(page_url) do
page_url = rich_media[:url] || page_url
site_name = rich_media[:site_name] || URI.parse(page_url).host
rich_media
|> Map.take([:image, :title, :description])
|> Map.put(:type, "link")
|> Map.put(:provider_name, site_name)
|> Map.put(:url, page_url)
else
_ -> %{}
end
end
def status_card(conn, %{"id" => status_id}) do def status_card(conn, %{"id" => status_id}) do
json(conn, get_status_card(status_id)) with %Activity{} = activity <- Repo.get(Activity, status_id),
true <- ActivityPub.is_public?(activity) do
data =
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
)
json(conn, data)
else
_e ->
%{}
end
end end
def try_render(conn, target, params) def try_render(conn, target, params)

View File

@ -139,6 +139,8 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
__MODULE__ __MODULE__
) )
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
%{ %{
id: to_string(activity.id), id: to_string(activity.id),
uri: object["id"], uri: object["id"],
@ -147,6 +149,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil, reblog: nil,
card: card,
content: content, content: content,
created_at: created_at, created_at: created_at,
reblogs_count: announcement_count, reblogs_count: announcement_count,
@ -175,6 +178,29 @@ def render("status.json", _) do
nil nil
end end
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
page_url = rich_media[:url] || page_url
page_url_data = URI.parse(page_url)
site_name = rich_media[:site_name] || page_url_data.host
%{
type: "link",
provider_name: site_name,
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url,
image: rich_media[:image] |> MediaProxy.url(),
title: rich_media[:title],
description: rich_media[:description],
pleroma: %{
opengraph: rich_media
}
}
end
def render("card.json", _) do
nil
end
def render("attachment.json", %{attachment: attachment}) do def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"] [attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image" media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"

View File

@ -1,17 +0,0 @@
defmodule Pleroma.Web.RichMedia.RichMediaController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
def parse(conn, %{"url" => url}) do
case Pleroma.Web.RichMedia.Parser.parse(url) do
{:ok, data} ->
conn
|> json_response(200, data)
{:error, msg} ->
conn
|> json_response(404, msg)
end
end
end

View File

@ -0,0 +1,18 @@
# Pleroma: A lightweight social networking server
# Copyright _ 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Helpers do
alias Pleroma.{Activity, Object, HTML}
alias Pleroma.Web.RichMedia.Parser
def fetch_data_for_activity(%Activity{} = activity) do
with %Object{} = object <- Object.normalize(activity.data["object"]),
{:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]),
{:ok, rich_media} <- Parser.parse(page_url) do
%{page_url: page_url, rich_media: rich_media}
else
_ -> %{}
end
end
end

View File

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser do defmodule Pleroma.Web.RichMedia.Parser do
@parsers [ @parsers [
Pleroma.Web.RichMedia.Parsers.OGP, Pleroma.Web.RichMedia.Parsers.OGP,
@ -11,19 +15,26 @@ def parse(nil), do: {:error, "No URL provided"}
def parse(url), do: parse_url(url) def parse(url), do: parse_url(url)
else else
def parse(url) do def parse(url) do
with {:ok, data} <- Cachex.fetch(:rich_media_cache, url, fn _ -> parse_url(url) end) do try do
data Cachex.fetch!(:rich_media_cache, url, fn _ ->
else {:commit, parse_url(url)}
_e -> end)
{:error, "Parsing error"} rescue
e ->
{:error, "Cachex error: #{inspect(e)}"}
end end
end end
end end
defp parse_url(url) do defp parse_url(url) do
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url) try do
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url)
html |> maybe_parse() |> get_parsed_data() html |> maybe_parse() |> get_parsed_data()
rescue
e ->
{:error, "Parsing error: #{inspect(e)}"}
end
end end
defp maybe_parse(html) do defp maybe_parse(html) do
@ -35,11 +46,11 @@ defp maybe_parse(html) do
end) end)
end end
defp get_parsed_data(data) when data == %{} do defp get_parsed_data(%{title: title} = data) when is_binary(title) and byte_size(title) > 0 do
{:error, "No metadata found"} {:ok, data}
end end
defp get_parsed_data(data) do defp get_parsed_data(data) do
{:ok, data} {:error, "Found metadata was invalid or incomplete: #{inspect(data)}"}
end end
end end

View File

@ -22,6 +22,10 @@ defp get_oembed_url(nodes) do
defp get_oembed_data(url) do defp get_oembed_data(url) do
{:ok, %Tesla.Env{body: json}} = Pleroma.HTTP.get(url) {:ok, %Tesla.Env{body: json}} = Pleroma.HTTP.get(url)
{:ok, Poison.decode!(json)} {:ok, data} = Jason.decode(json)
data = data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end)
{:ok, data}
end end
end end

View File

@ -239,12 +239,6 @@ defmodule Pleroma.Web.Router do
put("/settings", MastodonAPIController, :put_settings) put("/settings", MastodonAPIController, :put_settings)
end end
scope "/api", Pleroma.Web.RichMedia do
pipe_through(:authenticated_api)
get("/rich_media/parse", RichMediaController, :parse)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api) pipe_through(:api)
get("/instance", MastodonAPIController, :masto_instance) get("/instance", MastodonAPIController, :masto_instance)

View File

@ -12,6 +12,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Web.MastodonAPI.StatusView
defp user_by_ap_id(user_list, ap_id) do defp user_by_ap_id(user_list, ap_id) do
Enum.find(user_list, fn %{ap_id: user_id} -> ap_id == user_id end) Enum.find(user_list, fn %{ap_id: user_id} -> ap_id == user_id end)
@ -186,6 +187,12 @@ def to_map(
summary = HTML.strip_tags(object["summary"]) summary = HTML.strip_tags(object["summary"])
card =
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
)
%{ %{
"id" => activity.id, "id" => activity.id,
"uri" => activity.data["object"]["id"], "uri" => activity.data["object"]["id"],
@ -214,7 +221,8 @@ def to_map(
"possibly_sensitive" => possibly_sensitive, "possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object), "visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object),
"summary" => summary, "summary" => summary,
"summary_html" => summary |> Formatter.emojify(object["emoji"]) "summary_html" => summary |> Formatter.emojify(object["emoji"]),
"card" => card
} }
end end

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
alias Pleroma.Web.TwitterAPI.ActivityView alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Object alias Pleroma.Object
@ -274,6 +275,12 @@ def render(
summary = HTML.strip_tags(summary) summary = HTML.strip_tags(summary)
card =
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
)
%{ %{
"id" => activity.id, "id" => activity.id,
"uri" => activity.data["object"]["id"], "uri" => activity.data["object"]["id"],
@ -300,9 +307,10 @@ def render(
"tags" => tags, "tags" => tags,
"activity_type" => "post", "activity_type" => "post",
"possibly_sensitive" => possibly_sensitive, "possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object), "visibility" => StatusView.get_visibility(object),
"summary" => summary, "summary" => summary,
"summary_html" => summary |> Formatter.emojify(object["emoji"]) "summary_html" => summary |> Formatter.emojify(object["emoji"]),
"card" => card
} }
end end

View File

@ -136,6 +136,20 @@ test "posting a sensitive status", %{conn: conn} do
assert Repo.get(Activity, id) assert Repo.get(Activity, id)
end end
test "posting a status with OGP link preview", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "http://example.com/ogp"
})
assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200)
assert Repo.get(Activity, id)
end
test "posting a direct status", %{conn: conn} do test "posting a direct status", %{conn: conn} do
user1 = insert(:user) user1 = insert(:user)
user2 = insert(:user) user2 = insert(:user)
@ -1663,9 +1677,19 @@ test "Status rich-media Card", %{conn: conn, user: user} do
assert response == %{ assert response == %{
"image" => "http://ia.media-imdb.com/images/rock.jpg", "image" => "http://ia.media-imdb.com/images/rock.jpg",
"provider_name" => "www.imdb.com", "provider_name" => "www.imdb.com",
"provider_url" => "http://www.imdb.com",
"title" => "The Rock", "title" => "The Rock",
"type" => "link", "type" => "link",
"url" => "http://www.imdb.com/title/tt0117500/" "url" => "http://www.imdb.com/title/tt0117500/",
"description" => nil,
"pleroma" => %{
"opengraph" => %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "http://www.imdb.com/title/tt0117500/"
}
}
} }
end end
end end

View File

@ -84,6 +84,7 @@ test "a note activity" do
account: AccountView.render("account.json", %{user: user}), account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: nil, in_reply_to_id: nil,
in_reply_to_account_id: nil, in_reply_to_account_id: nil,
card: nil,
reblog: nil, reblog: nil,
content: HtmlSanitizeEx.basic_html(note.data["object"]["content"]), content: HtmlSanitizeEx.basic_html(note.data["object"]["content"]),
created_at: created_at, created_at: created_at,

View File

@ -1,49 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.RichMediaControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "GET /api/rich_media/parse" do
setup do
user = insert(:user)
[user: user]
end
test "returns 404 if not metadata found", %{user: user} do
build_conn()
|> with_credentials(user.nickname, "test")
|> get("/api/rich_media/parse", %{"url" => "http://example.com/empty"})
|> json_response(404)
end
test "returns OGP metadata", %{user: user} do
response =
build_conn()
|> with_credentials(user.nickname, "test")
|> get("/api/rich_media/parse", %{"url" => "http://example.com/ogp"})
|> json_response(200)
assert response == %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "http://www.imdb.com/title/tt0117500/"
}
end
end
defp with_credentials(conn, username, password) do
header_content = "Basic " <> Base.encode64("#{username}:#{password}")
put_req_header(conn, "authorization", header_content)
end
end

View File

@ -65,28 +65,27 @@ test "parses OEmbed" do
assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") == assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") ==
{:ok, {:ok,
%{ %{
"author_name" => "bees", author_name: "bees",
"author_url" => "https://www.flickr.com/photos/bees/", author_url: "https://www.flickr.com/photos/bees/",
"cache_age" => 3600, cache_age: 3600,
"flickr_type" => "photo", flickr_type: "photo",
"height" => "768", height: "768",
"html" => html:
"<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by bees, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>", "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by bees, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>",
"license" => "All Rights Reserved", license: "All Rights Reserved",
"license_id" => 0, license_id: 0,
"provider_name" => "Flickr", provider_name: "Flickr",
"provider_url" => "https://www.flickr.com/", provider_url: "https://www.flickr.com/",
"thumbnail_height" => 150, thumbnail_height: 150,
"thumbnail_url" => thumbnail_url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg",
"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg", thumbnail_width: 150,
"thumbnail_width" => 150, title: "Bacon Lollys",
"title" => "Bacon Lollys", type: "photo",
"type" => "photo", url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg",
"url" => "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg", version: "1.0",
"version" => "1.0", web_page: "https://www.flickr.com/photos/bees/2362225867/",
"web_page" => "https://www.flickr.com/photos/bees/2362225867/", web_page_short_url: "https://flic.kr/p/4AK2sc",
"web_page_short_url" => "https://flic.kr/p/4AK2sc", width: "1024"
"width" => "1024"
}} }}
end end
end end

View File

@ -164,6 +164,7 @@ test "an activity" do
"possibly_sensitive" => true, "possibly_sensitive" => true,
"uri" => activity.data["object"]["id"], "uri" => activity.data["object"]["id"],
"visibility" => "direct", "visibility" => "direct",
"card" => nil,
"summary" => "2hu :2hu:", "summary" => "2hu :2hu:",
"summary_html" => "summary_html" =>
"2hu <img height=\"32px\" width=\"32px\" alt=\"2hu\" title=\"2hu\" src=\"corndog.png\" />" "2hu <img height=\"32px\" width=\"32px\" alt=\"2hu\" title=\"2hu\" src=\"corndog.png\" />"

View File

@ -148,7 +148,8 @@ test "a create activity with a note" do
"text" => "Hey @shp!", "text" => "Hey @shp!",
"uri" => activity.data["object"]["id"], "uri" => activity.data["object"]["id"],
"user" => UserView.render("show.json", %{user: user}), "user" => UserView.render("show.json", %{user: user}),
"visibility" => "direct" "visibility" => "direct",
"card" => nil
} }
assert result == expected assert result == expected