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:
commit
ebb3496386
|
@ -69,7 +69,7 @@ def extract_first_external_url(object, content) do
|
|||
|> Floki.attribute("a", "href")
|
||||
|> Enum.at(0)
|
||||
|
||||
{:commit, result}
|
||||
{:commit, {:ok, result}}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -88,6 +88,10 @@ def insert(map, local \\ true) when is_map(map) do
|
|||
recipients: recipients
|
||||
})
|
||||
|
||||
Task.start(fn ->
|
||||
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
||||
end)
|
||||
|
||||
Notification.create_notifications(activity)
|
||||
stream_out(activity)
|
||||
{:ok, activity}
|
||||
|
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
use Pleroma.Web, :controller
|
||||
alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.HTML
|
||||
|
||||
alias Pleroma.Web.MastodonAPI.{
|
||||
StatusView,
|
||||
|
@ -500,7 +499,8 @@ def update_media(%{assigns: %{user: user}} = conn, data) do
|
|||
|
||||
def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
|
||||
with {:ok, object} <-
|
||||
ActivityPub.upload(file,
|
||||
ActivityPub.upload(
|
||||
file,
|
||||
actor: User.ap_id(user),
|
||||
description: Map.get(data, "description")
|
||||
) do
|
||||
|
@ -1101,7 +1101,9 @@ def login(conn, %{"code" => code}) do
|
|||
def login(conn, _) do
|
||||
with {:ok, app} <- get_or_make_app() do
|
||||
path =
|
||||
o_auth_path(conn, :authorize,
|
||||
o_auth_path(
|
||||
conn,
|
||||
:authorize,
|
||||
response_type: "code",
|
||||
client_id: app.client_id,
|
||||
redirect_uri: ".",
|
||||
|
@ -1342,27 +1344,20 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do
|
|||
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
|
||||
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
|
||||
|
||||
def try_render(conn, target, params)
|
||||
|
|
|
@ -139,6 +139,8 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
|
|||
__MODULE__
|
||||
)
|
||||
|
||||
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
|
||||
|
||||
%{
|
||||
id: to_string(activity.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_account_id: reply_to_user && to_string(reply_to_user.id),
|
||||
reblog: nil,
|
||||
card: card,
|
||||
content: content,
|
||||
created_at: created_at,
|
||||
reblogs_count: announcement_count,
|
||||
|
@ -175,6 +178,29 @@ def render("status.json", _) do
|
|||
nil
|
||||
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
|
||||
[attachment_url | _] = attachment["url"]
|
||||
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
@parsers [
|
||||
Pleroma.Web.RichMedia.Parsers.OGP,
|
||||
|
@ -11,19 +15,26 @@ def parse(nil), do: {:error, "No URL provided"}
|
|||
def parse(url), do: parse_url(url)
|
||||
else
|
||||
def parse(url) do
|
||||
with {:ok, data} <- Cachex.fetch(:rich_media_cache, url, fn _ -> parse_url(url) end) do
|
||||
data
|
||||
else
|
||||
_e ->
|
||||
{:error, "Parsing error"}
|
||||
try do
|
||||
Cachex.fetch!(:rich_media_cache, url, fn _ ->
|
||||
{:commit, parse_url(url)}
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
{:error, "Cachex error: #{inspect(e)}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
defp maybe_parse(html) do
|
||||
|
@ -35,11 +46,11 @@ defp maybe_parse(html) do
|
|||
end)
|
||||
end
|
||||
|
||||
defp get_parsed_data(data) when data == %{} do
|
||||
{:error, "No metadata found"}
|
||||
defp get_parsed_data(%{title: title} = data) when is_binary(title) and byte_size(title) > 0 do
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
defp get_parsed_data(data) do
|
||||
{:ok, data}
|
||||
{:error, "Found metadata was invalid or incomplete: #{inspect(data)}"}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,6 +22,10 @@ defp get_oembed_url(nodes) do
|
|||
defp get_oembed_data(url) do
|
||||
{: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
|
||||
|
|
|
@ -239,12 +239,6 @@ defmodule Pleroma.Web.Router do
|
|||
put("/settings", MastodonAPIController, :put_settings)
|
||||
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
|
||||
pipe_through(:api)
|
||||
get("/instance", MastodonAPIController, :masto_instance)
|
||||
|
|
|
@ -12,6 +12,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
|
|||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
defp user_by_ap_id(user_list, ap_id) do
|
||||
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"])
|
||||
|
||||
card =
|
||||
StatusView.render(
|
||||
"card.json",
|
||||
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
||||
)
|
||||
|
||||
%{
|
||||
"id" => activity.id,
|
||||
"uri" => activity.data["object"]["id"],
|
||||
|
@ -214,7 +221,8 @@ def to_map(
|
|||
"possibly_sensitive" => possibly_sensitive,
|
||||
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object),
|
||||
"summary" => summary,
|
||||
"summary_html" => summary |> Formatter.emojify(object["emoji"])
|
||||
"summary_html" => summary |> Formatter.emojify(object["emoji"]),
|
||||
"card" => card
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
|
|||
alias Pleroma.Web.TwitterAPI.ActivityView
|
||||
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
||||
alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Object
|
||||
|
@ -274,6 +275,12 @@ def render(
|
|||
|
||||
summary = HTML.strip_tags(summary)
|
||||
|
||||
card =
|
||||
StatusView.render(
|
||||
"card.json",
|
||||
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
||||
)
|
||||
|
||||
%{
|
||||
"id" => activity.id,
|
||||
"uri" => activity.data["object"]["id"],
|
||||
|
@ -300,9 +307,10 @@ def render(
|
|||
"tags" => tags,
|
||||
"activity_type" => "post",
|
||||
"possibly_sensitive" => possibly_sensitive,
|
||||
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object),
|
||||
"visibility" => StatusView.get_visibility(object),
|
||||
"summary" => summary,
|
||||
"summary_html" => summary |> Formatter.emojify(object["emoji"])
|
||||
"summary_html" => summary |> Formatter.emojify(object["emoji"]),
|
||||
"card" => card
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -136,6 +136,20 @@ test "posting a sensitive status", %{conn: conn} do
|
|||
assert Repo.get(Activity, id)
|
||||
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
|
||||
user1 = insert(:user)
|
||||
user2 = insert(:user)
|
||||
|
@ -1663,9 +1677,19 @@ test "Status rich-media Card", %{conn: conn, user: user} do
|
|||
assert response == %{
|
||||
"image" => "http://ia.media-imdb.com/images/rock.jpg",
|
||||
"provider_name" => "www.imdb.com",
|
||||
"provider_url" => "http://www.imdb.com",
|
||||
"title" => "The Rock",
|
||||
"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
|
||||
|
|
|
@ -84,6 +84,7 @@ test "a note activity" do
|
|||
account: AccountView.render("account.json", %{user: user}),
|
||||
in_reply_to_id: nil,
|
||||
in_reply_to_account_id: nil,
|
||||
card: nil,
|
||||
reblog: nil,
|
||||
content: HtmlSanitizeEx.basic_html(note.data["object"]["content"]),
|
||||
created_at: created_at,
|
||||
|
|
|
@ -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
|
|
@ -65,28 +65,27 @@ test "parses OEmbed" do
|
|||
assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") ==
|
||||
{:ok,
|
||||
%{
|
||||
"author_name" => "bees",
|
||||
"author_url" => "https://www.flickr.com/photos/bees/",
|
||||
"cache_age" => 3600,
|
||||
"flickr_type" => "photo",
|
||||
"height" => "768",
|
||||
"html" =>
|
||||
author_name: "bees",
|
||||
author_url: "https://www.flickr.com/photos/bees/",
|
||||
cache_age: 3600,
|
||||
flickr_type: "photo",
|
||||
height: "768",
|
||||
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>",
|
||||
"license" => "All Rights Reserved",
|
||||
"license_id" => 0,
|
||||
"provider_name" => "Flickr",
|
||||
"provider_url" => "https://www.flickr.com/",
|
||||
"thumbnail_height" => 150,
|
||||
"thumbnail_url" =>
|
||||
"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg",
|
||||
"thumbnail_width" => 150,
|
||||
"title" => "Bacon Lollys",
|
||||
"type" => "photo",
|
||||
"url" => "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg",
|
||||
"version" => "1.0",
|
||||
"web_page" => "https://www.flickr.com/photos/bees/2362225867/",
|
||||
"web_page_short_url" => "https://flic.kr/p/4AK2sc",
|
||||
"width" => "1024"
|
||||
license: "All Rights Reserved",
|
||||
license_id: 0,
|
||||
provider_name: "Flickr",
|
||||
provider_url: "https://www.flickr.com/",
|
||||
thumbnail_height: 150,
|
||||
thumbnail_url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg",
|
||||
thumbnail_width: 150,
|
||||
title: "Bacon Lollys",
|
||||
type: "photo",
|
||||
url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg",
|
||||
version: "1.0",
|
||||
web_page: "https://www.flickr.com/photos/bees/2362225867/",
|
||||
web_page_short_url: "https://flic.kr/p/4AK2sc",
|
||||
width: "1024"
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -164,6 +164,7 @@ test "an activity" do
|
|||
"possibly_sensitive" => true,
|
||||
"uri" => activity.data["object"]["id"],
|
||||
"visibility" => "direct",
|
||||
"card" => nil,
|
||||
"summary" => "2hu :2hu:",
|
||||
"summary_html" =>
|
||||
"2hu <img height=\"32px\" width=\"32px\" alt=\"2hu\" title=\"2hu\" src=\"corndog.png\" />"
|
||||
|
|
|
@ -148,7 +148,8 @@ test "a create activity with a note" do
|
|||
"text" => "Hey @shp!",
|
||||
"uri" => activity.data["object"]["id"],
|
||||
"user" => UserView.render("show.json", %{user: user}),
|
||||
"visibility" => "direct"
|
||||
"visibility" => "direct",
|
||||
"card" => nil
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
|
Loading…
Reference in New Issue