Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Multi-language posting #1

Open
wants to merge 44 commits into
base: fork
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
00591d9
Add support for *Map attributes
tusooa Dec 28, 2022
8822dc1
Add functions to process multi-language content
tusooa Dec 28, 2022
dbebd7f
Validate language codes in MapOfString
tusooa Dec 28, 2022
d86a1e7
Fix compile warning
tusooa Dec 28, 2022
4bcae35
Treat empty maps as nils
tusooa Dec 28, 2022
5c62c50
Generate * from *Map
tusooa Dec 29, 2022
4cfd7b4
Convert nameMap in question options
tusooa Jan 2, 2023
39cdde7
Render status with multilang maps
tusooa Jan 2, 2023
ad8f47f
Display multilang in history items
tusooa Jan 2, 2023
64db6ce
Render multilang of sources
tusooa Jan 2, 2023
6b20df1
Render multilang for attachments
tusooa Jan 2, 2023
e11d4b6
Render multilang in polls
tusooa Jan 2, 2023
0d96c04
Accept map of strings in ActivityDraft
tusooa Jan 3, 2023
72a2b33
Accept status_map and spoiler_text_map in POST statuses
tusooa Jan 3, 2023
b1bdbdc
Accept multilang polls on MastoAPI
tusooa Jan 3, 2023
7222ceb
Use MultiLanguage func to convert str to map
tusooa Jan 3, 2023
4d2813a
Accept multilang descriptions when uploading attachments
tusooa Jan 3, 2023
88ff45e
Fix unit tests
tusooa Jan 3, 2023
023eae2
Move validating multilang map into MultiLanguage
tusooa Jan 3, 2023
a9b1589
Validate multilang map
tusooa Jan 4, 2023
c34a4d8
Accept description_map when updating media
tusooa Jan 4, 2023
afab622
Fix article_note_page_validator_test
tusooa Jan 4, 2023
c4cec5f
Render language attr of a status
tusooa Jan 4, 2023
9555e63
Post single-language status with language attribute
tusooa Jan 4, 2023
792e495
Fix tests
tusooa Jan 4, 2023
cd0260f
Make ensure_re_prepended multilang-aware
tusooa Jan 15, 2023
2d66faf
Make force_mentions_in_content multilang aware
tusooa Jan 15, 2023
9354ee3
Make keyword_policy multilang-aware
tusooa Jan 15, 2023
e958ec5
Make no_empty_policy multilang-aware
tusooa Jan 15, 2023
fb26889
Make normalize_markup multilang aware
tusooa Jan 15, 2023
3d3162f
Make no_placeholder_text_policy multilang-aware
tusooa Jan 15, 2023
548473a
Remove unused and fix
mkljczk May 14, 2024
56f19a7
Fix language dectection when creating posts
mkljczk May 16, 2024
3d4510e
Merge remote-tracking branch 'mkljczk-github/fork' into multilang
mkljczk May 18, 2024
8dfd079
Fix multilanguage, add feature to InstanceView
mkljczk May 18, 2024
ef50dd5
Merge branch 'fork' into multilang
mkljczk May 21, 2024
1268453
Merge branch 'fork' into multilang
mkljczk May 23, 2024
7cb3f61
Merge branch 'fork' into multilang
mkljczk May 25, 2024
da19454
Merge branch 'fork' into multilang
mkljczk May 28, 2024
5b8ad04
Fix notification types
mkljczk May 28, 2024
9b0c05f
wip
mkljczk May 29, 2024
48be78b
MultiLanguage: Remove map_to_str
mkljczk May 31, 2024
305a8d9
Merge branch 'fork' into multilang
mkljczk May 31, 2024
a2f2d63
fix tests
mkljczk May 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMap do
use Ecto.Type

import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode,
only: [good_locale_code?: 1]
alias Pleroma.MultiLanguage

def type, do: :map

def cast(%{} = object) do
with {status, %{} = data} when status in [:modified, :ok] <- validate_map(object) do
with {status, %{} = data} when status in [:modified, :ok] <-
MultiLanguage.validate_map(object) do
{:ok, data}
else
{_, nil} -> {:ok, nil}
Expand All @@ -24,26 +24,4 @@ defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMap do
def dump(data), do: {:ok, data}

def load(data), do: {:ok, data}

defp validate_map(%{} = object) do
{status, data} =
object
|> Enum.reduce({:ok, %{}}, fn
{lang, value}, {status, acc} when is_binary(lang) and is_binary(value) ->
if good_locale_code?(lang) do
{status, Map.put(acc, lang, value)}
else
{:modified, acc}
end

_, {_status, acc} ->
{:modified, acc}
end)

if data == %{} do
{status, nil}
else
{status, data}
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode do
use Ecto.Type

alias Pleroma.MultiLanguage

def type, do: :string

def cast(language) when is_binary(language) do
if good_locale_code?(language) do
if MultiLanguage.good_locale_code?(language) do
{:ok, language}
else
{:error, :invalid_language}
Expand All @@ -20,8 +22,4 @@ defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode do
def dump(data), do: {:ok, data}

def load(data), do: {:ok, data}

def good_locale_code?(code) when is_binary(code), do: code =~ ~r<^[a-zA-Z0-9\-]+$>

def good_locale_code?(_code), do: false
end
4 changes: 4 additions & 0 deletions lib/pleroma/emoji/formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,9 @@ defmodule Pleroma.Emoji.Formatter do
end)
end

def get_emoji_map(%{} = map) do
Enum.reduce(map, %{}, fn {_, content}, acc -> Map.merge(acc, get_emoji_map(content)) end)
end

def get_emoji_map(_), do: %{}
end
43 changes: 43 additions & 0 deletions lib/pleroma/multi_language.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.MultiLanguage do
def good_locale_code?(code) when is_binary(code), do: code =~ ~r<^[a-zA-Z0-9\-]+$>

def good_locale_code?(_code), do: false

def validate_map(%{} = object) do
{status, data} =
object
|> Enum.reduce({:ok, %{}}, fn
{lang, value}, {status, acc} when is_binary(lang) and is_binary(value) ->
if good_locale_code?(lang) do
{status, Map.put(acc, lang, value)}
else
{:modified, acc}
end

_, {_status, acc} ->
{:modified, acc}
end)

if data == %{} do
{status, nil}
else
{status, data}
end
end

def validate_map(_), do: {:error, nil}

def str_to_map(data, opts \\ []) do
with lang when is_binary(lang) <- opts[:lang],
true <- good_locale_code?(lang) do
%{lang => data}
else
_ ->
%{"und" => data}
end
end
end
46 changes: 37 additions & 9 deletions lib/pleroma/upload.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ defmodule Pleroma.Upload do
height: integer(),
blurhash: String.t(),
description: String.t(),
description_map: map(),
path: String.t()
}
defstruct [
Expand All @@ -73,21 +74,44 @@ defmodule Pleroma.Upload do
:height,
:blurhash,
:description,
:description_map,
:path
]

@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)

defp get_description(upload) do
case {upload.description, Pleroma.Config.get([Pleroma.Upload, :default_description])} do
{description, _} when is_binary(description) -> description
case {upload, Pleroma.Config.get([Pleroma.Upload, :default_description])} do
{%{description_map: %{} = description_map}, _} -> description_map
{%{description: description}, _} when is_binary(description) -> description
{_, :filename} -> upload.name
{_, str} when is_binary(str) -> str
_ -> ""
end
end

@spec store(source, options :: [option()]) :: {:ok, map()} | {:error, any()}
defp validate_description_limit(%{} = description) do
Enum.all?(description, fn {_, content} ->
String.length(content) <= Pleroma.Config.get([:instance, :description_limit])
end)
end

defp validate_description_limit(description) when is_binary(description) do
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])
end

defp description_fields(%{} = description, language) do
%{
"name" => Map.get(description, language),
"nameMap" => description
}
end

defp description_fields(description, _) when is_binary(description) do
%{"name" => description}
end

@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
@doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct."
def store(upload, opts \\ []) do
opts = get_opts(opts)
Expand All @@ -96,9 +120,10 @@ defmodule Pleroma.Upload do
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
description = get_description(upload),
{_, true} <- {:description_limit, validate_description_limit(description)},
{_, true} <-
{:description_limit,
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
{:valid_locale,
opts[:language] == nil or Pleroma.MultiLanguage.good_locale_code?(opts[:language])},
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok,
%{
Expand All @@ -113,10 +138,11 @@ defmodule Pleroma.Upload do
}
|> Maps.put_if_present("width", upload.width)
|> Maps.put_if_present("height", upload.height)
],
"name" => description
]
}
|> Maps.put_if_present("blurhash", upload.blurhash)}
|> Map.merge(description_fields(description, opts[:language]))
|> Maps.put_if_present("blurhash", upload.blurhash)
|> Maps.put_if_present("language", opts[:language])}
else
{:description_limit, _} ->
{:error, :description_too_long}
Expand Down Expand Up @@ -156,6 +182,7 @@ defmodule Pleroma.Upload do
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
filters: Keyword.get(opts, :filters, Pleroma.Config.get([__MODULE__, :filters])),
description: Keyword.get(opts, :description),
description_map: Keyword.get(opts, :description_map),
base_url: base_url()
}
end
Expand All @@ -168,7 +195,8 @@ defmodule Pleroma.Upload do
name: file.filename,
tempfile: file.path,
content_type: file.content_type,
description: opts.description
description: opts.description,
description_map: opts.description_map
}}
end
end
Expand Down
28 changes: 27 additions & 1 deletion lib/pleroma/web/activity_pub/activity_pub.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
import Pleroma.Web.ActivityPub.Visibility
import Pleroma.Web.Gettext
import Pleroma.Webhook.Notify, only: [trigger_webhooks: 2]

require Logger
Expand Down Expand Up @@ -1592,12 +1593,37 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.reverse()
end

defp validate_media_description_map(%{} = map, language) do
with {:ok, %{}} <- Pleroma.MultiLanguage.validate_map(map),
true <- Pleroma.MultiLanguage.good_locale_code?(language) do
:ok
else
false -> :invalid_language
_ -> :error
end
end

defp validate_media_description_map(nil, _), do: :ok
defp validate_media_description_map(_, _), do: :error

@spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
def upload(file, opts \\ []) do
with {:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do
with {_, :ok} <-
{:description_map,
validate_media_description_map(opts[:description_map], opts[:language])},
{:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do
obj_data = Maps.put_if_present(data, "actor", opts[:actor])

Repo.insert(%Object{data: obj_data})
else
{:description_map, :invalid_language} ->
{:error, dgettext("errors", "valid language must be provided with description_map")}

{:description_map, _} ->
{:error, dgettext("errors", "description_map invalid")}

e ->
e
end
end

Expand Down
36 changes: 34 additions & 2 deletions lib/pleroma/web/activity_pub/builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -216,19 +216,51 @@ defmodule Pleroma.Web.ActivityPub.Builder do

@spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
def note(%ActivityDraft{} = draft) do
content_fields =
if draft.content_html_map do
case Map.keys(draft.content_html_map) do
["und"] ->
%{"content" => Map.get(draft.content_html_map, "und")}

_ ->
%{
"contentMap" => draft.content_html_map,
"content" => Map.get(draft.content_html_map, draft.language)
}
end
else
%{"content" => draft.content_html}
end

summary_fields =
if draft.summary_map do
case Map.keys(draft.summary_map) do
["und"] ->
%{"summary" => Map.get(draft.summary_map, "und")}

_ ->
%{
"summaryMap" => draft.summary_map,
"summary" => Map.get(draft.summary_map, draft.language)
}
end
else
%{"summary" => draft.summary}
end

data =
%{
"type" => "Note",
"to" => draft.to,
"cc" => draft.cc,
"content" => draft.content_html,
"summary" => draft.summary,
"sensitive" => draft.sensitive,
"context" => draft.context,
"attachment" => draft.attachments,
"actor" => draft.user.ap_id,
"tag" => Keyword.values(draft.tags) |> Enum.uniq()
}
|> Map.merge(content_fields)
|> Map.merge(summary_fields)
|> add_in_reply_to(draft.in_reply_to)
|> add_quote(draft.quote_post)
|> Map.merge(draft.extra)
Expand Down
49 changes: 44 additions & 5 deletions lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,41 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do

def history_awareness, do: :auto

def filter_by_summary(
%{data: %{"summaryMap" => %{} = parent_summary_map} = parent},
%{"summaryMap" => %{} = child_summary_map} = child
) do
fixed_summary_map =
Enum.reduce(child_summary_map, %{}, fn {lang, cur}, acc ->
with {:ok, fixed_cur} <- fix_one(cur, parent_summary_map[lang]) do
Map.put(acc, lang, fixed_cur)
else
_ -> Map.put(acc, lang, cur)
end
end)

fixed_summary =
with {:ok, fixed} <- fix_one(child["summary"], parent["summary"]) do
fixed
else
_ -> child["summary"]
end

child
|> Map.put("summaryMap", fixed_summary_map)
|> Map.put("summary", fixed_summary)
end

def filter_by_summary(
%{data: %{"summary" => parent_summary}} = _in_reply_to,
%{"summary" => child_summary} = child
)
when not is_nil(child_summary) and byte_size(child_summary) > 0 and
not is_nil(parent_summary) and byte_size(parent_summary) > 0 do
if (child_summary == parent_summary and not Regex.match?(@reply_prefix, child_summary)) or
(Regex.match?(@reply_prefix, parent_summary) &&
Regex.replace(@reply_prefix, parent_summary, "") == child_summary) do
Map.put(child, "summary", "re: " <> child_summary)
with {:ok, fixed_child_summary} <- fix_one(child_summary, parent_summary) do
Map.put(child, "summary", fixed_child_summary)
else
child
_ -> child
end
end

Expand All @@ -44,4 +67,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
def filter(object), do: {:ok, object}

def describe, do: {:ok, %{}}

defp fix_one(child_summary, parent_summary)
when is_binary(child_summary) and child_summary != "" and is_binary(parent_summary) and
parent_summary != "" do
if (child_summary == parent_summary and not Regex.match?(@reply_prefix, child_summary)) or
(Regex.match?(@reply_prefix, parent_summary) &&
Regex.replace(@reply_prefix, parent_summary, "") == child_summary) do
{:ok, "re: " <> child_summary}
else
{:nochange, nil}
end
end

defp fix_one(_, _) do
{:nochange, nil}
end
end
Loading