Merge branch 'federate-metadata' into 'master'

Federate metadata

See merge request framasoft/mobilizon!1020
This commit is contained in:
Thomas Citharel 2021-08-10 11:23:46 +00:00
commit cb148e29d7
8 changed files with 290 additions and 59 deletions

View file

@ -52,16 +52,14 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
}) do }) do
with {to, cc} <- with {to, cc} <-
extract_actors_from_mentions(mentions, actor, visibility), extract_actors_from_mentions(mentions, actor, visibility),
{to, cc} <- {Enum.uniq(to ++ add_in_reply_to(in_reply_to_comment)), cc}, {to, cc} <- {to ++ add_in_reply_to(in_reply_to_comment), cc},
{to, cc} <- {Enum.uniq(to ++ add_event_author(event)), cc}, {to, cc} <- add_event_organizers(event, to, cc),
{to, cc} <- {to, cc} <-
{to, {to,
Enum.uniq(
cc ++ cc ++
add_comments_authors([origin_comment]) ++ add_comments_authors([origin_comment]) ++
add_shares_actors_followers(url) add_shares_actors_followers(url)} do
)} do %{"to" => Enum.uniq(to), "cc" => Enum.uniq(cc)}
%{"to" => to, "cc" => cc}
end end
end end
@ -173,11 +171,22 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url] defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url]
defp add_in_reply_to(_), do: [] defp add_in_reply_to(_), do: []
defp add_event_author(%Event{} = event) do defp add_event_organizers(%Event{} = event, to, cc) do
[Repo.preload(event, [:organizer_actor]).organizer_actor.url] event = Repo.preload(event, [:organizer_actor, :attributed_to])
case event do
%Event{
attributed_to: %Actor{members_url: members_url, followers_url: followers_url},
organizer_actor: %Actor{url: organizer_actor_url}
} ->
{to ++ [organizer_actor_url, members_url], cc ++ [followers_url]}
%Event{organizer_actor: %Actor{url: organizer_actor_url}} ->
{to ++ [organizer_actor_url], cc}
end
end end
defp add_event_author(_), do: [] defp add_event_organizers(_, to, cc), do: {to, cc}
defp add_comment_author(%Comment{} = comment) do defp add_comment_author(%Comment{} = comment) do
case Repo.preload(comment, [:actor]) do case Repo.preload(comment, [:actor]) do

View file

@ -88,7 +88,10 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
"participationMessage" => %{ "participationMessage" => %{
"@id" => "mz:participationMessage", "@id" => "mz:participationMessage",
"@type" => "sc:Text" "@type" => "sc:Text"
} },
"PropertyValue" => "sc:PropertyValue",
"value" => "sc:value",
"propertyID" => "sc:propertyID"
} }
] ]
} }

View file

@ -10,9 +10,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
alias Mobilizon.Addresses alias Mobilizon.Addresses
alias Mobilizon.Addresses.Address alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Event, as: EventModel alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Medias.Media
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter
alias Mobilizon.Federation.ActivityStream.Converter.EventMetadata, as: EventMetadataConverter
alias Mobilizon.Federation.ActivityStream.Converter.Media, as: MediaConverter alias Mobilizon.Federation.ActivityStream.Converter.Media, as: MediaConverter
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
@ -53,6 +55,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
{:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])}, {:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])},
{:visibility, visibility} <- {:visibility, get_visibility(object)}, {:visibility, visibility} <- {:visibility, get_visibility(object)},
{:options, options} <- {:options, get_options(object)}, {:options, options} <- {:options, get_options(object)},
{:metadata, metadata} <- {:metadata, get_metdata(object)},
[description: description, picture_id: picture_id, medias: medias] <- [description: description, picture_id: picture_id, medias: medias] <-
process_pictures(object, actor_id) do process_pictures(object, actor_id) do
%{ %{
@ -69,6 +72,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
join_options: Map.get(object, "joinMode", "free"), join_options: Map.get(object, "joinMode", "free"),
local: is_local(object["id"]), local: is_local(object["id"]),
options: options, options: options,
metadata: metadata,
status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(), status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(),
online_address: object |> Map.get("attachment", []) |> get_online_address(), online_address: object |> Map.get("attachment", []) |> get_online_address(),
phone_address: object["phoneAddress"], phone_address: object["phoneAddress"],
@ -120,7 +124,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
"repliesModerationOption" => event.options.comment_moderation, "repliesModerationOption" => event.options.comment_moderation,
"commentsEnabled" => event.options.comment_moderation == :allow_all, "commentsEnabled" => event.options.comment_moderation == :allow_all,
"anonymousParticipationEnabled" => event.options.anonymous_participation, "anonymousParticipationEnabled" => event.options.anonymous_participation,
"attachment" => [], "attachment" => Enum.map(event.metadata, &EventMetadataConverter.metadata_to_as/1),
"draft" => event.draft, "draft" => event.draft,
"ical:status" => event.status |> to_string |> String.upcase(), "ical:status" => event.status |> to_string |> String.upcase(),
"id" => event.url, "id" => event.url,
@ -132,8 +136,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|> maybe_add_inline_media(event) |> maybe_add_inline_media(event)
end end
@spec attributed_to_or_default(Event.t()) :: Actor.t() @spec attributed_to_or_default(EventModel.t()) :: Actor.t()
defp attributed_to_or_default(event) do defp attributed_to_or_default(%EventModel{} = event) do
if(is_nil(event.attributed_to) or not Ecto.assoc_loaded?(event.attributed_to), if(is_nil(event.attributed_to) or not Ecto.assoc_loaded?(event.attributed_to),
do: nil, do: nil,
else: event.attributed_to else: event.attributed_to
@ -156,6 +160,14 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
} }
end end
defp get_metdata(%{"attachment" => attachments}) do
attachments
|> Enum.filter(&(&1["type"] == "PropertyValue"))
|> Enum.map(&EventMetadataConverter.as_to_metadata/1)
end
defp get_metdata(_), do: []
@spec get_address(map | binary | nil) :: integer | nil @spec get_address(map | binary | nil) :: integer | nil
defp get_address(address_url) when is_binary(address_url) do defp get_address(address_url) when is_binary(address_url) do
get_address(%{"id" => address_url}) get_address(%{"id" => address_url})
@ -219,36 +231,35 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
end) end)
end end
@spec maybe_add_physical_address(map(), Event.t()) :: map() @spec maybe_add_physical_address(map(), EventModel.t()) :: map()
defp maybe_add_physical_address(res, event) do defp maybe_add_physical_address(res, %EventModel{
if is_nil(event.physical_address), physical_address: %Address{} = physical_address
do: res, }) do
else: Map.put(res, "location", AddressConverter.model_to_as(event.physical_address)) Map.put(res, "location", AddressConverter.model_to_as(physical_address))
end end
@spec maybe_add_event_picture(map(), Event.t()) :: map() defp maybe_add_physical_address(res, %EventModel{physical_address: _}), do: res
defp maybe_add_event_picture(res, event) do
if is_nil(event.picture), @spec maybe_add_event_picture(map(), EventModel.t()) :: map()
do: res, defp maybe_add_event_picture(res, %EventModel{picture: %Media{} = picture}) do
else:
Map.update( Map.update(
res, res,
"attachment", "attachment",
[], [],
&(&1 ++ &(&1 ++
[ [
event.picture picture
|> MediaConverter.model_to_as() |> MediaConverter.model_to_as()
|> Map.put("name", @banner_picture_name) |> Map.put("name", @banner_picture_name)
]) ])
) )
end end
@spec maybe_add_online_address(map(), Event.t()) :: map() defp maybe_add_event_picture(res, %EventModel{picture: _}), do: res
defp maybe_add_online_address(res, event) do
if is_nil(event.online_address), @spec maybe_add_online_address(map(), EventModel.t()) :: map()
do: res, defp maybe_add_online_address(res, %EventModel{online_address: online_address})
else: when is_binary(online_address) do
Map.update( Map.update(
res, res,
"attachment", "attachment",
@ -257,7 +268,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
[ [
%{ %{
"type" => "Link", "type" => "Link",
"href" => event.online_address, "href" => online_address,
"mediaType" => "text/html", "mediaType" => "text/html",
"name" => @online_address_name "name" => @online_address_name
} }
@ -265,9 +276,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
) )
end end
@spec maybe_add_inline_media(map(), Event.t()) :: map() defp maybe_add_online_address(res, %EventModel{online_address: _}), do: res
defp maybe_add_inline_media(res, event) do
medias = Enum.map(event.media, &MediaConverter.model_to_as/1) @spec maybe_add_inline_media(map(), EventModel.t()) :: map()
defp maybe_add_inline_media(res, %EventModel{media: media}) do
medias = Enum.map(media, &MediaConverter.model_to_as/1)
Map.update( Map.update(
res, res,

View file

@ -0,0 +1,70 @@
defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do
@moduledoc """
Module to convert and validate event metadata
"""
alias Mobilizon.Events.EventMetadata
@property_value "PropertyValue"
def metadata_to_as(%EventMetadata{type: :boolean, value: value, key: key})
when value in ["true", "false"] do
%{
"type" => @property_value,
"propertyID" => key,
"value" => String.to_existing_atom(value)
}
end
def metadata_to_as(%EventMetadata{type: :integer, value: value, key: key}) do
%{
"type" => @property_value,
"propertyID" => key,
"value" => String.to_integer(value)
}
end
def metadata_to_as(%EventMetadata{type: :float, value: value, key: key}) do
{value, _} = Float.parse(value)
%{
"type" => @property_value,
"propertyID" => key,
"value" => value
}
end
def metadata_to_as(%EventMetadata{type: :string, value: value, key: key} = metadata) do
additional = if is_nil(metadata.title), do: %{}, else: %{"name" => metadata.title}
Map.merge(
%{
"type" => @property_value,
"propertyID" => key,
"value" => value
},
additional
)
end
def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value})
when is_boolean(value) do
%{type: :boolean, key: key, value: to_string(value)}
end
def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value})
when is_float(value) do
%{type: :float, key: key, value: to_string(value)}
end
def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value})
when is_integer(value) do
%{type: :integer, key: key, value: to_string(value)}
end
def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value} = args)
when is_binary(value) do
additional = if Map.has_key?(args, "name"), do: %{title: Map.get(args, "name")}, else: %{}
Map.merge(%{type: :string, key: key, value: value}, additional)
end
end

View file

@ -7,7 +7,7 @@ defmodule Mobilizon.Events.EventMetadata do
import Ecto.Changeset import Ecto.Changeset
import EctoEnum import EctoEnum
defenum(EventMetadataTypeEnum, string: 0, integer: 1, boolean: 2) defenum(EventMetadataTypeEnum, string: 0, integer: 1, boolean: 2, float: 3)
@type t :: %__MODULE__{ @type t :: %__MODULE__{
key: String.t(), key: String.t(),

View file

@ -247,6 +247,32 @@ defmodule Mobilizon.Federation.ActivityPub.AudienceTest do
assert %{"to" => [members_url], "cc" => []} == assert %{"to" => [members_url], "cc" => []} ==
Audience.get_audience(comment) Audience.get_audience(comment)
end end
test "reply to a remote comment" do
%Actor{id: remote_actor_id, url: remote_actor_url} =
remote_actor =
insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Actor{id: remote_group_id, url: remote_group_url} =
remote_group =
insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@somegroup")
%Event{} =
event =
insert(:event, local: false, organizer_actor: remote_actor, attributed_to: remote_group)
%Comment{} = comment = insert(:comment, event: event)
assert %{
"cc" => [comment.actor.followers_url, comment.event.attributed_to.followers_url],
"to" => [
@ap_public,
comment.event.organizer_actor.url,
comment.event.attributed_to.members_url
]
} ==
Audience.get_audience(comment)
end
end end
describe "participant" do describe "participant" do

View file

@ -0,0 +1,101 @@
defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadataTest do
@moduledoc """
Module to test converting from EventMetadata to AS
"""
use Mobilizon.DataCase
import Mobilizon.Factory
alias Mobilizon.Events.EventMetadata
alias Mobilizon.Federation.ActivityStream.Converter.EventMetadata, as: EventMetadataConverter
@property_value "PropertyValue"
describe "metadata_to_as/1" do
test "convert a simple metadata" do
%EventMetadata{} = metadata = build(:event_metadata)
assert %{"propertyID" => metadata.key, "value" => metadata.value, "type" => @property_value} ==
EventMetadataConverter.metadata_to_as(metadata)
end
test "convert a boolean" do
%EventMetadata{} = metadata = build(:event_metadata, type: :boolean, value: "false")
assert %{"propertyID" => metadata.key, "value" => false, "type" => @property_value} ==
EventMetadataConverter.metadata_to_as(metadata)
end
test "convert an integer" do
%EventMetadata{} = metadata = build(:event_metadata, type: :integer, value: "36")
assert %{"propertyID" => metadata.key, "value" => 36, "type" => @property_value} ==
EventMetadataConverter.metadata_to_as(metadata)
end
test "convert a float" do
%EventMetadata{} = metadata = build(:event_metadata, type: :float, value: "36.53")
assert %{"propertyID" => metadata.key, "value" => 36.53, "type" => @property_value} ==
EventMetadataConverter.metadata_to_as(metadata)
end
test "convert custom metadata with title" do
%EventMetadata{} = metadata = build(:event_metadata, title: "hello")
assert %{
"propertyID" => metadata.key,
"value" => metadata.value,
"name" => "hello",
"type" => @property_value
} ==
EventMetadataConverter.metadata_to_as(metadata)
end
end
describe "as_to_metadata/1" do
test "parse a simple metadata" do
assert %{key: "somekey", value: "somevalue", type: :string} ==
EventMetadataConverter.as_to_metadata(%{
"propertyID" => "somekey",
"value" => "somevalue",
"type" => @property_value
})
end
test "parse a boolean metadata" do
assert %{key: "somekey", value: "false", type: :boolean} ==
EventMetadataConverter.as_to_metadata(%{
"propertyID" => "somekey",
"value" => false,
"type" => @property_value
})
end
test "parse an integer metadata" do
assert %{key: "somekey", value: "4", type: :integer} ==
EventMetadataConverter.as_to_metadata(%{
"propertyID" => "somekey",
"value" => 4,
"type" => @property_value
})
end
test "parse a float metadata" do
assert %{key: "somekey", value: "4.36", type: :float} ==
EventMetadataConverter.as_to_metadata(%{
"propertyID" => "somekey",
"value" => 4.36,
"type" => @property_value
})
end
test "parse a custom metadata with title" do
assert %{key: "somekey", value: "somevalue", type: :string, title: "title"} ==
EventMetadataConverter.as_to_metadata(%{
"propertyID" => "somekey",
"value" => "somevalue",
"name" => "title",
"type" => @property_value
})
end
end
end

View file

@ -183,6 +183,7 @@ defmodule Mobilizon.Factory do
visibility: :public, visibility: :public,
tags: build_list(3, :tag), tags: build_list(3, :tag),
mentions: [], mentions: [],
metadata: build_list(2, :event_metadata),
local: true, local: true,
publish_at: DateTime.utc_now(), publish_at: DateTime.utc_now(),
url: Routes.page_url(Endpoint, :event, uuid), url: Routes.page_url(Endpoint, :event, uuid),
@ -458,4 +459,12 @@ defmodule Mobilizon.Factory do
uri: sequence("https://someshare.uri/p/12") uri: sequence("https://someshare.uri/p/12")
} }
end end
def event_metadata_factory do
%Mobilizon.Events.EventMetadata{
key: sequence("mz:custom:something"),
value: sequence("a value"),
type: :string
}
end
end end