Render status with multilang maps
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
4cfd7b4f79
commit
39cdde78e1
5 changed files with 189 additions and 174 deletions
|
@ -82,4 +82,18 @@ def empty_array_response do
|
|||
def no_content_response do
|
||||
Operation.response("No Content", "application/json", %Schema{type: :string, example: ""})
|
||||
end
|
||||
|
||||
def multilang_map_of(embedded_schema) do
|
||||
%Schema{
|
||||
type: :object,
|
||||
title:
|
||||
if embedded_schema.title do
|
||||
"MultiLang map of #{embedded_schema.title}"
|
||||
else
|
||||
"MultiLang map"
|
||||
end,
|
||||
additionalProperties: embedded_schema,
|
||||
description: "Map from a BCP47 language tag to a string in that language."
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Attachment
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||
|
@ -68,6 +69,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
}
|
||||
},
|
||||
content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"},
|
||||
content_map:
|
||||
Helpers.multilang_map_of(%Schema{
|
||||
type: :string,
|
||||
format: :html,
|
||||
description: "HTML-encoded status content"
|
||||
}),
|
||||
text: %Schema{
|
||||
type: :string,
|
||||
description: "Original unformatted content in plain text",
|
||||
|
@ -157,6 +164,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
description:
|
||||
"A map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`"
|
||||
},
|
||||
content_map: %Schema{
|
||||
type: :object,
|
||||
additionalProperties: Helpers.multilang_map_of(%Schema{type: :string}),
|
||||
description:
|
||||
"A map consisting of alternate representations of the `content_map` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`"
|
||||
},
|
||||
content_type: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
|
@ -246,6 +259,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
description:
|
||||
"A map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`."
|
||||
},
|
||||
spoiler_text_map: %Schema{
|
||||
type: :object,
|
||||
additionalProperties: Helpers.multilang_map_of(%Schema{type: :string}),
|
||||
description:
|
||||
"A map consisting of alternate representations of the `spoiler_text_map` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`."
|
||||
},
|
||||
thread_muted: %Schema{
|
||||
type: :boolean,
|
||||
description: "`true` if the thread the post belongs to is muted"
|
||||
|
@ -287,6 +306,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
description:
|
||||
"Subject or summary line, below which status content is collapsed until expanded"
|
||||
},
|
||||
spoiler_text_map:
|
||||
Helpers.multilang_map_of(%Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Subject or summary line, below which status content is collapsed until expanded"
|
||||
}),
|
||||
tags: %Schema{type: :array, items: Tag},
|
||||
uri: %Schema{
|
||||
type: :string,
|
||||
|
@ -364,6 +389,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
"bookmarked" => false,
|
||||
"card" => nil,
|
||||
"content" => "foobar",
|
||||
"content_map" => %{
|
||||
"en" => "mew mew",
|
||||
"cmn" => "喵喵"
|
||||
},
|
||||
"created_at" => "2020-04-07T19:48:51.000Z",
|
||||
"emojis" => [],
|
||||
"favourited" => false,
|
||||
|
@ -378,6 +407,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
"pinned" => false,
|
||||
"pleroma" => %{
|
||||
"content" => %{"text/plain" => "foobar"},
|
||||
"content_map" => %{
|
||||
"text/plain" => %{
|
||||
"en" => "mew mew",
|
||||
"cmn" => "喵喵"
|
||||
}
|
||||
},
|
||||
"context" => "http://localhost:4001/objects/8b4c0c80-6a37-4d2a-b1b9-05a19e3875aa",
|
||||
"conversation_id" => 345_972,
|
||||
"direct_conversation_id" => nil,
|
||||
|
@ -386,6 +421,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
"in_reply_to_account_acct" => nil,
|
||||
"local" => true,
|
||||
"spoiler_text" => %{"text/plain" => ""},
|
||||
"spoiler_text_map" => %{
|
||||
"text/plain" => %{
|
||||
"en" => "",
|
||||
"cmn" => ""
|
||||
}
|
||||
},
|
||||
"thread_muted" => false,
|
||||
"quotes_count" => 0
|
||||
},
|
||||
|
@ -396,6 +437,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
"replies_count" => 0,
|
||||
"sensitive" => false,
|
||||
"spoiler_text" => "",
|
||||
"spoiler_text_map" => %{
|
||||
"en" => "",
|
||||
"cmn" => ""
|
||||
},
|
||||
"tags" => [],
|
||||
"uri" => "http://localhost:4001/objects/0f5dad44-0e9e-4610-b377-a2631e499190",
|
||||
"url" => "http://localhost:4001/notice/9toJCu5YZW7O7gfvH6",
|
||||
|
|
|
@ -211,6 +211,7 @@ def render(
|
|||
in_reply_to_account_id: nil,
|
||||
reblog: reblogged,
|
||||
content: reblogged[:content] || "",
|
||||
content_map: reblogged[:content_map] || %{},
|
||||
created_at: created_at,
|
||||
reblogs_count: 0,
|
||||
replies_count: 0,
|
||||
|
@ -340,26 +341,28 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
|||
nil
|
||||
end
|
||||
|
||||
content =
|
||||
object
|
||||
|> render_content()
|
||||
{content_html, content_html_map} =
|
||||
get_content_and_map(%{
|
||||
type: :html,
|
||||
user: opts[:for],
|
||||
activity: activity,
|
||||
object: object,
|
||||
chrono_order: chrono_order
|
||||
})
|
||||
|
||||
content_html =
|
||||
content
|
||||
|> Activity.HTML.get_cached_scrubbed_html_for_activity(
|
||||
User.html_filter_policy(opts[:for]),
|
||||
activity,
|
||||
"mastoapi:content:#{chrono_order}"
|
||||
)
|
||||
{content_plaintext, content_plaintext_map} =
|
||||
get_content_and_map(%{
|
||||
type: :plain,
|
||||
user: opts[:for],
|
||||
activity: activity,
|
||||
object: object,
|
||||
chrono_order: chrono_order
|
||||
})
|
||||
|
||||
content_plaintext =
|
||||
content
|
||||
|> Activity.HTML.get_cached_stripped_html_for_activity(
|
||||
activity,
|
||||
"mastoapi:content:#{chrono_order}"
|
||||
)
|
||||
content_languages = Map.keys(content_html_map)
|
||||
|
||||
summary = object.data["summary"] || ""
|
||||
summary_map = object.data["summaryMap"] || %{}
|
||||
|
||||
card =
|
||||
case Card.get_by_activity(activity) do
|
||||
|
@ -426,6 +429,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
|||
reblog: nil,
|
||||
card: card,
|
||||
content: content_html,
|
||||
content_map: content_html_map,
|
||||
text: opts[:with_source] && get_source_text(object.data["source"]),
|
||||
created_at: created_at,
|
||||
edited_at: edited_at,
|
||||
|
@ -439,6 +443,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
|||
pinned: pinned?,
|
||||
sensitive: sensitive,
|
||||
spoiler_text: summary,
|
||||
spoiler_text_map: summary_map,
|
||||
visibility: get_visibility(object),
|
||||
media_attachments: attachments,
|
||||
poll: render(PollView, "show.json", object: object, for: opts[:for]),
|
||||
|
@ -457,7 +462,9 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
|||
quote_url: object.data["quoteUrl"],
|
||||
quote_visible: visible_for_user?(quote_activity, opts[:for]),
|
||||
content: %{"text/plain" => content_plaintext},
|
||||
content_map: %{"text/plain" => content_plaintext_map},
|
||||
spoiler_text: %{"text/plain" => summary},
|
||||
spoiler_text_map: %{"text/plain" => summary_map},
|
||||
expires_at: expires_at,
|
||||
direct_conversation_id: direct_conversation_id,
|
||||
thread_muted: thread_muted?,
|
||||
|
@ -717,14 +724,18 @@ def get_quote(%{data: %{"object" => _object}} = activity, _) do
|
|||
end
|
||||
end
|
||||
|
||||
def render_content(%{data: %{"name" => name, "type" => type}} = object)
|
||||
def render_content(object) do
|
||||
render_content(object, object.data["name"], object.data["content"])
|
||||
end
|
||||
|
||||
def render_content(%{data: %{"type" => type}} = object, name, content)
|
||||
when not is_nil(name) and name != "" and type != "Event" do
|
||||
url = object.data["url"] || object.data["id"]
|
||||
|
||||
"<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
|
||||
"<p><a href=\"#{url}\">#{name}</a></p>#{content}"
|
||||
end
|
||||
|
||||
def render_content(object), do: object.data["content"] || ""
|
||||
def render_content(_object, _name, content), do: content || ""
|
||||
|
||||
@doc """
|
||||
Builds a dictionary tags.
|
||||
|
@ -915,6 +926,71 @@ def build_source_location(%{"location_id" => location_id}) when is_binary(locati
|
|||
|
||||
def build_source_location(_), do: nil
|
||||
|
||||
defp get_content(%{
|
||||
type: type,
|
||||
content: content,
|
||||
user: user,
|
||||
activity: activity,
|
||||
chrono_order: chrono_order,
|
||||
language: language
|
||||
})
|
||||
when type in [:html, :plain] do
|
||||
language = language || "und"
|
||||
cache_key = "mastoapi:content:#{chrono_order}:#{language}"
|
||||
|
||||
if type == :html do
|
||||
content
|
||||
|> Activity.HTML.get_cached_scrubbed_html_for_activity(
|
||||
User.html_filter_policy(user),
|
||||
activity,
|
||||
cache_key
|
||||
)
|
||||
else
|
||||
content
|
||||
|> Activity.HTML.get_cached_stripped_html_for_activity(
|
||||
activity,
|
||||
cache_key
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_content_and_map(%{
|
||||
type: type,
|
||||
user: user,
|
||||
activity: activity,
|
||||
object: object,
|
||||
chrono_order: chrono_order
|
||||
}) do
|
||||
content_und =
|
||||
get_content(%{
|
||||
type: type,
|
||||
user: user,
|
||||
activity: activity,
|
||||
content: render_content(object),
|
||||
chrono_order: chrono_order,
|
||||
language: "und"
|
||||
})
|
||||
|
||||
content_map =
|
||||
(object.data["contentMap"] || %{})
|
||||
|> Enum.reduce(%{}, fn {lang, content}, acc ->
|
||||
Map.put(
|
||||
acc,
|
||||
lang,
|
||||
get_content(%{
|
||||
type: type,
|
||||
user: user,
|
||||
activity: activity,
|
||||
content: render_content(object, object.data["nameMap"][lang], content),
|
||||
chrono_order: chrono_order,
|
||||
language: lang
|
||||
})
|
||||
)
|
||||
end)
|
||||
|
||||
{content_und, content_map}
|
||||
end
|
||||
|
||||
defp get_language(%{data: %{"language" => "und"}}), do: nil
|
||||
|
||||
defp get_language(object), do: object.data["language"]
|
||||
|
|
|
@ -1,155 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.RichMedia.Parser.Card do
|
||||
alias Pleroma.Web.RichMedia.Parser.Card
|
||||
alias Pleroma.Web.RichMedia.Parser.Embed
|
||||
|
||||
@types ["link", "photo", "video", "rich"]
|
||||
|
||||
# https://docs.joinmastodon.org/entities/card/
|
||||
defstruct url: nil,
|
||||
title: nil,
|
||||
description: "",
|
||||
type: "link",
|
||||
author_name: "",
|
||||
author_url: "",
|
||||
provider_name: "",
|
||||
provider_url: "",
|
||||
html: "",
|
||||
width: 0,
|
||||
height: 0,
|
||||
image: nil,
|
||||
image_description: "",
|
||||
embed_url: "",
|
||||
blurhash: nil
|
||||
|
||||
def parse(%Embed{url: url, oembed: %{"type" => type, "title" => title} = oembed} = embed)
|
||||
when type in @types and is_binary(url) do
|
||||
uri = URI.parse(url)
|
||||
|
||||
%Card{
|
||||
url: url,
|
||||
title: title,
|
||||
description: get_description(embed),
|
||||
type: oembed["type"],
|
||||
author_name: oembed["author_name"],
|
||||
author_url: oembed["author_url"],
|
||||
provider_name: oembed["provider_name"] || uri.host,
|
||||
provider_url: oembed["provider_url"] || "#{uri.scheme}://#{uri.host}",
|
||||
html: sanitize_html(oembed["html"]),
|
||||
width: oembed["width"],
|
||||
height: oembed["height"],
|
||||
image: get_image(oembed) |> fix_uri(url) |> proxy(),
|
||||
image_description: get_image_description(embed),
|
||||
embed_url: oembed["url"] |> fix_uri(url) |> proxy()
|
||||
}
|
||||
|> IO.inspect
|
||||
|> validate()
|
||||
end
|
||||
|
||||
def parse(%Embed{url: url} = embed) when is_binary(url) do
|
||||
uri = URI.parse(url)
|
||||
|
||||
%Card{
|
||||
url: url,
|
||||
title: get_title(embed),
|
||||
description: get_description(embed),
|
||||
type: "link",
|
||||
provider_name: uri.host,
|
||||
provider_url: "#{uri.scheme}://#{uri.host}",
|
||||
image: get_image(embed) |> fix_uri(url) |> proxy(),
|
||||
image_description: get_image_description(embed),
|
||||
}
|
||||
|> validate()
|
||||
end
|
||||
|
||||
def parse(card), do: {:error, {:invalid_metadata, card}}
|
||||
|
||||
defp get_title(embed) do
|
||||
case embed do
|
||||
%{meta: %{"twitter:title" => title}} when is_binary(title) and title != "" -> title
|
||||
%{meta: %{"og:title" => title}} when is_binary(title) and title != "" -> title
|
||||
%{title: title} when is_binary(title) and title != "" -> title
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp get_description(%{meta: meta}) do
|
||||
case meta do
|
||||
%{"twitter:description" => desc} when is_binary(desc) and desc != "" -> desc
|
||||
%{"og:description" => desc} when is_binary(desc) and desc != "" -> desc
|
||||
%{"description" => desc} when is_binary(desc) and desc != "" -> desc
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp get_image(%{meta: meta}) do
|
||||
case meta do
|
||||
%{"twitter:image" => image} when is_binary(image) and image != "" -> image
|
||||
%{"og:image" => image} when is_binary(image) and image != "" -> image
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp get_image(%{"thumbnail_url" => image}) when is_binary(image) and image != "", do: image
|
||||
defp get_image(%{"type" => "photo", "url" => image}), do: image
|
||||
defp get_image(_), do: ""
|
||||
|
||||
defp get_image_description(%{meta: %{"og:image:alt" => image_description}}), do: image_description
|
||||
defp get_image_description(_), do: ""
|
||||
|
||||
defp sanitize_html(html) do
|
||||
with {:ok, html} <- FastSanitize.Sanitizer.scrub(html, Pleroma.HTML.Scrubber.OEmbed),
|
||||
{:ok, [{"iframe", _, _}]} <- Floki.parse_fragment(html) do
|
||||
html
|
||||
else
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
def to_map(%Card{} = card) do
|
||||
card
|
||||
|> Map.from_struct()
|
||||
|> stringify_keys()
|
||||
end
|
||||
|
||||
def to_map(%{} = card), do: stringify_keys(card)
|
||||
|
||||
defp stringify_keys(%{} = map), do: Map.new(map, fn {k, v} -> {Atom.to_string(k), v} end)
|
||||
|
||||
def fix_uri("http://" <> _ = uri, _base_uri), do: uri
|
||||
def fix_uri("https://" <> _ = uri, _base_uri), do: uri
|
||||
def fix_uri("/" <> _ = uri, base_uri), do: URI.merge(base_uri, uri) |> URI.to_string()
|
||||
def fix_uri("", _base_uri), do: nil
|
||||
|
||||
def fix_uri(uri, base_uri) when is_binary(uri),
|
||||
do: URI.merge(base_uri, "/#{uri}") |> URI.to_string()
|
||||
|
||||
def fix_uri(_uri, _base_uri), do: nil
|
||||
|
||||
defp proxy(url) when is_binary(url), do: Pleroma.Web.MediaProxy.url(url)
|
||||
defp proxy(_), do: nil
|
||||
|
||||
def validate(%Card{type: type, html: html} = card)
|
||||
when type in ["video", "rich"] and (is_binary(html) == false or html == "") do
|
||||
card
|
||||
|> Map.put(:type, "link")
|
||||
|> validate()
|
||||
end
|
||||
|
||||
def validate(%Card{type: type, title: title} = card)
|
||||
when type in @types and is_binary(title) and title != "" do
|
||||
{:ok, card}
|
||||
end
|
||||
|
||||
def validate(%Embed{} = embed) do
|
||||
case Card.parse(embed) do
|
||||
{:ok, %Card{} = card} -> validate(card)
|
||||
card -> {:error, {:invalid_metadata, card}}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(card), do: {:error, {:invalid_metadata, card}}
|
||||
end
|
|
@ -292,6 +292,7 @@ test "a note activity" do
|
|||
card: nil,
|
||||
reblog: nil,
|
||||
content: HTML.filter_tags(object_data["content"]),
|
||||
content_map: %{},
|
||||
text: nil,
|
||||
created_at: created_at,
|
||||
edited_at: nil,
|
||||
|
@ -306,6 +307,7 @@ test "a note activity" do
|
|||
sensitive: false,
|
||||
poll: nil,
|
||||
spoiler_text: HTML.filter_tags(object_data["summary"]),
|
||||
spoiler_text_map: %{},
|
||||
visibility: "public",
|
||||
media_attachments: [],
|
||||
mentions: [],
|
||||
|
@ -335,7 +337,9 @@ test "a note activity" do
|
|||
quote_url: nil,
|
||||
quote_visible: false,
|
||||
content: %{"text/plain" => HTML.strip_tags(object_data["content"])},
|
||||
content_map: %{"text/plain" => %{}},
|
||||
spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
|
||||
spoiler_text_map: %{"text/plain" => %{}},
|
||||
expires_at: nil,
|
||||
direct_conversation_id: nil,
|
||||
thread_muted: false,
|
||||
|
@ -353,6 +357,37 @@ test "a note activity" do
|
|||
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
|
||||
end
|
||||
|
||||
test "a note activity with multiple languages" do
|
||||
user = insert(:user)
|
||||
|
||||
note_obj =
|
||||
insert(:note,
|
||||
data: %{
|
||||
"content" => "mew mew",
|
||||
"contentMap" => %{"en" => "mew mew", "cmn" => "喵喵"},
|
||||
"summary" => "mew",
|
||||
"summaryMap" => %{"en" => "mew", "cmn" => "喵"}
|
||||
}
|
||||
)
|
||||
|
||||
note = insert(:note_activity, note: note_obj, user: user)
|
||||
|
||||
status = StatusView.render("show.json", %{activity: note})
|
||||
|
||||
assert %{
|
||||
content: "mew mew",
|
||||
content_map: %{"en" => "mew mew", "cmn" => "喵喵"},
|
||||
spoiler_text: "mew",
|
||||
spoiler_text_map: %{"en" => "mew", "cmn" => "喵"},
|
||||
pleroma: %{
|
||||
content: %{"text/plain" => "mew mew"},
|
||||
content_map: %{"text/plain" => %{"en" => "mew mew", "cmn" => "喵喵"}},
|
||||
spoiler_text: %{"text/plain" => "mew"},
|
||||
spoiler_text_map: %{"text/plain" => %{"en" => "mew", "cmn" => "喵"}}
|
||||
}
|
||||
} = status
|
||||
end
|
||||
|
||||
test "tells if the message is muted for some reason" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
|
Loading…
Reference in a new issue