2020-01-22 21:40:40 +00:00
|
|
|
defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
2019-10-25 15:43:37 +00:00
|
|
|
@moduledoc """
|
2020-01-22 01:14:42 +00:00
|
|
|
Various utils for converters.
|
2019-10-25 15:43:37 +00:00
|
|
|
"""
|
|
|
|
|
2021-11-06 09:09:07 +00:00
|
|
|
alias Mobilizon.{Actors, Addresses, Events}
|
2019-10-25 15:43:37 +00:00
|
|
|
alias Mobilizon.Actors.Actor
|
2021-11-06 09:09:07 +00:00
|
|
|
alias Mobilizon.Addresses.Address
|
2019-10-25 15:43:37 +00:00
|
|
|
alias Mobilizon.Events.Tag
|
2020-12-16 08:54:56 +00:00
|
|
|
alias Mobilizon.Medias.Media
|
2019-10-25 15:43:37 +00:00
|
|
|
alias Mobilizon.Mention
|
|
|
|
alias Mobilizon.Storage.Repo
|
2020-01-22 01:14:42 +00:00
|
|
|
|
2021-04-22 10:17:56 +00:00
|
|
|
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
2021-11-06 09:09:07 +00:00
|
|
|
alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter
|
2020-12-16 08:54:56 +00:00
|
|
|
alias Mobilizon.Federation.ActivityStream.Converter.Media, as: MediaConverter
|
2020-01-22 01:14:42 +00:00
|
|
|
|
2020-01-28 18:18:33 +00:00
|
|
|
alias Mobilizon.Web.Endpoint
|
|
|
|
|
2019-10-25 15:43:37 +00:00
|
|
|
require Logger
|
|
|
|
|
2020-12-16 08:54:56 +00:00
|
|
|
@banner_picture_name "Banner"
|
|
|
|
|
2019-10-25 15:43:37 +00:00
|
|
|
@spec fetch_tags([String.t()]) :: [Tag.t()]
|
|
|
|
def fetch_tags(tags) when is_list(tags) do
|
|
|
|
Logger.debug("fetching tags")
|
2019-12-03 10:29:51 +00:00
|
|
|
Logger.debug(inspect(tags))
|
2019-10-25 15:43:37 +00:00
|
|
|
|
2023-06-20 13:50:27 +00:00
|
|
|
tags
|
|
|
|
|> Enum.flat_map(&fetch_tag/1)
|
|
|
|
|> Enum.uniq()
|
|
|
|
|> Enum.filter(& &1)
|
|
|
|
|> Enum.map(&existing_tag_or_data/1)
|
2019-10-25 15:43:37 +00:00
|
|
|
end
|
|
|
|
|
2020-07-09 15:24:28 +00:00
|
|
|
def fetch_tags(_), do: []
|
|
|
|
|
2019-10-25 15:43:37 +00:00
|
|
|
@spec fetch_mentions([map()]) :: [map()]
|
|
|
|
def fetch_mentions(mentions) when is_list(mentions) do
|
|
|
|
Logger.debug("fetching mentions")
|
2022-04-20 07:00:30 +00:00
|
|
|
Logger.debug(inspect(mentions))
|
2019-10-25 15:43:37 +00:00
|
|
|
|
|
|
|
Enum.reduce(mentions, [], fn mention, acc -> create_mention(mention, acc) end)
|
|
|
|
end
|
|
|
|
|
2020-07-09 15:24:28 +00:00
|
|
|
def fetch_mentions(_), do: []
|
|
|
|
|
2020-09-29 07:53:48 +00:00
|
|
|
def fetch_actors(actors) when is_list(actors) do
|
|
|
|
Logger.debug("fetching contacts")
|
|
|
|
actors |> Enum.map(& &1.id) |> Enum.filter(& &1) |> Enum.map(&Actors.get_actor/1)
|
|
|
|
end
|
|
|
|
|
|
|
|
def fetch_actors(_), do: []
|
|
|
|
|
2020-07-09 15:24:28 +00:00
|
|
|
@spec build_tags([Tag.t()]) :: [map()]
|
2019-10-25 15:43:37 +00:00
|
|
|
def build_tags(tags) do
|
|
|
|
Enum.map(tags, fn %Tag{} = tag ->
|
|
|
|
%{
|
2020-01-28 18:18:33 +00:00
|
|
|
"href" => Endpoint.url() <> "/tags/#{tag.slug}",
|
2019-10-25 15:43:37 +00:00
|
|
|
"name" => "##{tag.title}",
|
|
|
|
"type" => "Hashtag"
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_mentions(mentions) do
|
|
|
|
Enum.map(mentions, fn %Mention{} = mention ->
|
|
|
|
if Ecto.assoc_loaded?(mention.actor) do
|
|
|
|
build_mention(mention.actor)
|
|
|
|
else
|
|
|
|
build_mention(Repo.preload(mention, [:actor]).actor)
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp build_mention(%Actor{} = actor) do
|
|
|
|
%{
|
|
|
|
"href" => actor.url,
|
2020-01-28 18:18:33 +00:00
|
|
|
"name" => "@#{Actor.preferred_username_and_domain(actor)}",
|
2019-10-25 15:43:37 +00:00
|
|
|
"type" => "Mention"
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2019-12-03 10:29:51 +00:00
|
|
|
defp fetch_tag(%{title: title}), do: [title]
|
|
|
|
|
2019-11-19 14:36:25 +00:00
|
|
|
defp fetch_tag(tag) when is_map(tag) do
|
2019-10-25 15:43:37 +00:00
|
|
|
case tag["type"] do
|
|
|
|
"Hashtag" ->
|
2019-11-19 14:36:25 +00:00
|
|
|
[tag_without_hash(tag["name"])]
|
2019-10-25 15:43:37 +00:00
|
|
|
|
|
|
|
_err ->
|
2019-11-19 14:36:25 +00:00
|
|
|
[]
|
2019-10-25 15:43:37 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-04-08 14:41:49 +00:00
|
|
|
defp fetch_tag(tag) when is_binary(tag), do: [tag_without_hash(tag)]
|
2019-11-04 14:10:58 +00:00
|
|
|
|
2019-11-19 14:36:25 +00:00
|
|
|
defp tag_without_hash("#" <> tag_title), do: tag_title
|
|
|
|
defp tag_without_hash(tag_title), do: tag_title
|
2019-11-04 14:10:58 +00:00
|
|
|
|
|
|
|
defp existing_tag_or_data(tag_title) do
|
|
|
|
case Events.get_tag_by_title(tag_title) do
|
|
|
|
%Tag{} = tag -> %{title: tag.title, id: tag.id}
|
|
|
|
nil -> %{title: tag_title}
|
|
|
|
end
|
2019-10-25 15:43:37 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
@spec create_mention(map(), list()) :: list()
|
|
|
|
defp create_mention(%Actor{id: actor_id} = _mention, acc) do
|
|
|
|
acc ++ [%{actor_id: actor_id}]
|
|
|
|
end
|
|
|
|
|
|
|
|
defp create_mention(mention, acc) when is_map(mention) do
|
|
|
|
with true <- mention["type"] == "Mention",
|
2021-04-22 10:17:56 +00:00
|
|
|
{:ok, %Actor{id: actor_id}} <-
|
|
|
|
ActivityPubActor.get_or_fetch_actor_by_url(mention["href"]) do
|
2019-10-25 15:43:37 +00:00
|
|
|
acc ++ [%{actor_id: actor_id}]
|
|
|
|
else
|
|
|
|
_err ->
|
|
|
|
acc
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@spec create_mention({String.t(), map()}, list()) :: list()
|
|
|
|
defp create_mention({_, mention}, acc) when is_map(mention) do
|
|
|
|
create_mention(mention, acc)
|
|
|
|
end
|
2020-07-09 15:24:28 +00:00
|
|
|
|
2023-06-20 13:38:19 +00:00
|
|
|
defp create_mention(_, acc), do: acc
|
|
|
|
|
2021-09-24 14:46:42 +00:00
|
|
|
@spec maybe_fetch_actor_and_attributed_to_id(map()) ::
|
|
|
|
{:ok, Actor.t(), Actor.t() | nil} | {:error, atom()}
|
2020-07-09 15:24:28 +00:00
|
|
|
def maybe_fetch_actor_and_attributed_to_id(%{
|
|
|
|
"actor" => actor_url,
|
|
|
|
"attributedTo" => attributed_to_url
|
|
|
|
})
|
|
|
|
when is_nil(attributed_to_url) do
|
2021-09-24 14:46:42 +00:00
|
|
|
case fetch_actor(actor_url) do
|
|
|
|
{:ok, %Actor{} = actor} ->
|
|
|
|
{:ok, actor, nil}
|
|
|
|
|
|
|
|
{:error, err} ->
|
|
|
|
{:error, err}
|
|
|
|
end
|
2020-07-09 15:24:28 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def maybe_fetch_actor_and_attributed_to_id(%{
|
|
|
|
"actor" => actor_url,
|
|
|
|
"attributedTo" => attributed_to_url
|
|
|
|
})
|
|
|
|
when is_nil(actor_url) do
|
2021-09-24 14:46:42 +00:00
|
|
|
case fetch_actor(attributed_to_url) do
|
|
|
|
{:ok, %Actor{} = actor} ->
|
|
|
|
{:ok, actor, nil}
|
|
|
|
|
|
|
|
{:error, err} ->
|
|
|
|
{:error, err}
|
|
|
|
end
|
2020-07-09 15:24:28 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Only when both actor and attributedTo fields are both filled is when we can return both
|
|
|
|
def maybe_fetch_actor_and_attributed_to_id(%{
|
|
|
|
"actor" => actor_url,
|
|
|
|
"attributedTo" => attributed_to_url
|
|
|
|
})
|
|
|
|
when actor_url != attributed_to_url do
|
2021-09-24 14:46:42 +00:00
|
|
|
with {:ok, %Actor{} = actor} <- fetch_actor(actor_url),
|
|
|
|
{:ok, %Actor{} = attributed_to} <- fetch_actor(attributed_to_url) do
|
|
|
|
{:ok, actor, attributed_to}
|
|
|
|
else
|
|
|
|
{:error, err} ->
|
|
|
|
{:error, err}
|
2020-07-09 15:24:28 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# If we only have attributedTo and no actor, take attributedTo as the actor
|
|
|
|
def maybe_fetch_actor_and_attributed_to_id(%{
|
|
|
|
"attributedTo" => attributed_to_url
|
|
|
|
}) do
|
2021-09-24 14:46:42 +00:00
|
|
|
case fetch_actor(attributed_to_url) do
|
|
|
|
{:ok, %Actor{} = attributed_to} -> {:ok, attributed_to, nil}
|
|
|
|
{:error, err} -> {:error, err}
|
|
|
|
end
|
2020-07-09 15:24:28 +00:00
|
|
|
end
|
|
|
|
|
2021-09-24 14:46:42 +00:00
|
|
|
def maybe_fetch_actor_and_attributed_to_id(_), do: {:error, :no_actor_found}
|
2020-07-09 15:24:28 +00:00
|
|
|
|
2023-06-20 13:31:31 +00:00
|
|
|
@spec fetch_actor(String.t() | map()) :: {:ok, Actor.t()} | {:error, atom()}
|
|
|
|
def fetch_actor(%{"id" => actor_url}) when is_binary(actor_url), do: fetch_actor(actor_url)
|
|
|
|
|
|
|
|
def fetch_actor(actor_url) when is_binary(actor_url) do
|
2021-09-24 14:46:42 +00:00
|
|
|
case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
|
|
|
|
{:ok, %Actor{suspended: false} = actor} ->
|
|
|
|
{:ok, actor}
|
|
|
|
|
|
|
|
{:ok, %Actor{suspended: true} = _actor} ->
|
|
|
|
{:error, :actor_suspended}
|
|
|
|
|
|
|
|
{:error, err} ->
|
|
|
|
{:error, err}
|
2020-07-09 15:24:28 +00:00
|
|
|
end
|
|
|
|
end
|
2020-12-16 08:54:56 +00:00
|
|
|
|
2023-06-20 13:31:31 +00:00
|
|
|
def fetch_actor(_), do: {:error, :no_actor_found}
|
|
|
|
|
2020-12-16 08:54:56 +00:00
|
|
|
@spec process_pictures(map(), integer()) :: Keyword.t()
|
|
|
|
def process_pictures(object, actor_id) do
|
2024-01-20 08:28:07 +00:00
|
|
|
{banner, media_attachements} = get_medias(object)
|
2020-12-16 08:54:56 +00:00
|
|
|
|
|
|
|
media_attachements_map =
|
|
|
|
media_attachements
|
|
|
|
|> Enum.map(fn media_attachement ->
|
|
|
|
{media_attachement["url"],
|
|
|
|
MediaConverter.find_or_create_media(media_attachement, actor_id)}
|
|
|
|
end)
|
|
|
|
|> Enum.reduce(%{}, fn {old_url, media}, acc ->
|
|
|
|
case media do
|
|
|
|
{:ok, %Media{} = media} ->
|
|
|
|
Map.put(acc, old_url, media)
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
acc
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
|
|
|
|
media_attachements_map_urls =
|
|
|
|
media_attachements_map
|
|
|
|
|> Enum.map(fn {old_url, new_media} -> {old_url, new_media.file.url} end)
|
|
|
|
|> Map.new()
|
|
|
|
|
|
|
|
picture_id =
|
2021-11-10 15:39:47 +00:00
|
|
|
case banner do
|
|
|
|
banner_map when is_map(banner_map) ->
|
|
|
|
case MediaConverter.find_or_create_media(banner_map, actor_id) do
|
2021-09-24 14:46:42 +00:00
|
|
|
{:error, _err} ->
|
|
|
|
nil
|
|
|
|
|
|
|
|
{:ok, %Media{id: picture_id}} ->
|
|
|
|
picture_id
|
|
|
|
end
|
|
|
|
|
|
|
|
_ ->
|
2020-12-16 08:54:56 +00:00
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
description = replace_media_urls_in_body(object["content"], media_attachements_map_urls)
|
|
|
|
[description: description, picture_id: picture_id, medias: Map.values(media_attachements_map)]
|
|
|
|
end
|
|
|
|
|
|
|
|
defp replace_media_urls_in_body(body, media_urls),
|
|
|
|
do:
|
|
|
|
Enum.reduce(media_urls, body, fn media_url, body ->
|
|
|
|
replace_media_url_in_body(body, media_url)
|
|
|
|
end)
|
|
|
|
|
|
|
|
defp replace_media_url_in_body(body, {old_url, new_url}),
|
|
|
|
do: String.replace(body, old_url, new_url)
|
|
|
|
|
2021-11-10 15:39:47 +00:00
|
|
|
@spec get_medias(list(map())) :: {map(), list(map())}
|
2024-01-20 08:28:07 +00:00
|
|
|
def get_medias(object) do
|
|
|
|
banner = get_banner_picture(object)
|
|
|
|
attachments = Map.get(object, "attachment", [])
|
|
|
|
{banner, Enum.filter(attachments, &(valid_banner_media?(&1) && &1["url"] != banner["url"]))}
|
2020-12-16 08:54:56 +00:00
|
|
|
end
|
|
|
|
|
2024-01-20 08:28:07 +00:00
|
|
|
@spec get_banner_picture(map()) :: map()
|
|
|
|
defp get_banner_picture(object) do
|
|
|
|
attachments = Map.get(object, "attachment", [])
|
|
|
|
image = Map.get(object, "image", %{})
|
|
|
|
|
2021-11-10 15:39:47 +00:00
|
|
|
media_with_picture_name =
|
2024-01-20 08:28:07 +00:00
|
|
|
Enum.find(attachments, &(valid_banner_media?(&1) && &1["name"] == @banner_picture_name))
|
|
|
|
|
|
|
|
cond do
|
|
|
|
# Check if the "image" key is set and of type "Document" or "Image"
|
|
|
|
is_nil(media_with_picture_name) and valid_banner_media?(image) ->
|
|
|
|
image
|
|
|
|
|
|
|
|
is_nil(media_with_picture_name) and Enum.find(attachments, &valid_banner_media?/1) ->
|
|
|
|
Enum.find(attachments, &valid_banner_media?/1)
|
|
|
|
|
|
|
|
!is_nil(media_with_picture_name) ->
|
|
|
|
media_with_picture_name
|
2021-11-10 15:39:47 +00:00
|
|
|
|
2024-01-20 08:28:07 +00:00
|
|
|
true ->
|
|
|
|
nil
|
2021-11-10 15:39:47 +00:00
|
|
|
end
|
2020-12-16 08:54:56 +00:00
|
|
|
end
|
2021-11-06 09:09:07 +00:00
|
|
|
|
2024-01-20 08:28:07 +00:00
|
|
|
@spec valid_banner_media?(map()) :: boolean()
|
|
|
|
defp valid_banner_media?(media) do
|
|
|
|
media |> Map.get("type") |> valid_attachment_type?()
|
|
|
|
end
|
|
|
|
|
|
|
|
@spec valid_attachment_type?(any()) :: boolean()
|
|
|
|
defp valid_attachment_type?(type) do
|
|
|
|
type in ["Document", "Image"]
|
|
|
|
end
|
|
|
|
|
2021-11-06 09:09:07 +00:00
|
|
|
@spec get_address(map | binary | nil) :: Address.t() | nil
|
2023-12-19 09:52:31 +00:00
|
|
|
def get_address(text_address) when is_binary(text_address) do
|
|
|
|
get_address(%{"type" => "Place", "name" => text_address})
|
2021-11-06 09:09:07 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def get_address(%{"id" => url} = map) when is_map(map) and is_binary(url) do
|
|
|
|
Logger.debug("Address with an URL, let's check against our own database")
|
|
|
|
|
|
|
|
case Addresses.get_address_by_url(url) do
|
|
|
|
%Address{} = address ->
|
|
|
|
address
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
Logger.debug("not in our database, let's try to create it")
|
2021-11-13 15:53:03 +00:00
|
|
|
# This is odd, why do addresses have url instead of just @id?
|
2021-11-06 09:09:07 +00:00
|
|
|
map = Map.put(map, "url", map["id"])
|
|
|
|
do_get_address(map)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_address(map) when is_map(map) do
|
|
|
|
do_get_address(map)
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_address(nil), do: nil
|
|
|
|
|
|
|
|
@spec do_get_address(map) :: Address.t() | nil
|
|
|
|
defp do_get_address(map) do
|
|
|
|
map = AddressConverter.as_to_model_data(map)
|
|
|
|
|
|
|
|
case Addresses.create_address(map) do
|
|
|
|
{:ok, %Address{} = address} ->
|
|
|
|
address
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
2024-02-08 16:51:14 +00:00
|
|
|
|
|
|
|
@ap_public "https://www.w3.org/ns/activitystreams#Public"
|
|
|
|
|
|
|
|
@spec visibility_public?(String.t() | list(String.t())) :: boolean()
|
|
|
|
def visibility_public?(to) when is_binary(to), do: visibility_public?([to])
|
|
|
|
|
|
|
|
def visibility_public?(to) when is_list(to) do
|
|
|
|
!MapSet.disjoint?(MapSet.new(to), MapSet.new([@ap_public, "as:Public", "Public"]))
|
|
|
|
end
|
2019-10-25 15:43:37 +00:00
|
|
|
end
|