Render status with multilang maps

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
tusooa 2023-01-01 23:23:17 -05:00 committed by marcin mikołajczak
parent 4cfd7b4f79
commit 39cdde78e1
5 changed files with 189 additions and 174 deletions

View file

@ -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

View file

@ -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",

View file

@ -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"]

View file

@ -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

View file

@ -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)