From d3a05b55685f7d8e3aca1f81f35e5816652646e7 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Tue, 10 Aug 2021 11:03:03 +0200 Subject: [PATCH 1/2] Federate metadata Signed-off-by: Thomas Citharel --- lib/federation/activity_pub/utils.ex | 5 +- .../activity_stream/converter/event.ex | 105 ++++++++++-------- .../converter/event_metadata.ex | 70 ++++++++++++ lib/mobilizon/events/event_metadata.ex | 2 +- .../converter/event_metadata_test.exs | 101 +++++++++++++++++ test/support/factory.ex | 9 ++ 6 files changed, 244 insertions(+), 48 deletions(-) create mode 100644 lib/federation/activity_stream/converter/event_metadata.ex create mode 100644 test/federation/activity_stream/converter/event_metadata_test.exs diff --git a/lib/federation/activity_pub/utils.ex b/lib/federation/activity_pub/utils.ex index 9305f3b67..7a7af94a1 100644 --- a/lib/federation/activity_pub/utils.ex +++ b/lib/federation/activity_pub/utils.ex @@ -88,7 +88,10 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do "participationMessage" => %{ "@id" => "mz:participationMessage", "@type" => "sc:Text" - } + }, + "PropertyValue" => "sc:PropertyValue", + "value" => "sc:value", + "propertyID" => "sc:propertyID" } ] } diff --git a/lib/federation/activity_stream/converter/event.ex b/lib/federation/activity_stream/converter/event.ex index 99e814eca..e5ff9574d 100644 --- a/lib/federation/activity_stream/converter/event.ex +++ b/lib/federation/activity_stream/converter/event.ex @@ -10,9 +10,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do alias Mobilizon.Addresses alias Mobilizon.Addresses.Address alias Mobilizon.Events.Event, as: EventModel + alias Mobilizon.Medias.Media alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} 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.Web.Endpoint @@ -53,6 +55,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do {:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])}, {:visibility, visibility} <- {:visibility, get_visibility(object)}, {:options, options} <- {:options, get_options(object)}, + {:metadata, metadata} <- {:metadata, get_metdata(object)}, [description: description, picture_id: picture_id, medias: medias] <- process_pictures(object, actor_id) do %{ @@ -69,6 +72,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do join_options: Map.get(object, "joinMode", "free"), local: is_local(object["id"]), options: options, + metadata: metadata, status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(), online_address: object |> Map.get("attachment", []) |> get_online_address(), phone_address: object["phoneAddress"], @@ -120,7 +124,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do "repliesModerationOption" => event.options.comment_moderation, "commentsEnabled" => event.options.comment_moderation == :allow_all, "anonymousParticipationEnabled" => event.options.anonymous_participation, - "attachment" => [], + "attachment" => Enum.map(event.metadata, &EventMetadataConverter.metadata_to_as/1), "draft" => event.draft, "ical:status" => event.status |> to_string |> String.upcase(), "id" => event.url, @@ -132,8 +136,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do |> maybe_add_inline_media(event) end - @spec attributed_to_or_default(Event.t()) :: Actor.t() - defp attributed_to_or_default(event) do + @spec attributed_to_or_default(EventModel.t()) :: Actor.t() + defp attributed_to_or_default(%EventModel{} = event) do if(is_nil(event.attributed_to) or not Ecto.assoc_loaded?(event.attributed_to), do: nil, else: event.attributed_to @@ -156,6 +160,14 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do } 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 defp get_address(address_url) when is_binary(address_url) do get_address(%{"id" => address_url}) @@ -219,55 +231,56 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do end) end - @spec maybe_add_physical_address(map(), Event.t()) :: map() - defp maybe_add_physical_address(res, event) do - if is_nil(event.physical_address), - do: res, - else: Map.put(res, "location", AddressConverter.model_to_as(event.physical_address)) + @spec maybe_add_physical_address(map(), EventModel.t()) :: map() + defp maybe_add_physical_address(res, %EventModel{ + physical_address: %Address{} = physical_address + }) do + Map.put(res, "location", AddressConverter.model_to_as(physical_address)) end - @spec maybe_add_event_picture(map(), Event.t()) :: map() - defp maybe_add_event_picture(res, event) do - if is_nil(event.picture), - do: res, - else: - Map.update( - res, - "attachment", - [], - &(&1 ++ - [ - event.picture - |> MediaConverter.model_to_as() - |> Map.put("name", @banner_picture_name) - ]) - ) + defp maybe_add_physical_address(res, %EventModel{physical_address: _}), do: res + + @spec maybe_add_event_picture(map(), EventModel.t()) :: map() + defp maybe_add_event_picture(res, %EventModel{picture: %Media{} = picture}) do + Map.update( + res, + "attachment", + [], + &(&1 ++ + [ + picture + |> MediaConverter.model_to_as() + |> Map.put("name", @banner_picture_name) + ]) + ) end - @spec maybe_add_online_address(map(), Event.t()) :: map() - defp maybe_add_online_address(res, event) do - if is_nil(event.online_address), - do: res, - else: - Map.update( - res, - "attachment", - [], - &(&1 ++ - [ - %{ - "type" => "Link", - "href" => event.online_address, - "mediaType" => "text/html", - "name" => @online_address_name - } - ]) - ) + defp maybe_add_event_picture(res, %EventModel{picture: _}), do: res + + @spec maybe_add_online_address(map(), EventModel.t()) :: map() + defp maybe_add_online_address(res, %EventModel{online_address: online_address}) + when is_binary(online_address) do + Map.update( + res, + "attachment", + [], + &(&1 ++ + [ + %{ + "type" => "Link", + "href" => online_address, + "mediaType" => "text/html", + "name" => @online_address_name + } + ]) + ) end - @spec maybe_add_inline_media(map(), Event.t()) :: map() - defp maybe_add_inline_media(res, event) do - medias = Enum.map(event.media, &MediaConverter.model_to_as/1) + defp maybe_add_online_address(res, %EventModel{online_address: _}), do: res + + @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( res, diff --git a/lib/federation/activity_stream/converter/event_metadata.ex b/lib/federation/activity_stream/converter/event_metadata.ex new file mode 100644 index 000000000..eca14d16b --- /dev/null +++ b/lib/federation/activity_stream/converter/event_metadata.ex @@ -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 diff --git a/lib/mobilizon/events/event_metadata.ex b/lib/mobilizon/events/event_metadata.ex index 70f5b273a..6ef9add33 100644 --- a/lib/mobilizon/events/event_metadata.ex +++ b/lib/mobilizon/events/event_metadata.ex @@ -7,7 +7,7 @@ defmodule Mobilizon.Events.EventMetadata do import Ecto.Changeset import EctoEnum - defenum(EventMetadataTypeEnum, string: 0, integer: 1, boolean: 2) + defenum(EventMetadataTypeEnum, string: 0, integer: 1, boolean: 2, float: 3) @type t :: %__MODULE__{ key: String.t(), diff --git a/test/federation/activity_stream/converter/event_metadata_test.exs b/test/federation/activity_stream/converter/event_metadata_test.exs new file mode 100644 index 000000000..e01a0fb77 --- /dev/null +++ b/test/federation/activity_stream/converter/event_metadata_test.exs @@ -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 diff --git a/test/support/factory.ex b/test/support/factory.ex index 0c896ab98..db1a40967 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -183,6 +183,7 @@ defmodule Mobilizon.Factory do visibility: :public, tags: build_list(3, :tag), mentions: [], + metadata: build_list(2, :event_metadata), local: true, publish_at: DateTime.utc_now(), url: Routes.page_url(Endpoint, :event, uuid), @@ -458,4 +459,12 @@ defmodule Mobilizon.Factory do uri: sequence("https://someshare.uri/p/12") } end + + def event_metadata_factory do + %Mobilizon.Events.EventMetadata{ + key: sequence("mz:custom:something"), + value: sequence("a value"), + type: :string + } + end end From 56861d64836499af0e5950c27078b3d2e1364b86 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Tue, 10 Aug 2021 11:35:12 +0200 Subject: [PATCH 2/2] Fix audience for comments under a remote event from group Signed-off-by: Thomas Citharel --- lib/federation/activity_pub/audience.ex | 31 ++++++++++++------- .../federation/activity_pub/audience_test.exs | 26 ++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/lib/federation/activity_pub/audience.ex b/lib/federation/activity_pub/audience.ex index fb98f1e23..e78513050 100644 --- a/lib/federation/activity_pub/audience.ex +++ b/lib/federation/activity_pub/audience.ex @@ -52,16 +52,14 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do }) do with {to, cc} <- extract_actors_from_mentions(mentions, actor, visibility), - {to, cc} <- {Enum.uniq(to ++ add_in_reply_to(in_reply_to_comment)), cc}, - {to, cc} <- {Enum.uniq(to ++ add_event_author(event)), cc}, + {to, cc} <- {to ++ add_in_reply_to(in_reply_to_comment), cc}, + {to, cc} <- add_event_organizers(event, to, cc), {to, cc} <- {to, - Enum.uniq( - cc ++ - add_comments_authors([origin_comment]) ++ - add_shares_actors_followers(url) - )} do - %{"to" => to, "cc" => cc} + cc ++ + add_comments_authors([origin_comment]) ++ + add_shares_actors_followers(url)} do + %{"to" => Enum.uniq(to), "cc" => Enum.uniq(cc)} 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(_), do: [] - defp add_event_author(%Event{} = event) do - [Repo.preload(event, [:organizer_actor]).organizer_actor.url] + defp add_event_organizers(%Event{} = event, to, cc) do + 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 - defp add_event_author(_), do: [] + defp add_event_organizers(_, to, cc), do: {to, cc} defp add_comment_author(%Comment{} = comment) do case Repo.preload(comment, [:actor]) do diff --git a/test/federation/activity_pub/audience_test.exs b/test/federation/activity_pub/audience_test.exs index bf16bc0da..4549698c9 100644 --- a/test/federation/activity_pub/audience_test.exs +++ b/test/federation/activity_pub/audience_test.exs @@ -247,6 +247,32 @@ defmodule Mobilizon.Federation.ActivityPub.AudienceTest do assert %{"to" => [members_url], "cc" => []} == Audience.get_audience(comment) 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 describe "participant" do