From b5d9b82bdd4676422cc17303b900fd045c9be042 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Tue, 28 Sep 2021 19:40:37 +0200 Subject: [PATCH] Refactor Mobilizon.Federation.ActivityPub and add typespecs Signed-off-by: Thomas Citharel --- .doctor.exs | 8 +- lib/config_provider.ex | 2 + lib/federation/activity_pub/actions/accept.ex | 169 ++++ .../activity_pub/actions/announce.ex | 60 ++ lib/federation/activity_pub/actions/create.ex | 71 ++ lib/federation/activity_pub/actions/delete.ex | 33 + lib/federation/activity_pub/actions/flag.ex | 31 + lib/federation/activity_pub/actions/follow.ex | 74 ++ lib/federation/activity_pub/actions/invite.ex | 86 ++ lib/federation/activity_pub/actions/join.ex | 51 + lib/federation/activity_pub/actions/leave.ex | 104 ++ lib/federation/activity_pub/actions/move.ex | 30 + lib/federation/activity_pub/actions/reject.ex | 127 +++ lib/federation/activity_pub/actions/remove.ex | 56 ++ lib/federation/activity_pub/actions/update.ex | 44 + lib/federation/activity_pub/activity_pub.ex | 908 +----------------- lib/federation/activity_pub/audience.ex | 45 +- lib/federation/activity_pub/federator.ex | 14 +- lib/federation/activity_pub/fetcher.ex | 88 +- lib/federation/activity_pub/publisher.ex | 128 +++ lib/federation/activity_pub/relay.ex | 7 +- lib/federation/activity_pub/transmogrifier.ex | 81 +- lib/federation/activity_pub/types/actors.ex | 9 +- lib/federation/activity_pub/types/comments.ex | 8 +- .../activity_pub/types/discussions.ex | 146 +-- lib/federation/activity_pub/types/entity.ex | 48 +- lib/federation/activity_pub/types/events.ex | 21 +- lib/federation/activity_pub/types/members.ex | 5 +- lib/federation/activity_pub/types/reports.ex | 65 +- .../activity_pub/types/resources.ex | 165 ++-- .../activity_pub/types/todo_lists.ex | 35 +- lib/federation/activity_pub/types/todos.ex | 92 +- .../activity_pub/types/tombstones.ex | 3 + lib/federation/activity_pub/utils.ex | 29 +- lib/federation/activity_stream.ex | 2 +- .../activity_stream/converter/discussion.ex | 5 +- .../activity_stream/converter/event.ex | 87 +- .../converter/event_metadata.ex | 3 + .../activity_stream/converter/todo.ex | 3 + lib/graphql/api/comments.ex | 11 +- lib/graphql/api/events.ex | 9 +- lib/graphql/api/follows.ex | 18 +- lib/graphql/api/groups.ex | 42 +- lib/graphql/api/participations.ex | 30 +- lib/graphql/api/reports.ex | 82 +- lib/graphql/resolvers/activity.ex | 3 + lib/graphql/resolvers/actor.ex | 42 +- lib/graphql/resolvers/admin.ex | 41 +- lib/graphql/resolvers/comment.ex | 76 +- lib/graphql/resolvers/config.ex | 7 +- lib/graphql/resolvers/discussion.ex | 65 +- lib/graphql/resolvers/event.ex | 22 + lib/graphql/resolvers/followers.ex | 6 +- lib/graphql/resolvers/group.ex | 123 ++- lib/graphql/resolvers/member.ex | 31 +- lib/graphql/resolvers/participant.ex | 6 + lib/graphql/resolvers/person.ex | 72 +- lib/graphql/resolvers/post.ex | 32 +- lib/graphql/resolvers/push_subscription.ex | 4 + lib/graphql/resolvers/report.ex | 10 + lib/graphql/resolvers/resource.ex | 51 +- lib/graphql/resolvers/search.ex | 11 +- lib/graphql/resolvers/statistics.ex | 1 + lib/graphql/resolvers/tag.ex | 9 +- lib/graphql/resolvers/todos.ex | 37 +- lib/graphql/resolvers/user.ex | 20 +- .../resolvers/users/activity_settings.ex | 6 +- lib/graphql/schema.ex | 1 + lib/mix/tasks/mobilizon/common.ex | 12 +- lib/mix/tasks/mobilizon/create_bot.ex | 3 +- lib/mix/tasks/mobilizon/site_map.ex | 1 + lib/mobilizon/activities/activities.ex | 6 + lib/mobilizon/actors/actor.ex | 1 + lib/mobilizon/actors/actors.ex | 3 +- lib/mobilizon/actors/member.ex | 2 + lib/mobilizon/addresses/address.ex | 2 + lib/mobilizon/cli.ex | 7 +- lib/mobilizon/config.ex | 4 +- lib/mobilizon/discussions/comment.ex | 1 + lib/mobilizon/discussions/discussion.ex | 2 + lib/mobilizon/discussions/discussions.ex | 36 +- lib/mobilizon/events/event.ex | 10 +- lib/mobilizon/events/events.ex | 5 +- lib/mobilizon/events/participant.ex | 1 + lib/mobilizon/posts/post.ex | 2 + lib/mobilizon/reports/report.ex | 1 + lib/mobilizon/resources/resource.ex | 1 + lib/mobilizon/storage/repo.ex | 1 + lib/mobilizon/todos/todo.ex | 2 + lib/mobilizon/todos/todo_list.ex | 2 + lib/mobilizon/users/users.ex | 4 +- lib/service/activity/member.ex | 2 +- lib/service/error_page.ex | 2 + lib/service/formatter/formatter.ex | 10 + lib/service/http/activity_pub.ex | 3 + lib/service/notifications/scheduler.ex | 6 +- lib/service/notifier/notifier.ex | 1 + .../rich_media/parsers/meta_tags_parser.ex | 6 + .../rich_media/parsers/oembed_parser.ex | 1 + lib/service/rich_media/parsers/ogp.ex | 2 + lib/service/site_map.ex | 2 + lib/service/statistics/statistics.ex | 1 + lib/web/auth/error_handler.ex | 1 + lib/web/auth/guardian.ex | 6 + lib/web/controllers/auth_controller.ex | 4 + lib/web/controllers/fallback_controller.ex | 1 + lib/web/controllers/feed_controller.ex | 7 + lib/web/controllers/media_proxy_controller.ex | 2 + lib/web/controllers/node_info_controller.ex | 2 + lib/web/controllers/page_controller.ex | 73 +- lib/web/controllers/web_finger_controller.ex | 2 + lib/web/email/event.ex | 16 + lib/web/email/notification.ex | 5 + lib/web/media_proxy.ex | 14 + lib/web/plugs/detect_locale_plug.ex | 7 + lib/web/plugs/federating.ex | 2 + lib/web/plugs/http_security_plug.ex | 2 + lib/web/plugs/mapped_signature_to_identity.ex | 2 + lib/web/plugs/uploaded_media.ex | 2 + .../activity_pub/activity_pub_test.exs | 39 +- .../transmogrifier/follow_test.exs | 10 +- .../activity_pub/transmogrifier/join_test.exs | 8 +- .../transmogrifier/leave_test.exs | 5 +- test/graphql/resolvers/group_test.exs | 2 +- test/graphql/resolvers/resource_test.exs | 2 +- 125 files changed, 2497 insertions(+), 1673 deletions(-) create mode 100644 lib/federation/activity_pub/actions/accept.ex create mode 100644 lib/federation/activity_pub/actions/announce.ex create mode 100644 lib/federation/activity_pub/actions/create.ex create mode 100644 lib/federation/activity_pub/actions/delete.ex create mode 100644 lib/federation/activity_pub/actions/flag.ex create mode 100644 lib/federation/activity_pub/actions/follow.ex create mode 100644 lib/federation/activity_pub/actions/invite.ex create mode 100644 lib/federation/activity_pub/actions/join.ex create mode 100644 lib/federation/activity_pub/actions/leave.ex create mode 100644 lib/federation/activity_pub/actions/move.ex create mode 100644 lib/federation/activity_pub/actions/reject.ex create mode 100644 lib/federation/activity_pub/actions/remove.ex create mode 100644 lib/federation/activity_pub/actions/update.ex create mode 100644 lib/federation/activity_pub/publisher.ex diff --git a/.doctor.exs b/.doctor.exs index 593403e0..bbbcb158 100644 --- a/.doctor.exs +++ b/.doctor.exs @@ -1,12 +1,12 @@ %Doctor.Config{ exception_moduledoc_required: true, failed: false, - ignore_modules: [], + ignore_modules: [Mobilizon.Web, Mobilizon.GraphQL.Schema, Mobilizon.Service.Activity.Renderer, Mobilizon.Service.Workers.Helper], ignore_paths: [], - min_module_doc_coverage: 70, + min_module_doc_coverage: 100, min_module_spec_coverage: 50, - min_overall_doc_coverage: 90, - min_overall_spec_coverage: 30, + min_overall_doc_coverage: 100, + min_overall_spec_coverage: 90, moduledoc_required: true, raise: false, reporter: Doctor.Reporters.Full, diff --git a/lib/config_provider.ex b/lib/config_provider.ex index 6b393f23..e199b0ea 100644 --- a/lib/config_provider.ex +++ b/lib/config_provider.ex @@ -4,8 +4,10 @@ defmodule Mobilizon.ConfigProvider do """ @behaviour Config.Provider + @spec init(String.t()) :: String.t() def init(path) when is_binary(path), do: path + @spec load(Keyword.t(), String.t()) :: Keyword.t() def load(config, path) do config_path = System.get_env("MOBILIZON_CONFIG_PATH") || path diff --git a/lib/federation/activity_pub/actions/accept.ex b/lib/federation/activity_pub/actions/accept.ex new file mode 100644 index 00000000..63d15303 --- /dev/null +++ b/lib/federation/activity_pub/actions/accept.ex @@ -0,0 +1,169 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Accept do + @moduledoc """ + Accept things + """ + + alias Mobilizon.{Actors, Events} + alias Mobilizon.Actors.{Actor, Follower, Member} + alias Mobilizon.Events.Participant + alias Mobilizon.Federation.ActivityPub.{Audience, Refresher} + alias Mobilizon.Federation.ActivityStream + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Service.Notifications.Scheduler + alias Mobilizon.Web.Endpoint + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + make_accept_join_data: 2, + create_activity: 2, + maybe_federate: 1, + maybe_relay_if_group_activity: 1 + ] + + @type acceptable_types :: :join | :follow | :invite + @type acceptable_entities :: + accept_join_entities | accept_follow_entities | accept_invite_entities + + @spec accept(acceptable_types, acceptable_entities, boolean, map) :: + {:ok, ActivityStream.t(), acceptable_entities} + def accept(type, entity, local \\ true, additional \\ %{}) do + Logger.debug("We're accepting something") + + accept_res = + case type do + :join -> accept_join(entity, additional) + :follow -> accept_follow(entity, additional) + :invite -> accept_invite(entity, additional) + end + + with {:ok, entity, update_data} <- accept_res do + {:ok, activity} = create_activity(update_data, local) + maybe_federate(activity) + maybe_relay_if_group_activity(activity) + {:ok, activity, entity} + end + end + + @type accept_follow_entities :: Follower.t() + + @spec accept_follow(Follower.t(), map) :: + {:ok, Follower.t(), Activity.t()} | {:error, Ecto.Changeset.t()} + defp accept_follow(%Follower{} = follower, additional) do + with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}) do + follower_as_data = Convertible.model_to_as(follower) + + update_data = + make_accept_join_data( + follower_as_data, + Map.merge(additional, %{ + "id" => "#{Endpoint.url()}/accept/follow/#{follower.id}", + "to" => [follower.actor.url], + "cc" => [], + "actor" => follower.target_actor.url + }) + ) + + {:ok, follower, update_data} + end + end + + @type accept_join_entities :: Participant.t() | Member.t() + + @spec accept_join(Participant.t() | Member.t(), map) :: + {:ok, Participant.t() | Member.t(), Activity.t()} | {:error, Ecto.Changeset.t()} + defp accept_join(%Participant{} = participant, additional) do + with {:ok, %Participant{} = participant} <- + Events.update_participant(participant, %{role: :participant}) do + Absinthe.Subscription.publish(Endpoint, participant.actor, + event_person_participation_changed: participant.actor.id + ) + + Scheduler.trigger_notifications_for_participant(participant) + participant_as_data = Convertible.model_to_as(participant) + audience = Audience.get_audience(participant) + + accept_join_data = + make_accept_join_data( + participant_as_data, + Map.merge(Map.merge(audience, additional), %{ + "id" => "#{Endpoint.url()}/accept/join/#{participant.id}" + }) + ) + + {:ok, participant, accept_join_data} + end + end + + defp accept_join(%Member{} = member, additional) do + with {:ok, %Member{} = member} <- + Actors.update_member(member, %{role: :member}) do + Mobilizon.Service.Activity.Member.insert_activity(member, + subject: "member_approved" + ) + + maybe_refresh_group(member) + + Absinthe.Subscription.publish(Endpoint, member.actor, + group_membership_changed: [ + Actor.preferred_username_and_domain(member.parent), + member.actor.id + ] + ) + + member_as_data = Convertible.model_to_as(member) + audience = Audience.get_audience(member) + + accept_join_data = + make_accept_join_data( + member_as_data, + Map.merge(Map.merge(audience, additional), %{ + "id" => "#{Endpoint.url()}/accept/join/#{member.id}" + }) + ) + + {:ok, member, accept_join_data} + end + end + + @type accept_invite_entities :: Member.t() + + @spec accept_invite(Member.t(), map()) :: + {:ok, Member.t(), Activity.t()} | {:error, Ecto.Changeset.t()} + defp accept_invite( + %Member{invited_by_id: invited_by_id, actor_id: actor_id} = member, + _additional + ) do + with %Actor{} = inviter <- Actors.get_actor!(invited_by_id), + %Actor{url: actor_url} <- Actors.get_actor!(actor_id), + {:ok, %Member{id: member_id} = member} <- + Actors.update_member(member, %{role: :member}) do + Mobilizon.Service.Activity.Member.insert_activity(member, + subject: "member_accepted_invitation" + ) + + maybe_refresh_group(member) + + accept_data = %{ + "type" => "Accept", + "attributedTo" => member.parent.url, + "to" => [inviter.url, member.parent.members_url], + "cc" => [member.parent.url], + "actor" => actor_url, + "object" => Convertible.model_to_as(member), + "id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}" + } + + {:ok, member, accept_data} + end + end + + @spec maybe_refresh_group(Member.t()) :: :ok | nil + defp maybe_refresh_group(%Member{ + parent: %Actor{domain: parent_domain, url: parent_url}, + actor: %Actor{} = actor + }) do + unless is_nil(parent_domain), + do: Refresher.fetch_group(parent_url, actor) + end +end diff --git a/lib/federation/activity_pub/actions/announce.ex b/lib/federation/activity_pub/actions/announce.ex new file mode 100644 index 00000000..125b563c --- /dev/null +++ b/lib/federation/activity_pub/actions/announce.ex @@ -0,0 +1,60 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Announce do + @moduledoc """ + Announce things + """ + alias Mobilizon.Actors.Actor + alias Mobilizon.Federation.ActivityPub.Activity + alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor + alias Mobilizon.Federation.ActivityStream + alias Mobilizon.Share + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1, + make_announce_data: 3, + make_announce_data: 4, + make_unannounce_data: 3 + ] + + @doc """ + Announce (reshare) an activity to the world, using an activity of type `Announce`. + """ + @spec announce(Actor.t(), ActivityStream.t(), String.t() | nil, boolean, boolean) :: + {:ok, Activity.t(), ActivityStream.t()} | {:error, any()} + def announce( + %Actor{} = actor, + object, + activity_id \\ nil, + local \\ true, + public \\ true + ) do + with {:ok, %Actor{id: object_owner_actor_id}} <- + ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]), + {:ok, %Share{} = _share} <- Share.create(object["id"], actor.id, object_owner_actor_id) do + announce_data = make_announce_data(actor, object, activity_id, public) + {:ok, activity} = create_activity(announce_data, local) + :ok = maybe_federate(activity) + {:ok, activity, object} + end + end + + @doc """ + Cancel the announcement of an activity to the world, using an activity of type `Undo` an `Announce`. + """ + @spec unannounce(Actor.t(), ActivityStream.t(), String.t() | nil, String.t() | nil, boolean) :: + {:ok, Activity.t(), ActivityStream.t()} + def unannounce( + %Actor{} = actor, + object, + activity_id \\ nil, + cancelled_activity_id \\ nil, + local \\ true + ) do + announce_activity = make_announce_data(actor, object, cancelled_activity_id) + unannounce_data = make_unannounce_data(actor, announce_activity, activity_id) + {:ok, unannounce_activity} = create_activity(unannounce_data, local) + maybe_federate(unannounce_activity) + {:ok, unannounce_activity, object} + end +end diff --git a/lib/federation/activity_pub/actions/create.ex b/lib/federation/activity_pub/actions/create.ex new file mode 100644 index 00000000..fb2d1b3b --- /dev/null +++ b/lib/federation/activity_pub/actions/create.ex @@ -0,0 +1,71 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Create do + @moduledoc """ + Create things + """ + alias Mobilizon.Tombstone + alias Mobilizon.Federation.ActivityPub.{Activity, Types} + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1, + maybe_relay_if_group_activity: 1 + ] + + @type create_entities :: + :event | :comment | :discussion | :actor | :todo_list | :todo | :resource | :post + + @doc """ + Create an activity of type `Create` + + * Creates the object, which returns AS data + * Wraps ActivityStreams data into a `Create` activity + * Creates an `Mobilizon.Federation.ActivityPub.Activity` from this + * Federates (asynchronously) the activity + * Returns the activity + """ + @spec create(create_entities(), map(), boolean, map()) :: + {:ok, Activity.t(), Entity.t()} + | {:error, :entity_tombstoned | atom() | Ecto.Changeset.t()} + def create(type, args, local \\ false, additional \\ %{}) do + Logger.debug("creating an activity") + Logger.debug(inspect(args)) + + case check_for_tombstones(args) do + nil -> + case do_create(type, args, additional) do + {:ok, entity, create_data} -> + {:ok, activity} = create_activity(create_data, local) + maybe_federate(activity) + maybe_relay_if_group_activity(activity) + {:ok, activity, entity} + + {:error, err} -> + {:error, err} + end + + %Tombstone{} -> + {:error, :entity_tombstoned} + end + end + + @spec do_create(create_entities(), map(), map()) :: + {:ok, Entity.t(), Activity.t()} | {:error, Ecto.Changeset.t() | atom()} + defp do_create(type, args, additional) do + case type do + :event -> Types.Events.create(args, additional) + :comment -> Types.Comments.create(args, additional) + :discussion -> Types.Discussions.create(args, additional) + :actor -> Types.Actors.create(args, additional) + :todo_list -> Types.TodoLists.create(args, additional) + :todo -> Types.Todos.create(args, additional) + :resource -> Types.Resources.create(args, additional) + :post -> Types.Posts.create(args, additional) + end + end + + @spec check_for_tombstones(map()) :: Tombstone.t() | nil + defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url) + defp check_for_tombstones(_), do: nil +end diff --git a/lib/federation/activity_pub/actions/delete.ex b/lib/federation/activity_pub/actions/delete.ex new file mode 100644 index 00000000..8530f6f5 --- /dev/null +++ b/lib/federation/activity_pub/actions/delete.ex @@ -0,0 +1,33 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Delete do + @moduledoc """ + Delete things + """ + alias Mobilizon.Actors.Actor + alias Mobilizon.Federation.ActivityPub.Activity + alias Mobilizon.Federation.ActivityPub.Types.{Entity, Managable, Ownable} + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1, + maybe_relay_if_group_activity: 2, + check_for_actor_key_rotation: 1 + ] + + @doc """ + Delete an entity, using an activity of type `Delete` + """ + @spec delete(Entity.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Entity.t()} + def delete(object, actor, local \\ true, additional \\ %{}) do + with {:ok, activity_data, actor, object} <- + Managable.delete(object, actor, local, additional), + group <- Ownable.group_actor(object), + :ok <- check_for_actor_key_rotation(actor), + {:ok, activity} <- create_activity(activity_data, local), + :ok <- maybe_federate(activity), + :ok <- maybe_relay_if_group_activity(activity, group) do + {:ok, activity, object} + end + end +end diff --git a/lib/federation/activity_pub/actions/flag.ex b/lib/federation/activity_pub/actions/flag.ex new file mode 100644 index 00000000..50def2cd --- /dev/null +++ b/lib/federation/activity_pub/actions/flag.ex @@ -0,0 +1,31 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Flag do + @moduledoc """ + Delete things + """ + alias Mobilizon.Users + alias Mobilizon.Federation.ActivityPub.{Activity, Types} + alias Mobilizon.Web.Email.{Admin, Mailer} + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1 + ] + + @spec flag(map, boolean, map) :: {:ok, Activity.t(), Report.t()} | {:error, Ecto.Changeset.t()} + def flag(args, local \\ false, additional \\ %{}) do + with {:ok, report, report_as_data} <- Types.Reports.flag(args, local, additional) do + {:ok, activity} = create_activity(report_as_data, local) + maybe_federate(activity) + + Enum.each(Users.list_moderators(), fn moderator -> + moderator + |> Admin.report(report) + |> Mailer.send_email_later() + end) + + {:ok, activity, report} + end + end +end diff --git a/lib/federation/activity_pub/actions/follow.ex b/lib/federation/activity_pub/actions/follow.ex new file mode 100644 index 00000000..75f67de3 --- /dev/null +++ b/lib/federation/activity_pub/actions/follow.ex @@ -0,0 +1,74 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Follow do + @moduledoc """ + Follow people + """ + + alias Mobilizon.Actors + alias Mobilizon.Actors.{Actor, Follower} + alias Mobilizon.Federation.ActivityPub.Types + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Web.Endpoint + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1, + make_unfollow_data: 4 + ] + + @doc """ + Make an actor follow another, using an activity of type `Follow` + """ + @spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean, map) :: + {:ok, Activity.t(), Follower.t()} | {:error, atom | Ecto.Changeset.t() | String.t()} + def follow( + %Actor{} = follower, + %Actor{} = followed, + activity_id \\ nil, + local \\ true, + additional \\ %{} + ) do + if followed.id != follower.id do + case Types.Actors.follow( + follower, + followed, + local, + Map.merge(additional, %{"activity_id" => activity_id}) + ) do + {:ok, activity_data, %Follower{} = follower} -> + {:ok, activity} = create_activity(activity_data, local) + maybe_federate(activity) + {:ok, activity, follower} + + {:error, err} -> + {:error, err} + end + else + {:error, "Can't follow yourself"} + end + end + + @doc """ + Make an actor unfollow another, using an activity of type `Undo` a `Follow`. + """ + @spec unfollow(Actor.t(), Actor.t(), String.t() | nil, boolean()) :: + {:ok, Activity.t(), Follower.t()} | {:error, String.t()} + def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do + with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower) do + # We recreate the follow activity + follow_as_data = + Convertible.model_to_as(%{follow | actor: follower, target_actor: followed}) + + {:ok, follow_activity} = create_activity(follow_as_data, local) + activity_unfollow_id = activity_id || "#{Endpoint.url()}/unfollow/#{follow_id}/activity" + + unfollow_data = + make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id) + + {:ok, activity} = create_activity(unfollow_data, local) + maybe_federate(activity) + {:ok, activity, follow} + end + end +end diff --git a/lib/federation/activity_pub/actions/invite.ex b/lib/federation/activity_pub/actions/invite.ex new file mode 100644 index 00000000..18b065f9 --- /dev/null +++ b/lib/federation/activity_pub/actions/invite.ex @@ -0,0 +1,86 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Invite do + @moduledoc """ + Invite people to things + """ + alias Mobilizon.Actors + alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Web.Email.Group + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1, + maybe_relay_if_group_activity: 1 + ] + + @spec invite(Actor.t(), Actor.t(), Actor.t(), boolean, map()) :: + {:ok, map(), Member.t()} | {:error, :not_able_to_invite | Ecto.Changeset.t()} + def invite( + %Actor{url: group_url, id: group_id, members_url: members_url} = group, + %Actor{url: actor_url, id: actor_id} = actor, + %Actor{url: target_actor_url, id: target_actor_id} = _target_actor, + local \\ true, + additional \\ %{} + ) do + Logger.debug("Handling #{actor_url} invite to #{group_url} sent to #{target_actor_url}") + + if is_able_to_invite?(actor, group) do + with {:ok, %Member{url: member_url} = member} <- + Actors.create_member(%{ + parent_id: group_id, + actor_id: target_actor_id, + role: :invited, + invited_by_id: actor_id, + url: Map.get(additional, :url) + }) do + Mobilizon.Service.Activity.Member.insert_activity(member, + moderator: actor, + subject: "member_invited" + ) + + {:ok, activity} = + create_activity( + %{ + "type" => "Invite", + "attributedTo" => group_url, + "actor" => actor_url, + "object" => group_url, + "target" => target_actor_url, + "id" => member_url + } + |> Map.merge(%{"to" => [target_actor_url, members_url], "cc" => [group_url]}) + |> Map.merge(additional), + local + ) + + maybe_federate(activity) + maybe_relay_if_group_activity(activity) + Group.send_invite_to_user(member) + {:ok, activity, member} + end + else + {:error, :not_able_to_invite} + end + end + + @spec is_able_to_invite?(Actor.t(), Actor.t()) :: boolean + defp is_able_to_invite?(%Actor{domain: actor_domain, id: actor_id}, %Actor{ + domain: group_domain, + id: group_id + }) do + # If the actor comes from the same domain we trust it + if actor_domain == group_domain do + true + else + # If local group, we'll send the invite + case Actors.get_member(actor_id, group_id) do + {:ok, %Member{} = admin_member} -> + Member.is_administrator(admin_member) + + _ -> + false + end + end + end +end diff --git a/lib/federation/activity_pub/actions/join.ex b/lib/federation/activity_pub/actions/join.ex new file mode 100644 index 00000000..3c760b91 --- /dev/null +++ b/lib/federation/activity_pub/actions/join.ex @@ -0,0 +1,51 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Join do + @moduledoc """ + Join things + """ + + alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Federation.ActivityPub.Types + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1 + ] + + @doc """ + Join an entity (an event or a group), using an activity of type `Join` + """ + @spec join(Event.t(), Actor.t(), boolean, map) :: + {:ok, Activity.t(), Participant.t()} | {:error, :maximum_attendee_capacity} + @spec join(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()} + def join(entity_to_join, actor_joining, local \\ true, additional \\ %{}) + + def join(%Event{} = event, %Actor{} = actor, local, additional) do + case Types.Events.join(event, actor, local, additional) do + {:ok, activity_data, participant} -> + {:ok, activity} = create_activity(activity_data, local) + maybe_federate(activity) + {:ok, activity, participant} + + {:error, :maximum_attendee_capacity_reached} -> + {:error, :maximum_attendee_capacity_reached} + + {:accept, accept} -> + accept + end + end + + def join(%Actor{type: :Group} = group, %Actor{} = actor, local, additional) do + with {:ok, activity_data, %Member{} = member} <- + Types.Actors.join(group, actor, local, additional), + {:ok, activity} <- create_activity(activity_data, local), + :ok <- maybe_federate(activity) do + {:ok, activity, member} + else + {:accept, accept} -> + accept + end + end +end diff --git a/lib/federation/activity_pub/actions/leave.ex b/lib/federation/activity_pub/actions/leave.ex new file mode 100644 index 00000000..aef1259a --- /dev/null +++ b/lib/federation/activity_pub/actions/leave.ex @@ -0,0 +1,104 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Leave do + @moduledoc """ + Leave things + """ + + alias Mobilizon.{Actors, Events} + alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Federation.ActivityPub.Audience + alias Mobilizon.Web.Endpoint + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1, + maybe_relay_if_group_activity: 1 + ] + + @spec leave(Event.t(), Actor.t(), boolean, map) :: + {:ok, Activity.t(), Participant.t()} + | {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()} + @spec leave(Actor.t(), Actor.t(), boolean, map) :: + {:ok, Activity.t(), Member.t()} | {:error, atom() | Ecto.Changeset.t()} + def leave(object, actor, local \\ true, additional \\ %{}) + + @doc """ + Leave an event or a group + """ + def leave( + %Event{id: event_id, url: event_url} = _event, + %Actor{id: actor_id, url: actor_url} = _actor, + local, + additional + ) do + if Participant.is_not_only_organizer(event_id, actor_id) do + {:error, :is_only_organizer} + else + case Mobilizon.Events.get_participant( + event_id, + actor_id, + Map.get(additional, :metadata, %{}) + ) do + {:ok, %Participant{} = participant} -> + case Events.delete_participant(participant) do + {:ok, %{participant: %Participant{} = participant}} -> + leave_data = %{ + "type" => "Leave", + # If it's an exclusion it should be something else + "actor" => actor_url, + "object" => event_url, + "id" => "#{Endpoint.url()}/leave/event/#{participant.id}" + } + + audience = Audience.get_audience(participant) + {:ok, activity} = create_activity(Map.merge(leave_data, audience), local) + maybe_federate(activity) + {:ok, activity, participant} + + {:error, _type, %Ecto.Changeset{} = err, _} -> + {:error, err} + end + + {:error, :participant_not_found} -> + {:error, :participant_not_found} + end + end + end + + def leave( + %Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url}, + %Actor{id: actor_id, url: actor_url}, + local, + additional + ) do + with {:member, {:ok, %Member{id: member_id} = member}} <- + {:member, Actors.get_member(actor_id, group_id)}, + {:is_not_only_admin, true} <- + {:is_not_only_admin, + Map.get(additional, :force_member_removal, false) || + !Actors.is_only_administrator?(member_id, group_id)}, + {:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)} do + Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_quit") + + leave_data = %{ + "to" => [group_members_url], + "cc" => [group_url], + "attributedTo" => group_url, + "type" => "Leave", + "actor" => actor_url, + "object" => group_url + } + + {:ok, activity} = create_activity(leave_data, local) + maybe_federate(activity) + maybe_relay_if_group_activity(activity) + {:ok, activity, member} + else + {:member, nil} -> {:error, :member_not_found} + {:is_not_only_admin, false} -> {:error, :is_not_only_admin} + {:error, %Ecto.Changeset{} = err} -> {:error, err} + end + end +end diff --git a/lib/federation/activity_pub/actions/move.ex b/lib/federation/activity_pub/actions/move.ex new file mode 100644 index 00000000..037e0f5b --- /dev/null +++ b/lib/federation/activity_pub/actions/move.ex @@ -0,0 +1,30 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Move do + @moduledoc """ + Move things + """ + alias Mobilizon.Resources.Resource + alias Mobilizon.Federation.ActivityPub.{Activity, Types} + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1 + ] + + @spec move(:resource, Resource.t(), map, boolean, map) :: + {:ok, Activity.t(), Resource.t()} | {:error, Ecto.Changeset.t() | atom()} + def move(type, old_entity, args, local \\ false, additional \\ %{}) do + Logger.debug("We're moving something") + Logger.debug(inspect(args)) + + with {:ok, entity, update_data} <- + (case type do + :resource -> Types.Resources.move(old_entity, args, additional) + end) do + {:ok, activity} = create_activity(update_data, local) + maybe_federate(activity) + {:ok, activity, entity} + end + end +end diff --git a/lib/federation/activity_pub/actions/reject.ex b/lib/federation/activity_pub/actions/reject.ex new file mode 100644 index 00000000..cdc0ae50 --- /dev/null +++ b/lib/federation/activity_pub/actions/reject.ex @@ -0,0 +1,127 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Reject do + @moduledoc """ + Reject things + """ + + alias Mobilizon.{Actors, Events} + alias Mobilizon.Actors.{Actor, Follower, Member} + alias Mobilizon.Events.Participant + alias Mobilizon.Federation.ActivityPub.Actions.Accept + alias Mobilizon.Federation.ActivityPub.Audience + alias Mobilizon.Federation.ActivityStream + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Web.Endpoint + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1, + maybe_relay_if_group_activity: 1 + ] + + @spec reject(Accept.acceptable_types(), Accept.acceptable_entities(), boolean, map) :: + {:ok, ActivityStream.t(), Accept.acceptable_entities()} + def reject(type, entity, local \\ true, additional \\ %{}) do + {:ok, entity, update_data} = + case type do + :join -> reject_join(entity, additional) + :follow -> reject_follow(entity, additional) + :invite -> reject_invite(entity, additional) + end + + with {:ok, activity} <- create_activity(update_data, local), + :ok <- maybe_federate(activity), + :ok <- maybe_relay_if_group_activity(activity) do + {:ok, activity, entity} + else + err -> + Logger.error("Something went wrong while creating an activity") + Logger.debug(inspect(err)) + err + end + end + + @spec reject_join(Participant.t(), map()) :: {:ok, Participant.t(), Activity.t()} | any() + defp reject_join(%Participant{} = participant, additional) do + with {:ok, %Participant{} = participant} <- + Events.update_participant(participant, %{role: :rejected}), + Absinthe.Subscription.publish(Endpoint, participant.actor, + event_person_participation_changed: participant.actor.id + ), + participant_as_data <- Convertible.model_to_as(participant), + audience <- + participant + |> Audience.get_audience() + |> Map.merge(additional), + reject_data <- %{ + "type" => "Reject", + "object" => participant_as_data + }, + update_data <- + reject_data + |> Map.merge(audience) + |> Map.merge(%{ + "id" => "#{Endpoint.url()}/reject/join/#{participant.id}" + }) do + {:ok, participant, update_data} + else + err -> + Logger.error("Something went wrong while creating an update activity") + Logger.debug(inspect(err)) + err + end + end + + @spec reject_follow(Follower.t(), map()) :: {:ok, Follower.t(), Activity.t()} | any() + defp reject_follow(%Follower{} = follower, additional) do + with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower), + follower_as_data <- Convertible.model_to_as(follower), + audience <- + follower.actor |> Audience.get_audience() |> Map.merge(additional), + reject_data <- %{ + "to" => [follower.actor.url], + "type" => "Reject", + "actor" => follower.target_actor.url, + "object" => follower_as_data + }, + update_data <- + audience + |> Map.merge(reject_data) + |> Map.merge(%{ + "id" => "#{Endpoint.url()}/reject/follow/#{follower.id}" + }) do + {:ok, follower, update_data} + else + err -> + Logger.error("Something went wrong while creating an update activity") + Logger.debug(inspect(err)) + err + end + end + + @spec reject_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any + defp reject_invite( + %Member{invited_by_id: invited_by_id, actor_id: actor_id} = member, + _additional + ) do + with %Actor{} = inviter <- Actors.get_actor(invited_by_id), + %Actor{url: actor_url} <- Actors.get_actor(actor_id), + {:ok, %Member{url: member_url, id: member_id} = member} <- + Actors.delete_member(member), + Mobilizon.Service.Activity.Member.insert_activity(member, + subject: "member_rejected_invitation" + ), + accept_data <- %{ + "type" => "Reject", + "actor" => actor_url, + "attributedTo" => member.parent.url, + "to" => [inviter.url, member.parent.members_url], + "cc" => [member.parent.url], + "object" => member_url, + "id" => "#{Endpoint.url()}/reject/invite/member/#{member_id}" + } do + {:ok, member, accept_data} + end + end +end diff --git a/lib/federation/activity_pub/actions/remove.ex b/lib/federation/activity_pub/actions/remove.ex new file mode 100644 index 00000000..9663f5f9 --- /dev/null +++ b/lib/federation/activity_pub/actions/remove.ex @@ -0,0 +1,56 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Remove do + @moduledoc """ + Remove things + """ + alias Mobilizon.Actors + alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Federation.ActivityPub.Activity + alias Mobilizon.Web.Email.Group + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1, + maybe_relay_if_group_activity: 1 + ] + + @doc """ + Remove an activity, using an activity of type `Remove` + """ + @spec remove(Member.t(), Actor.t(), Actor.t(), boolean, map) :: + {:ok, Activity.t(), Member.t()} | {:error, :member_not_found | Ecto.Changeset.t()} + def remove( + %Member{} = member, + %Actor{type: :Group, url: group_url, members_url: group_members_url}, + %Actor{url: moderator_url} = moderator, + local, + _additional \\ %{} + ) do + with {:ok, %Member{id: member_id}} <- Actors.update_member(member, %{role: :rejected}), + %Member{} = member <- Actors.get_member(member_id) do + Mobilizon.Service.Activity.Member.insert_activity(member, + moderator: moderator, + subject: "member_removed" + ) + + Group.send_notification_to_removed_member(member) + + remove_data = %{ + "to" => [group_members_url], + "type" => "Remove", + "actor" => moderator_url, + "object" => member.url, + "origin" => group_url + } + + {:ok, activity} = create_activity(remove_data, local) + maybe_federate(activity) + maybe_relay_if_group_activity(activity) + {:ok, activity, member} + else + nil -> {:error, :member_not_found} + {:error, %Ecto.Changeset{} = err} -> {:error, err} + end + end +end diff --git a/lib/federation/activity_pub/actions/update.ex b/lib/federation/activity_pub/actions/update.ex new file mode 100644 index 00000000..1306934b --- /dev/null +++ b/lib/federation/activity_pub/actions/update.ex @@ -0,0 +1,44 @@ +defmodule Mobilizon.Federation.ActivityPub.Actions.Update do + @moduledoc """ + Update things + """ + alias Mobilizon.Federation.ActivityPub.Activity + alias Mobilizon.Federation.ActivityPub.Types.Managable + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [ + create_activity: 2, + maybe_federate: 1, + maybe_relay_if_group_activity: 1 + ] + + @doc """ + Create an activity of type `Update` + + * Updates the object, which returns AS data + * Wraps ActivityStreams data into a `Update` activity + * Creates an `Mobilizon.Federation.ActivityPub.Activity` from this + * Federates (asynchronously) the activity + * Returns the activity + """ + @spec update(Entity.t(), map(), boolean, map()) :: + {:ok, Activity.t(), Entity.t()} | {:error, atom() | Ecto.Changeset.t()} + def update(old_entity, args, local \\ false, additional \\ %{}) do + Logger.debug("updating an activity") + Logger.debug(inspect(args)) + + case Managable.update(old_entity, args, additional) do + {:ok, entity, update_data} -> + {:ok, activity} = create_activity(update_data, local) + maybe_federate(activity) + maybe_relay_if_group_activity(activity) + {:ok, activity, entity} + + {:error, err} -> + Logger.error("Something went wrong while creating an activity") + Logger.debug(inspect(err)) + {:error, err} + end + end +end diff --git a/lib/federation/activity_pub/activity_pub.ex b/lib/federation/activity_pub/activity_pub.ex index df1c5c4e..7dd39c60 100644 --- a/lib/federation/activity_pub/activity_pub.ex +++ b/lib/federation/activity_pub/activity_pub.ex @@ -12,64 +12,34 @@ defmodule Mobilizon.Federation.ActivityPub do alias Mobilizon.{ Actors, - Config, Discussions, Events, Posts, - Resources, - Share, - Users + Resources } - alias Mobilizon.Actors.{Actor, Follower, Member} + alias Mobilizon.Actors.Actor alias Mobilizon.Discussions.Comment - alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Events.Event alias Mobilizon.Tombstone alias Mobilizon.Federation.ActivityPub.{ Activity, - Audience, - Federator, Fetcher, Preloader, - Refresher, - Relay, - Transmogrifier, - Types, - Visibility + Relay } - alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor - - alias Mobilizon.Federation.ActivityPub.Types.{Entity, Managable, Ownable} - alias Mobilizon.Federation.ActivityStream.Convertible - alias Mobilizon.Federation.HTTPSignatures.Signature - alias Mobilizon.Service.Notifications.Scheduler alias Mobilizon.Storage.Page alias Mobilizon.Web.Endpoint - alias Mobilizon.Web.Email.{Admin, Group, Mailer} require Logger @public_ap_adress "https://www.w3.org/ns/activitystreams#Public" - # Wraps an object into an activity - @spec create_activity(map(), boolean()) :: {:ok, Activity.t()} - defp create_activity(map, local) when is_map(map) do - with map <- lazy_put_activity_defaults(map) do - {:ok, - %Activity{ - data: map, - local: local, - actor: map["actor"], - recipients: get_recipients(map) - }} - end - end - @doc """ Fetch an object from an URL, from our local database of events and comments, then eventually remote """ @@ -79,33 +49,32 @@ defmodule Mobilizon.Federation.ActivityPub do def fetch_object_from_url(url, options \\ []) do Logger.info("Fetching object from url #{url}") - with {:not_http, true} <- {:not_http, String.starts_with?(url, "http")}, - {:existing, nil} <- - {:existing, Tombstone.find_tombstone(url)}, - {:existing, nil} <- {:existing, Events.get_event_by_url(url)}, - {:existing, nil} <- - {:existing, Discussions.get_discussion_by_url(url)}, - {:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)}, - {:existing, nil} <- {:existing, Resources.get_resource_by_url(url)}, - {:existing, nil} <- {:existing, Posts.get_post_by_url(url)}, - {:existing, nil} <- - {:existing, Actors.get_actor_by_url_2(url)}, - {:existing, nil} <- {:existing, Actors.get_member_by_url(url)}, - :ok <- Logger.info("Data for URL not found anywhere, going to fetch it"), - {:ok, _activity, entity} <- Fetcher.fetch_and_create(url, options) do - Logger.debug("Going to preload the new entity") - Preloader.maybe_preload(entity) + if String.starts_with?(url, "http") do + with {:existing, nil} <- + {:existing, Tombstone.find_tombstone(url)}, + {:existing, nil} <- {:existing, Events.get_event_by_url(url)}, + {:existing, nil} <- + {:existing, Discussions.get_discussion_by_url(url)}, + {:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)}, + {:existing, nil} <- {:existing, Resources.get_resource_by_url(url)}, + {:existing, nil} <- {:existing, Posts.get_post_by_url(url)}, + {:existing, nil} <- + {:existing, Actors.get_actor_by_url_2(url)}, + {:existing, nil} <- {:existing, Actors.get_member_by_url(url)}, + :ok <- Logger.info("Data for URL not found anywhere, going to fetch it"), + {:ok, _activity, entity} <- Fetcher.fetch_and_create(url, options) do + Logger.debug("Going to preload the new entity") + Preloader.maybe_preload(entity) + else + {:existing, entity} -> + handle_existing_entity(url, entity, options) + + {:error, e} -> + Logger.warn("Something failed while fetching url #{url} #{inspect(e)}") + {:error, e} + end else - {:existing, entity} -> - handle_existing_entity(url, entity, options) - - {:error, e} -> - Logger.warn("Something failed while fetching url #{url} #{inspect(e)}") - {:error, e} - - e -> - Logger.warn("Something failed while fetching url #{url} #{inspect(e)}") - {:error, e} + {:error, :url_not_http} end end @@ -132,7 +101,7 @@ defmodule Mobilizon.Federation.ActivityPub do end @spec refresh_entity(String.t(), struct(), Keyword.t()) :: - {:ok, struct()} | {:error, atom(), struct()} | {:error, String.t()} + {:ok, struct()} | {:error, atom(), struct()} | {:error, atom()} defp refresh_entity(url, entity, options) do force_fetch = Keyword.get(options, :force, false) @@ -149,609 +118,14 @@ defmodule Mobilizon.Federation.ActivityPub do {:error, :http_not_found} -> {:error, :http_not_found, entity} - {:error, "Object origin check failed"} -> - {:error, "Object origin check failed"} - end - else - {:ok, entity} - end - end - - @doc """ - Create an activity of type `Create` - - * Creates the object, which returns AS data - * Wraps ActivityStreams data into a `Create` activity - * Creates an `Mobilizon.Federation.ActivityPub.Activity` from this - * Federates (asynchronously) the activity - * Returns the activity - """ - @spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), Entity.entities()} | any() - def create(type, args, local \\ false, additional \\ %{}) do - Logger.debug("creating an activity") - Logger.debug(inspect(args)) - - with {:tombstone, nil} <- {:tombstone, check_for_tombstones(args)}, - {:ok, entity, create_data} <- - (case type do - :event -> Types.Events.create(args, additional) - :comment -> Types.Comments.create(args, additional) - :discussion -> Types.Discussions.create(args, additional) - :actor -> Types.Actors.create(args, additional) - :todo_list -> Types.TodoLists.create(args, additional) - :todo -> Types.Todos.create(args, additional) - :resource -> Types.Resources.create(args, additional) - :post -> Types.Posts.create(args, additional) - end), - {:ok, activity} <- create_activity(create_data, local), - :ok <- maybe_federate(activity), - :ok <- maybe_relay_if_group_activity(activity) do - {:ok, activity, entity} - else - err -> - Logger.error("Something went wrong while creating an activity") - Logger.debug(inspect(err)) - err - end - end - - @doc """ - Create an activity of type `Update` - - * Updates the object, which returns AS data - * Wraps ActivityStreams data into a `Update` activity - * Creates an `Mobilizon.Federation.ActivityPub.Activity` from this - * Federates (asynchronously) the activity - * Returns the activity - """ - @spec update(Entity.entities(), map(), boolean, map()) :: - {:ok, Activity.t(), Entity.entities()} | {:error, any()} - def update(old_entity, args, local \\ false, additional \\ %{}) do - Logger.debug("updating an activity") - Logger.debug(inspect(args)) - - case Managable.update(old_entity, args, additional) do - {:ok, entity, update_data} -> - {:ok, activity} = create_activity(update_data, local) - maybe_federate(activity) - maybe_relay_if_group_activity(activity) - {:ok, activity, entity} - - {:error, err} -> - Logger.error("Something went wrong while creating an activity") - Logger.debug(inspect(err)) - {:error, err} - end - end - - @type acceptable_types :: :join | :follow | :invite - @type acceptable_entities :: - accept_join_entities | accept_follow_entities | accept_invite_entities - - @spec accept(acceptable_types, acceptable_entities, boolean, map) :: - {:ok, ActivityStream.t(), acceptable_entities} - def accept(type, entity, local \\ true, additional \\ %{}) do - Logger.debug("We're accepting something") - - {:ok, entity, update_data} = - case type do - :join -> accept_join(entity, additional) - :follow -> accept_follow(entity, additional) - :invite -> accept_invite(entity, additional) - end - - with {:ok, activity} <- create_activity(update_data, local), - :ok <- maybe_federate(activity), - :ok <- maybe_relay_if_group_activity(activity) do - {:ok, activity, entity} - else - err -> - Logger.error("Something went wrong while creating an activity") - Logger.debug(inspect(err)) - err - end - end - - @spec reject(acceptable_types, acceptable_entities, boolean, map) :: - {:ok, ActivityStream.t(), acceptable_entities} - def reject(type, entity, local \\ true, additional \\ %{}) do - {:ok, entity, update_data} = - case type do - :join -> reject_join(entity, additional) - :follow -> reject_follow(entity, additional) - :invite -> reject_invite(entity, additional) - end - - with {:ok, activity} <- create_activity(update_data, local), - :ok <- maybe_federate(activity), - :ok <- maybe_relay_if_group_activity(activity) do - {:ok, activity, entity} - else - err -> - Logger.error("Something went wrong while creating an activity") - Logger.debug(inspect(err)) - err - end - end - - @spec announce(Actor.t(), ActivityStream.t(), String.t() | nil, boolean, boolean) :: - {:ok, Activity.t(), ActivityStream.t()} | {:error, any()} - def announce( - %Actor{} = actor, - object, - activity_id \\ nil, - local \\ true, - public \\ true - ) do - with {:ok, %Actor{id: object_owner_actor_id}} <- - ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]), - {:ok, %Share{} = _share} <- Share.create(object["id"], actor.id, object_owner_actor_id), - announce_data <- make_announce_data(actor, object, activity_id, public), - {:ok, activity} <- create_activity(announce_data, local), - :ok <- maybe_federate(activity) do - {:ok, activity, object} - else - error -> - {:error, error} - end - end - - @spec unannounce(Actor.t(), ActivityStream.t(), String.t() | nil, String.t() | nil, boolean) :: - {:ok, Activity.t(), ActivityStream.t()} - def unannounce( - %Actor{} = actor, - object, - activity_id \\ nil, - cancelled_activity_id \\ nil, - local \\ true - ) do - with announce_activity <- make_announce_data(actor, object, cancelled_activity_id), - unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id), - {:ok, unannounce_activity} <- create_activity(unannounce_data, local), - :ok <- maybe_federate(unannounce_activity) do - {:ok, unannounce_activity, object} - else - _e -> {:ok, object} - end - end - - @doc """ - Make an actor follow another - """ - @spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean, map) :: - {:ok, Activity.t(), Follower.t()} | {:error, atom | Ecto.Changeset.t() | String.t()} - def follow( - %Actor{} = follower, - %Actor{} = followed, - activity_id \\ nil, - local \\ true, - additional \\ %{} - ) do - if followed.id != follower.id do - case Types.Actors.follow( - follower, - followed, - local, - Map.merge(additional, %{"activity_id" => activity_id}) - ) do - {:ok, activity_data, %Follower{} = follower} -> - {:ok, activity} = create_activity(activity_data, local) - maybe_federate(activity) - {:ok, activity, follower} - {:error, err} -> {:error, err} end else - {:error, "Can't follow yourself"} + {:ok, entity} end end - @doc """ - Make an actor unfollow another - """ - @spec unfollow(Actor.t(), Actor.t(), String.t() | nil, boolean()) :: - {:ok, Activity.t(), Follower.t()} | {:error, String.t()} - def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do - with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower), - # We recreate the follow activity - follow_as_data <- - Convertible.model_to_as(%{follow | actor: follower, target_actor: followed}), - {:ok, follow_activity} <- create_activity(follow_as_data, local), - activity_unfollow_id <- - activity_id || "#{Endpoint.url()}/unfollow/#{follow_id}/activity", - unfollow_data <- - make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id), - {:ok, activity} <- create_activity(unfollow_data, local), - :ok <- maybe_federate(activity) do - {:ok, activity, follow} - else - err -> - Logger.debug("Error while unfollowing an actor #{inspect(err)}") - err - end - end - - @spec delete(Entity.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Entity.t()} - def delete(object, actor, local \\ true, additional \\ %{}) do - with {:ok, activity_data, actor, object} <- - Managable.delete(object, actor, local, additional), - group <- Ownable.group_actor(object), - :ok <- check_for_actor_key_rotation(actor), - {:ok, activity} <- create_activity(activity_data, local), - :ok <- maybe_federate(activity), - :ok <- maybe_relay_if_group_activity(activity, group) do - {:ok, activity, object} - end - end - - @spec join(Event.t(), Actor.t(), boolean, map) :: - {:ok, Activity.t(), Participant.t()} | {:error, :maximum_attendee_capacity} - @spec join(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()} - def join(entity_to_join, actor_joining, local \\ true, additional \\ %{}) - - def join(%Event{} = event, %Actor{} = actor, local, additional) do - case Types.Events.join(event, actor, local, additional) do - {:ok, activity_data, participant} -> - {:ok, activity} = create_activity(activity_data, local) - maybe_federate(activity) - {:ok, activity, participant} - - {:error, :maximum_attendee_capacity_reached} -> - {:error, :maximum_attendee_capacity_reached} - - {:accept, accept} -> - accept - end - end - - def join(%Actor{type: :Group} = group, %Actor{} = actor, local, additional) do - with {:ok, activity_data, %Member{} = member} <- - Types.Actors.join(group, actor, local, additional), - {:ok, activity} <- create_activity(activity_data, local), - :ok <- maybe_federate(activity) do - {:ok, activity, member} - else - {:accept, accept} -> - accept - end - end - - @spec leave(Event.t(), Actor.t(), boolean, map) :: - {:ok, Activity.t(), Participant.t()} - | {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()} - @spec leave(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()} - def leave(object, actor, local \\ true, additional \\ %{}) - - @doc """ - Leave an event or a group - """ - def leave( - %Event{id: event_id, url: event_url} = _event, - %Actor{id: actor_id, url: actor_url} = _actor, - local, - additional - ) do - if Participant.is_not_only_organizer(event_id, actor_id) do - {:error, :is_only_organizer} - else - case Mobilizon.Events.get_participant( - event_id, - actor_id, - Map.get(additional, :metadata, %{}) - ) do - {:ok, %Participant{} = participant} -> - case Events.delete_participant(participant) do - {:ok, %{participant: %Participant{} = participant}} -> - leave_data = %{ - "type" => "Leave", - # If it's an exclusion it should be something else - "actor" => actor_url, - "object" => event_url, - "id" => "#{Endpoint.url()}/leave/event/#{participant.id}" - } - - audience = Audience.get_audience(participant) - {:ok, activity} = create_activity(Map.merge(leave_data, audience), local) - maybe_federate(activity) - {:ok, activity, participant} - - {:error, _type, %Ecto.Changeset{} = err, _} -> - {:error, err} - end - - {:error, :participant_not_found} -> - {:error, :participant_not_found} - end - end - end - - def leave( - %Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url}, - %Actor{id: actor_id, url: actor_url}, - local, - additional - ) do - with {:member, {:ok, %Member{id: member_id} = member}} <- - {:member, Actors.get_member(actor_id, group_id)}, - {:is_not_only_admin, true} <- - {:is_not_only_admin, - Map.get(additional, :force_member_removal, false) || - !Actors.is_only_administrator?(member_id, group_id)}, - {:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)}, - Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_quit"), - leave_data <- %{ - "to" => [group_members_url], - "cc" => [group_url], - "attributedTo" => group_url, - "type" => "Leave", - "actor" => actor_url, - "object" => group_url - }, - {:ok, activity} <- create_activity(leave_data, local), - :ok <- maybe_federate(activity), - :ok <- maybe_relay_if_group_activity(activity) do - {:ok, activity, member} - end - end - - @spec remove(Member.t(), Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()} - def remove( - %Member{} = member, - %Actor{type: :Group, url: group_url, members_url: group_members_url}, - %Actor{url: moderator_url} = moderator, - local, - _additional \\ %{} - ) do - with {:ok, %Member{id: member_id}} <- Actors.update_member(member, %{role: :rejected}), - %Member{} = member <- Actors.get_member(member_id), - {:ok, _} <- - Mobilizon.Service.Activity.Member.insert_activity(member, - moderator: moderator, - subject: "member_removed" - ), - :ok <- Group.send_notification_to_removed_member(member), - remove_data <- %{ - "to" => [group_members_url], - "type" => "Remove", - "actor" => moderator_url, - "object" => member.url, - "origin" => group_url - }, - {:ok, activity} <- create_activity(remove_data, local), - :ok <- maybe_federate(activity), - :ok <- maybe_relay_if_group_activity(activity) do - {:ok, activity, member} - end - end - - @spec invite(Actor.t(), Actor.t(), Actor.t(), boolean, map()) :: - {:ok, map(), Member.t()} | {:error, :member_not_found} - def invite( - %Actor{url: group_url, id: group_id, members_url: members_url} = group, - %Actor{url: actor_url, id: actor_id} = actor, - %Actor{url: target_actor_url, id: target_actor_id} = _target_actor, - local \\ true, - additional \\ %{} - ) do - Logger.debug("Handling #{actor_url} invite to #{group_url} sent to #{target_actor_url}") - - with {:is_able_to_invite, true} <- {:is_able_to_invite, is_able_to_invite?(actor, group)}, - {:ok, %Member{url: member_url} = member} <- - Actors.create_member(%{ - parent_id: group_id, - actor_id: target_actor_id, - role: :invited, - invited_by_id: actor_id, - url: Map.get(additional, :url) - }), - {:ok, _} <- - Mobilizon.Service.Activity.Member.insert_activity(member, - moderator: actor, - subject: "member_invited" - ), - invite_data <- %{ - "type" => "Invite", - "attributedTo" => group_url, - "actor" => actor_url, - "object" => group_url, - "target" => target_actor_url, - "id" => member_url - }, - {:ok, activity} <- - create_activity( - invite_data - |> Map.merge(%{"to" => [target_actor_url, members_url], "cc" => [group_url]}) - |> Map.merge(additional), - local - ), - :ok <- maybe_federate(activity), - :ok <- maybe_relay_if_group_activity(activity), - :ok <- Group.send_invite_to_user(member) do - {:ok, activity, member} - end - end - - @spec is_able_to_invite?(Actor.t(), Actor.t()) :: boolean - defp is_able_to_invite?(%Actor{domain: actor_domain, id: actor_id}, %Actor{ - domain: group_domain, - id: group_id - }) do - # If the actor comes from the same domain we trust it - if actor_domain == group_domain do - true - else - # If local group, we'll send the invite - case Actors.get_member(actor_id, group_id) do - {:ok, %Member{} = admin_member} -> - Member.is_administrator(admin_member) - - _ -> - false - end - end - end - - @spec move(:resource, Resource.t(), map, boolean, map) :: {:ok, Activity.t(), Resource.t()} - def move(type, old_entity, args, local \\ false, additional \\ %{}) do - Logger.debug("We're moving something") - Logger.debug(inspect(args)) - - with {:ok, entity, update_data} <- - (case type do - :resource -> Types.Resources.move(old_entity, args, additional) - end), - {:ok, activity} <- create_activity(update_data, local), - :ok <- maybe_federate(activity) do - {:ok, activity, entity} - else - err -> - Logger.error("Something went wrong while creating a Move activity") - Logger.debug(inspect(err)) - err - end - end - - @spec flag(map, boolean, map) :: {:ok, Activity.t(), Report.t()} - def flag(args, local \\ false, additional \\ %{}) do - with {report, report_as_data} <- Types.Reports.flag(args, local, additional), - {:ok, activity} <- create_activity(report_as_data, local), - :ok <- maybe_federate(activity) do - Enum.each(Users.list_moderators(), fn moderator -> - moderator - |> Admin.report(report) - |> Mailer.send_email_later() - end) - - {:ok, activity, report} - else - err -> - Logger.error("Something went wrong while creating an activity") - Logger.debug(inspect(err)) - err - end - end - - @spec is_create_activity?(Activity.t()) :: boolean - defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true - defp is_create_activity?(_), do: false - - @spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())} - defp convert_members_in_recipients(recipients) do - Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc -> - case Actors.get_group_by_members_url(recipient) do - # If the group is local just add external members - %Actor{domain: domain} = group when is_nil(domain) -> - {Enum.filter(recipients, fn recipient -> recipient != group.members_url end), - member_actors ++ Actors.list_external_actors_members_for_group(group)} - - # If it's remote add the remote group actor as well - %Actor{} = group -> - {Enum.filter(recipients, fn recipient -> recipient != group.members_url end), - member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]} - - _ -> - acc - end - end) - end - - @spec convert_followers_in_recipients(list(String.t())) :: {list(String.t()), list(String.t())} - defp convert_followers_in_recipients(recipients) do - Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc -> - case Actors.get_actor_by_followers_url(recipient) do - %Actor{} = group -> - {Enum.filter(recipients, fn recipient -> recipient != group.followers_url end), - follower_actors ++ Actors.list_external_followers_for_actor(group)} - - _ -> - acc - end - end) - end - - # @spec is_announce_activity?(Activity.t()) :: boolean - # defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true - # defp is_announce_activity?(_), do: false - - @doc """ - Publish an activity to all appropriated audiences inboxes - """ - # credo:disable-for-lines:47 - @spec publish(Actor.t(), Activity.t()) :: :ok - def publish(actor, %Activity{recipients: recipients} = activity) do - Logger.debug("Publishing an activity") - Logger.debug(inspect(activity, pretty: true)) - - public = Visibility.is_public?(activity) - Logger.debug("is public ? #{public}") - - if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do - Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) - - Relay.publish(activity) - end - - recipients = Enum.uniq(recipients) - - {recipients, followers} = convert_followers_in_recipients(recipients) - - {recipients, members} = convert_members_in_recipients(recipients) - - remote_inboxes = - (remote_actors(recipients) ++ followers ++ members) - |> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end) - |> Enum.uniq() - - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - json = Jason.encode!(data) - Logger.debug(fn -> "Remote inboxes are : #{inspect(remote_inboxes)}" end) - - Enum.each(remote_inboxes, fn inbox -> - Federator.enqueue(:publish_single_ap, %{ - inbox: inbox, - json: json, - actor: actor, - id: activity.data["id"] - }) - end) - end - - @doc """ - Publish an activity to a specific inbox - """ - @spec publish_one(%{inbox: String.t(), json: String.t(), actor: Actor.t(), id: String.t()}) :: - Tesla.Env.result() - def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do - Logger.info("Federating #{id} to #{inbox}") - %URI{host: host, path: path} = URI.parse(inbox) - - digest = Signature.build_digest(json) - date = Signature.generate_date_header() - - # request_target = Signature.generate_request_target("POST", path) - - signature = - Signature.sign(actor, %{ - "(request-target)": "post #{path}", - host: host, - "content-length": byte_size(json), - digest: digest, - date: date - }) - - Tesla.post( - inbox, - json, - headers: [ - {"Content-Type", "application/activity+json"}, - {"signature", signature}, - {"digest", digest}, - {"date", date} - ] - ) - end - @doc """ Return all public activities (events & comments) for an actor """ @@ -802,224 +176,4 @@ defmodule Mobilizon.Federation.ActivityPub do local: local } end - - # Get recipients for an activity or object - @spec get_recipients(map()) :: list() - defp get_recipients(data) do - Map.get(data, "to", []) ++ Map.get(data, "cc", []) - end - - @spec check_for_tombstones(map()) :: Tombstone.t() | nil - defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url) - defp check_for_tombstones(_), do: nil - - @typep accept_follow_entities :: Follower.t() - - @spec accept_follow(Follower.t(), map) :: {:ok, Follower.t(), Activity.t()} | any - defp accept_follow(%Follower{} = follower, additional) do - with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}), - follower_as_data <- Convertible.model_to_as(follower), - update_data <- - make_accept_join_data( - follower_as_data, - Map.merge(additional, %{ - "id" => "#{Endpoint.url()}/accept/follow/#{follower.id}", - "to" => [follower.actor.url], - "cc" => [], - "actor" => follower.target_actor.url - }) - ) do - {:ok, follower, update_data} - else - err -> - Logger.error("Something went wrong while creating an update activity") - Logger.debug(inspect(err)) - err - end - end - - @typep accept_join_entities :: Participant.t() | Member.t() - - @spec accept_join(Participant.t(), map) :: {:ok, Participant.t(), Activity.t()} - @spec accept_join(Member.t(), map) :: {:ok, Member.t(), Activity.t()} - defp accept_join(%Participant{} = participant, additional) do - with {:ok, %Participant{} = participant} <- - Events.update_participant(participant, %{role: :participant}), - Absinthe.Subscription.publish(Endpoint, participant.actor, - event_person_participation_changed: participant.actor.id - ), - {:ok, _} <- - Scheduler.trigger_notifications_for_participant(participant), - participant_as_data <- Convertible.model_to_as(participant), - audience <- - Audience.get_audience(participant), - accept_join_data <- - make_accept_join_data( - participant_as_data, - Map.merge(Map.merge(audience, additional), %{ - "id" => "#{Endpoint.url()}/accept/join/#{participant.id}" - }) - ) do - {:ok, participant, accept_join_data} - else - err -> - Logger.error("Something went wrong while creating an update activity") - Logger.debug(inspect(err)) - err - end - end - - defp accept_join(%Member{} = member, additional) do - with {:ok, %Member{} = member} <- - Actors.update_member(member, %{role: :member}), - {:ok, _} <- - Mobilizon.Service.Activity.Member.insert_activity(member, - subject: "member_approved" - ), - _ <- maybe_refresh_group(member), - Absinthe.Subscription.publish(Endpoint, member.actor, - group_membership_changed: [ - Actor.preferred_username_and_domain(member.parent), - member.actor.id - ] - ), - member_as_data <- Convertible.model_to_as(member), - audience <- - Audience.get_audience(member), - accept_join_data <- - make_accept_join_data( - member_as_data, - Map.merge(Map.merge(audience, additional), %{ - "id" => "#{Endpoint.url()}/accept/join/#{member.id}" - }) - ) do - {:ok, member, accept_join_data} - else - err -> - Logger.error("Something went wrong while creating an update activity") - Logger.debug(inspect(err)) - err - end - end - - @typep accept_invite_entities :: Member.t() - - @spec accept_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any - defp accept_invite( - %Member{invited_by_id: invited_by_id, actor_id: actor_id} = member, - _additional - ) do - with %Actor{} = inviter <- Actors.get_actor(invited_by_id), - %Actor{url: actor_url} <- Actors.get_actor(actor_id), - {:ok, %Member{id: member_id} = member} <- - Actors.update_member(member, %{role: :member}), - {:ok, _} <- - Mobilizon.Service.Activity.Member.insert_activity(member, - subject: "member_accepted_invitation" - ), - _ <- maybe_refresh_group(member), - accept_data <- %{ - "type" => "Accept", - "attributedTo" => member.parent.url, - "to" => [inviter.url, member.parent.members_url], - "cc" => [member.parent.url], - "actor" => actor_url, - "object" => Convertible.model_to_as(member), - "id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}" - } do - {:ok, member, accept_data} - end - end - - @spec maybe_refresh_group(Member.t()) :: :ok | nil - defp maybe_refresh_group(%Member{ - parent: %Actor{domain: parent_domain, url: parent_url}, - actor: %Actor{} = actor - }) do - unless is_nil(parent_domain), - do: Refresher.fetch_group(parent_url, actor) - end - - @spec reject_join(Participant.t(), map()) :: {:ok, Participant.t(), Activity.t()} | any() - defp reject_join(%Participant{} = participant, additional) do - with {:ok, %Participant{} = participant} <- - Events.update_participant(participant, %{role: :rejected}), - Absinthe.Subscription.publish(Endpoint, participant.actor, - event_person_participation_changed: participant.actor.id - ), - participant_as_data <- Convertible.model_to_as(participant), - audience <- - participant - |> Audience.get_audience() - |> Map.merge(additional), - reject_data <- %{ - "type" => "Reject", - "object" => participant_as_data - }, - update_data <- - reject_data - |> Map.merge(audience) - |> Map.merge(%{ - "id" => "#{Endpoint.url()}/reject/join/#{participant.id}" - }) do - {:ok, participant, update_data} - else - err -> - Logger.error("Something went wrong while creating an update activity") - Logger.debug(inspect(err)) - err - end - end - - @spec reject_follow(Follower.t(), map()) :: {:ok, Follower.t(), Activity.t()} | any() - defp reject_follow(%Follower{} = follower, additional) do - with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower), - follower_as_data <- Convertible.model_to_as(follower), - audience <- - follower.actor |> Audience.get_audience() |> Map.merge(additional), - reject_data <- %{ - "to" => [follower.actor.url], - "type" => "Reject", - "actor" => follower.target_actor.url, - "object" => follower_as_data - }, - update_data <- - audience - |> Map.merge(reject_data) - |> Map.merge(%{ - "id" => "#{Endpoint.url()}/reject/follow/#{follower.id}" - }) do - {:ok, follower, update_data} - else - err -> - Logger.error("Something went wrong while creating an update activity") - Logger.debug(inspect(err)) - err - end - end - - @spec reject_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any - defp reject_invite( - %Member{invited_by_id: invited_by_id, actor_id: actor_id} = member, - _additional - ) do - with %Actor{} = inviter <- Actors.get_actor(invited_by_id), - %Actor{url: actor_url} <- Actors.get_actor(actor_id), - {:ok, %Member{url: member_url, id: member_id} = member} <- - Actors.delete_member(member), - Mobilizon.Service.Activity.Member.insert_activity(member, - subject: "member_rejected_invitation" - ), - accept_data <- %{ - "type" => "Reject", - "actor" => actor_url, - "attributedTo" => member.parent.url, - "to" => [inviter.url, member.parent.members_url], - "cc" => [member.parent.url], - "object" => member_url, - "id" => "#{Endpoint.url()}/reject/invite/member/#{member_id}" - } do - {:ok, member, accept_data} - end - end end diff --git a/lib/federation/activity_pub/audience.ex b/lib/federation/activity_pub/audience.ex index f8832508..d412ef36 100644 --- a/lib/federation/activity_pub/audience.ex +++ b/lib/federation/activity_pub/audience.ex @@ -7,6 +7,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Posts.Post alias Mobilizon.Storage.Repo @@ -154,22 +155,20 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do {mentions, []} end - @spec maybe_add_group_members(List.t(), Actor.t()) :: List.t() + @spec maybe_add_group_members(list(String.t()), Actor.t()) :: list(String.t()) defp maybe_add_group_members(collection, %Actor{type: :Group, members_url: members_url}) do [members_url | collection] end defp maybe_add_group_members(collection, %Actor{type: _}), do: collection - @spec maybe_add_followers(List.t(), Actor.t()) :: List.t() + @spec maybe_add_followers(list(String.t()), Actor.t()) :: list(String.t()) defp maybe_add_followers(collection, %Actor{type: :Group, followers_url: followers_url}) do [followers_url | collection] end defp maybe_add_followers(collection, %Actor{type: _}), do: collection - def get_addressed_actors(mentioned_users, _), do: mentioned_users - defp add_in_reply_to(%Comment{actor: %Actor{url: url}} = _comment), do: [url] defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url] defp add_in_reply_to(_), do: [] @@ -237,29 +236,27 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do @spec extract_actors_from_mentions(list(), Actor.t(), atom()) :: {list(), list()} defp extract_actors_from_mentions(mentions, actor, visibility) do - with mentioned_actors <- Enum.map(mentions, &process_mention/1), - addressed_actors <- get_addressed_actors(mentioned_actors, nil) do - get_to_and_cc(actor, addressed_actors, visibility) - end + get_to_and_cc(actor, Enum.map(mentions, &process_mention/1), visibility) end + @spec extract_actors_from_event(Event.t()) :: %{ + String.t() => list(String.t()) + } defp extract_actors_from_event(%Event{} = event) do - with {to, cc} <- - extract_actors_from_mentions( - event.mentions, - group_or_organizer_event(event), - event.visibility - ), - {to, cc} <- - {to, - Enum.uniq( - cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url) - )} do - %{"to" => to, "cc" => cc} - else - _ -> - %{"to" => [], "cc" => []} - end + {to, cc} = + extract_actors_from_mentions( + event.mentions, + group_or_organizer_event(event), + event.visibility + ) + + {to, cc} = + {to, + Enum.uniq( + cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url) + )} + + %{"to" => to, "cc" => cc} end @spec group_or_organizer_event(Event.t()) :: Actor.t() diff --git a/lib/federation/activity_pub/federator.ex b/lib/federation/activity_pub/federator.ex index 8f811f42..ac145aff 100644 --- a/lib/federation/activity_pub/federator.ex +++ b/lib/federation/activity_pub/federator.ex @@ -19,10 +19,12 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do @max_jobs 20 + @spec init(any()) :: {:ok, any()} def init(args) do {:ok, args} end + @spec start_link(any) :: GenServer.on_start() def start_link(_) do spawn(fn -> # 1 minute @@ -39,6 +41,8 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do ) end + @spec handle(:publish | :publish_single_ap | atom(), Activity.t() | map()) :: + :ok | {:ok, Activity.t()} | Tesla.Env.result() | {:error, String.t()} def handle(:publish, activity) do Logger.debug(inspect(activity)) Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) @@ -46,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do with {:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(activity.data["actor"]) do Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end) - ActivityPub.publish(actor, activity) + ActivityPub.Publisher.publish(actor, activity) end end @@ -67,7 +71,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do end def handle(:publish_single_ap, params) do - ActivityPub.publish_one(params) + ActivityPub.Publisher.publish_one(params) end def handle(type, _) do @@ -75,6 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do {:error, "Don't know what to do with this"} end + @spec enqueue(atom(), map(), pos_integer()) :: :ok | {:ok, any()} | {:error, any()} def enqueue(type, payload, priority \\ 1) do Logger.debug("enqueue something with type #{inspect(type)}") @@ -85,6 +90,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do end end + @spec maybe_start_job(any(), any()) :: {any(), any()} def maybe_start_job(running_jobs, queue) do if :sets.size(running_jobs) < @max_jobs && queue != [] do {{type, payload}, queue} = queue_pop(queue) @@ -96,6 +102,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do end end + @spec handle_cast(any(), any()) :: {:noreply, any()} def handle_cast({:enqueue, type, payload, _priority}, state) when type in [:incoming_doc, :incoming_ap_doc] do %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state @@ -119,6 +126,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do {:noreply, state} end + @spec handle_info({:DOWN, any(), :process, any, any()}, any) :: {:noreply, map()} def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state i_running_jobs = :sets.del_element(ref, i_running_jobs) @@ -129,11 +137,13 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do {:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}} end + @spec enqueue_sorted(any(), any(), pos_integer()) :: any() def enqueue_sorted(queue, element, priority) do [%{item: element, priority: priority} | queue] |> Enum.sort_by(fn %{priority: priority} -> priority end) end + @spec queue_pop(list(any())) :: {any(), list(any())} def queue_pop([%{item: element} | queue]) do {element, queue} end diff --git a/lib/federation/activity_pub/fetcher.ex b/lib/federation/activity_pub/fetcher.ex index a64c813b..02895bcd 100644 --- a/lib/federation/activity_pub/fetcher.ex +++ b/lib/federation/activity_pub/fetcher.ex @@ -19,9 +19,8 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do @spec fetch(String.t(), Keyword.t()) :: {:ok, map()} - | {:ok, Tesla.Env.t()} - | {:error, any()} - | {:error, :invalid_url} + | {:error, + :invalid_url | :http_gone | :http_error | :http_not_found | :content_not_json} def fetch(url, options \\ []) do on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor()) date = Signature.generate_date_header() @@ -35,7 +34,7 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do if address_valid?(url) do case ActivityPubClient.get(client, url) do - {:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 -> + {:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 and is_map(data) -> {:ok, data} {:ok, %Tesla.Env{status: 410}} -> @@ -46,8 +45,12 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do Logger.debug("Resource at #{url} is 404 Gone") {:error, :http_not_found} + {:ok, %Tesla.Env{body: data}} when is_binary(data) -> + {:error, :content_not_json} + {:ok, %Tesla.Env{} = res} -> - {:error, res} + Logger.debug("Resource returned bad HTTP code inspect #{res}") + {:error, :http_error} end else {:error, :invalid_url} @@ -55,30 +58,32 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do end @spec fetch_and_create(String.t(), Keyword.t()) :: - {:ok, map(), struct()} | {:error, :invalid_url} | {:error, String.t()} | {:error, any} + {:ok, map(), struct()} | {:error, atom()} | :error def fetch_and_create(url, options \\ []) do - with {:ok, data} when is_map(data) <- fetch(url, options), - {:origin_check, true} <- {:origin_check, origin_check?(url, data)}, - params <- %{ - "type" => "Create", - "to" => data["to"], - "cc" => data["cc"], - "actor" => data["actor"] || data["attributedTo"], - "attributedTo" => data["attributedTo"] || data["actor"], - "object" => data - } do - Transmogrifier.handle_incoming(params) - else - {:origin_check, false} -> - Logger.warn("Object origin check failed") - {:error, "Object origin check failed"} + case fetch(url, options) do + {:ok, data} when is_map(data) -> + if origin_check?(url, data) do + case Transmogrifier.handle_incoming(%{ + "type" => "Create", + "to" => data["to"], + "cc" => data["cc"], + "actor" => data["actor"] || data["attributedTo"], + "attributedTo" => data["attributedTo"] || data["actor"], + "object" => data + }) do + {:ok, entity, structure} -> + {:ok, entity, structure} - # Returned content is not JSON - {:ok, data} when is_binary(data) -> - {:error, "Failed to parse content as JSON"} + {:error, error} when is_atom(error) -> + {:error, error} - {:error, :invalid_url} -> - {:error, :invalid_url} + :error -> + {:error, :transmogrifier_error} + end + else + Logger.warn("Object origin check failed") + {:error, :object_origin_check_failed} + end {:error, err} -> {:error, err} @@ -86,22 +91,23 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do end @spec fetch_and_update(String.t(), Keyword.t()) :: - {:ok, map(), struct()} | {:error, String.t()} | :error | {:error, any} + {:ok, map(), struct()} | {:error, atom()} def fetch_and_update(url, options \\ []) do - with {:ok, data} when is_map(data) <- fetch(url, options), - {:origin_check, true} <- {:origin_check, origin_check(url, data)}, - params <- %{ - "type" => "Update", - "to" => data["to"], - "cc" => data["cc"], - "actor" => data["actor"] || data["attributedTo"], - "attributedTo" => data["attributedTo"] || data["actor"], - "object" => data - } do - Transmogrifier.handle_incoming(params) - else - {:origin_check, false} -> - {:error, "Object origin check failed"} + case fetch(url, options) do + {:ok, data} when is_map(data) -> + if origin_check(url, data) do + Transmogrifier.handle_incoming(%{ + "type" => "Update", + "to" => data["to"], + "cc" => data["cc"], + "actor" => data["actor"] || data["attributedTo"], + "attributedTo" => data["attributedTo"] || data["actor"], + "object" => data + }) + else + Logger.warn("Object origin check failed") + {:error, :object_origin_check_failed} + end {:error, err} -> {:error, err} diff --git a/lib/federation/activity_pub/publisher.ex b/lib/federation/activity_pub/publisher.ex new file mode 100644 index 00000000..b2b90f50 --- /dev/null +++ b/lib/federation/activity_pub/publisher.ex @@ -0,0 +1,128 @@ +defmodule Mobilizon.Federation.ActivityPub.Publisher do + @moduledoc """ + Handle publishing activities + """ + alias Mobilizon.Actors + alias Mobilizon.Actors.Actor + alias Mobilizon.Config + alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay, Transmogrifier, Visibility} + alias Mobilizon.Federation.HTTPSignatures.Signature + require Logger + import Mobilizon.Federation.ActivityPub.Utils, only: [remote_actors: 1] + + @doc """ + Publish an activity to all appropriated audiences inboxes + """ + # credo:disable-for-lines:47 + @spec publish(Actor.t(), Activity.t()) :: :ok + def publish(actor, %Activity{recipients: recipients} = activity) do + Logger.debug("Publishing an activity") + Logger.debug(inspect(activity, pretty: true)) + + public = Visibility.is_public?(activity) + Logger.debug("is public ? #{public}") + + if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do + Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) + + Relay.publish(activity) + end + + recipients = Enum.uniq(recipients) + + {recipients, followers} = convert_followers_in_recipients(recipients) + + {recipients, members} = convert_members_in_recipients(recipients) + + remote_inboxes = + (remote_actors(recipients) ++ followers ++ members) + |> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end) + |> Enum.uniq() + + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + json = Jason.encode!(data) + Logger.debug(fn -> "Remote inboxes are : #{inspect(remote_inboxes)}" end) + + Enum.each(remote_inboxes, fn inbox -> + Federator.enqueue(:publish_single_ap, %{ + inbox: inbox, + json: json, + actor: actor, + id: activity.data["id"] + }) + end) + end + + @doc """ + Publish an activity to a specific inbox + """ + @spec publish_one(%{inbox: String.t(), json: String.t(), actor: Actor.t(), id: String.t()}) :: + Tesla.Env.result() + def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do + Logger.info("Federating #{id} to #{inbox}") + %URI{host: host, path: path} = URI.parse(inbox) + + digest = Signature.build_digest(json) + date = Signature.generate_date_header() + + # request_target = Signature.generate_request_target("POST", path) + + signature = + Signature.sign(actor, %{ + "(request-target)": "post #{path}", + host: host, + "content-length": byte_size(json), + digest: digest, + date: date + }) + + Tesla.post( + inbox, + json, + headers: [ + {"Content-Type", "application/activity+json"}, + {"signature", signature}, + {"digest", digest}, + {"date", date} + ] + ) + end + + @spec convert_followers_in_recipients(list(String.t())) :: {list(String.t()), list(String.t())} + defp convert_followers_in_recipients(recipients) do + Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc -> + case Actors.get_actor_by_followers_url(recipient) do + %Actor{} = group -> + {Enum.filter(recipients, fn recipient -> recipient != group.followers_url end), + follower_actors ++ Actors.list_external_followers_for_actor(group)} + + nil -> + acc + end + end) + end + + @spec is_create_activity?(Activity.t()) :: boolean + defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true + defp is_create_activity?(_), do: false + + @spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())} + defp convert_members_in_recipients(recipients) do + Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc -> + case Actors.get_group_by_members_url(recipient) do + # If the group is local just add external members + %Actor{domain: domain} = group when is_nil(domain) -> + {Enum.filter(recipients, fn recipient -> recipient != group.members_url end), + member_actors ++ Actors.list_external_actors_members_for_group(group)} + + # If it's remote add the remote group actor as well + %Actor{} = group -> + {Enum.filter(recipients, fn recipient -> recipient != group.members_url end), + member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]} + + _ -> + acc + end + end) + end +end diff --git a/lib/federation/activity_pub/relay.ex b/lib/federation/activity_pub/relay.ex index 9ea39354..52ab3fe6 100644 --- a/lib/federation/activity_pub/relay.ex +++ b/lib/federation/activity_pub/relay.ex @@ -11,8 +11,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, Follower} - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier} + alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Transmogrifier} alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.WebFinger alias Mobilizon.Service.Workers.Background @@ -118,7 +117,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()} | {:error, :bad_url} - | {:error, Mobilizon.Federation.ActivityPub.Actor.make_actor_errors()} + | {:error, ActivityPubActor.make_actor_errors()} | {:error, :no_internal_relay_actor} | {:error, :url_nil} def refresh(address) do @@ -145,7 +144,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do {object, object_id} <- fetch_object(object), id <- "#{object_id}/announces/#{actor_id}" do Logger.info("Publishing activity #{id} to all relays") - ActivityPub.announce(actor, object, id, true, false) + Actions.Announce.announce(actor, object, id, true, false) else e -> Logger.error("Error while getting local instance actor: #{inspect(e)}") diff --git a/lib/federation/activity_pub/transmogrifier.ex b/lib/federation/activity_pub/transmogrifier.ex index 6e330132..5b957f34 100644 --- a/lib/federation/activity_pub/transmogrifier.ex +++ b/lib/federation/activity_pub/transmogrifier.ex @@ -17,7 +17,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.{Activity, Permission, Relay, Utils} + alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Permission, Relay, Utils} alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Types.Ownable alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} @@ -50,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do local: false } - ActivityPub.flag(params, false) + Actions.Flag.flag(params, false) end end @@ -77,10 +77,10 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:ok, %Activity{} = activity, entity} <- (if is_data_for_comment_or_discussion?(object_data) do Logger.debug("Chosing to create a regular comment") - ActivityPub.create(:comment, object_data, false) + Actions.Create.create(:comment, object_data, false) else Logger.debug("Chosing to initialize or add a comment to a conversation") - ActivityPub.create(:discussion, object_data, false) + Actions.Create.create(:discussion, object_data, false) end) do {:ok, activity, entity} else @@ -110,7 +110,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do object |> Converter.Event.as_to_model_data(), {:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)}, {:ok, %Activity{} = activity, %Event{} = event} <- - ActivityPub.create(:event, object_data, false) do + Actions.Create.create(:event, object_data, false) do {:ok, activity, event} else {:existing_event, %Event{} = event} -> {:ok, nil, event} @@ -146,7 +146,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do %Actor{type: :Group} = group <- Actors.get_actor(object_data.parent_id), %Actor{} = actor <- Actors.get_actor(object_data.actor_id), {:ok, %Activity{} = activity, %Member{} = member} <- - ActivityPub.join(group, actor, false, %{ + Actions.Join.join(group, actor, false, %{ url: object_data.url, metadata: %{role: object_data.role} }) do @@ -173,7 +173,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:existing_post, nil} <- {:existing_post, Posts.get_post_by_url(object_data.url)}, {:ok, %Activity{} = activity, %Post{} = post} <- - ActivityPub.create(:post, object_data, false) do + Actions.Create.create(:post, object_data, false) do {:ok, activity, post} else {:existing_post, %Post{} = post} -> @@ -198,7 +198,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:ok, nil, comment} {:ok, entity} -> - ActivityPub.delete(entity, Relay.get_actor(), false) + Actions.Delete.delete(entity, Relay.get_actor(), false) end end @@ -207,7 +207,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do ) do with {:ok, %Actor{} = followed} <- ActivityPubActor.get_or_fetch_actor_by_url(followed, true), {:ok, %Actor{} = follower} <- ActivityPubActor.get_or_fetch_actor_by_url(follower), - {:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do + {:ok, activity, object} <- + Actions.Follow.follow(follower, followed, id, false) do {:ok, activity, object} else {:error, :person_no_follow} -> @@ -233,7 +234,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do object_data when is_map(object_data) <- object |> Converter.TodoList.as_to_model_data(), {:ok, %Activity{} = activity, %TodoList{} = todo_list} <- - ActivityPub.create(:todo_list, object_data, false, %{"actor" => actor_url}) do + Actions.Create.create(:todo_list, object_data, false, %{ + "actor" => actor_url + }) do {:ok, activity, todo_list} else {:error, :group_not_found} -> :error @@ -252,7 +255,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do object_data <- object |> Converter.Todo.as_to_model_data(), {:ok, %Activity{} = activity, %Todo{} = todo} <- - ActivityPub.create(:todo, object_data, false) do + Actions.Create.create(:todo, object_data, false) do {:ok, activity, todo} else {:existing_todo, %Todo{} = todo} -> {:ok, nil, todo} @@ -277,7 +280,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:member, true} <- {:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)}, {:ok, %Activity{} = activity, %Resource{} = resource} <- - ActivityPub.create(:resource, object_data, false) do + Actions.Create.create(:resource, object_data, false) do {:ok, activity, resource} else {:existing_resource, %Resource{} = resource} -> @@ -388,7 +391,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do object_data <- object |> Converter.Actor.as_to_model_data(), {:ok, %Activity{} = activity, %Actor{} = new_actor} <- - ActivityPub.update(old_actor, object_data, false) do + Actions.Update.update(old_actor, object_data, false) do {:ok, activity, new_actor} else e -> @@ -416,7 +419,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do Utils.origin_check?(actor_url, update_data) || Permission.can_update_group_object?(actor, old_event)}, {:ok, %Activity{} = activity, %Event{} = new_event} <- - ActivityPub.update(old_event, object_data, false) do + Actions.Update.update(old_event, object_data, false) do {:ok, activity, new_event} else _e -> @@ -438,7 +441,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(), object_data <- transform_object_data_for_discussion(object_data), {:ok, %Activity{} = activity, new_entity} <- - ActivityPub.update(old_entity, object_data, false) do + Actions.Update.update(old_entity, object_data, false) do {:ok, activity, new_entity} else _e -> @@ -461,7 +464,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do Utils.origin_check?(actor_url, update_data["object"]) || Permission.can_update_group_object?(actor, old_post)}, {:ok, %Activity{} = activity, %Post{} = new_post} <- - ActivityPub.update(old_post, object_data, false) do + Actions.Update.update(old_post, object_data, false) do {:ok, activity, new_post} else {:origin_check, _} -> @@ -489,7 +492,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do Utils.origin_check?(actor_url, update_data) || Permission.can_update_group_object?(actor, old_resource)}, {:ok, %Activity{} = activity, %Resource{} = new_resource} <- - ActivityPub.update(old_resource, object_data, false) do + Actions.Update.update(old_resource, object_data, false) do {:ok, activity, new_resource} else _e -> @@ -510,7 +513,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do object_data <- Converter.Member.as_to_model_data(object), {:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(), {:ok, %Activity{} = activity, new_entity} <- - ActivityPub.update(old_entity, object_data, false, %{moderator: actor}) do + Actions.Update.update(old_entity, object_data, false, %{moderator: actor}) do {:ok, activity, new_entity} else _e -> @@ -527,7 +530,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do with object_url <- Utils.get_url(object), {:ok, entity} <- ActivityPub.fetch_object_from_url(object_url) do - ActivityPub.delete(entity, Relay.get_actor(), false) + Actions.Delete.delete(entity, Relay.get_actor(), false) else {:ok, %Tombstone{} = tombstone} -> {:ok, nil, tombstone} @@ -550,7 +553,13 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor), {:ok, object} <- fetch_obj_helper_as_activity_streams(object_id), {:ok, activity, object} <- - ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do + Actions.Announce.unannounce( + actor, + object, + id, + cancelled_activity_id, + false + ) do {:ok, activity, object} else _e -> :error @@ -568,7 +577,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do with {:ok, %Actor{domain: nil} = followed} <- ActivityPubActor.get_or_fetch_actor_by_url(followed), {:ok, %Actor{} = follower} <- ActivityPubActor.get_or_fetch_actor_by_url(follower), - {:ok, activity, object} <- ActivityPub.unfollow(follower, followed, id, false) do + {:ok, activity, object} <- + Actions.Follow.unfollow(follower, followed, id, false) do {:ok, activity, object} else e -> @@ -593,7 +603,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:origin_check, Utils.origin_check_from_id?(actor_url, object_id) || Permission.can_delete_group_object?(actor, object)}, - {:ok, activity, object} <- ActivityPub.delete(object, actor, false) do + {:ok, activity, object} <- Actions.Delete.delete(object, actor, false) do {:ok, activity, object} else {:origin_check, false} -> @@ -637,7 +647,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:origin_check, Utils.origin_check?(actor_url, data) || Permission.can_update_group_object?(actor, old_resource)}, - {:ok, activity, new_resource} <- ActivityPub.move(:resource, old_resource, object_data) do + {:ok, activity, new_resource} <- + Actions.Move.move(:resource, old_resource, object_data) do {:ok, activity, new_resource} else e -> @@ -665,7 +676,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do object <- Utils.get_url(object), {:ok, object} <- ActivityPub.fetch_object_from_url(object), {:ok, activity, object} <- - ActivityPub.join(object, actor, false, %{ + Actions.Join.join(object, actor, false, %{ url: id, metadata: %{message: Map.get(data, "participationMessage")} }) do @@ -682,7 +693,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor), object <- Utils.get_url(object), {:ok, object} <- ActivityPub.fetch_object_from_url(object), - {:ok, activity, object} <- ActivityPub.leave(object, actor, false) do + {:ok, activity, object} <- Actions.Leave.leave(object, actor, false) do {:ok, activity, object} else {:only_organizer, true} -> @@ -714,7 +725,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:ok, %Actor{} = target} <- target |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url(), {:ok, activity, %Member{} = member} <- - ActivityPub.invite(object, actor, target, false, %{url: id}) do + Actions.Invite.invite(object, actor, target, false, %{url: id}) do {:ok, activity, member} end end @@ -734,7 +745,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:is_admin, Actors.get_member(moderator_id, group_id)}, {:is_member, {:ok, %Member{role: role} = member}} when role != :rejected <- {:is_member, Actors.get_member(person_id, group_id)} do - ActivityPub.remove(member, group, moderator, false) + Actions.Remove.remove(member, group, moderator, false) else {:is_admin, {:ok, %Member{}}} -> Logger.warn( @@ -786,7 +797,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:follow, get_follow(follow_object)}, {:same_actor, true} <- {:same_actor, actor.id == followed.id}, {:ok, %Activity{} = activity, %Follower{approved: true} = follow} <- - ActivityPub.accept( + Actions.Accept.accept( :follow, follow, false @@ -824,7 +835,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:follow, get_follow(follow_object)}, {:same_actor, true} <- {:same_actor, actor.id == followed.id}, {:ok, activity, _} <- - ActivityPub.reject(:follow, follow) do + Actions.Reject.reject(:follow, follow) do {:ok, activity, follow} else {:follow, _err} -> @@ -879,7 +890,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:can_accept_event_join, true} <- {:can_accept_event_join, can_manage_event?(actor_accepting, event)}, {:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <- - ActivityPub.accept( + Actions.Accept.accept( :join, participant, false @@ -911,7 +922,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do when role in [:not_approved, :rejected, :invited] and type in [:join, :invite] do # Or maybe for groups it's the group that sends the Accept activity with {:ok, %Activity{} = activity, %Member{role: :member} = member} <- - ActivityPub.accept( + Actions.Accept.accept( type, member, false @@ -929,7 +940,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:can_accept_event_reject, true} <- {:can_accept_event_reject, can_manage_event?(actor_accepting, event)}, {:ok, activity, participant} <- - ActivityPub.reject(:join, participant, false), + Actions.Reject.reject(:join, participant, false), :ok <- Participation.send_emails_to_local_user(participant) do {:ok, activity, participant} else @@ -960,7 +971,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:invite, get_member(invite_object)}, {:same_actor, true} <- {:same_actor, actor_rejecting.id == actor_id}, {:ok, activity, member} <- - ActivityPub.reject(:invite, member, false) do + Actions.Reject.reject(:invite, member, false) do {:ok, activity, member} end end @@ -1139,7 +1150,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do # Before 1.0.4 the object of a "Remove" activity was an actor's URL # instead of the member's URL. # TODO: Remove in 1.2 - @spec get_remove_object(map() | String.t()) :: {:ok, String.t() | integer()} + @spec get_remove_object(map() | String.t()) :: {:ok, integer()} defp get_remove_object(object) do case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do {:ok, %Member{actor: %Actor{id: person_id}}} -> {:ok, person_id} @@ -1162,7 +1173,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do organizer_actor_id == actor_id end - defp can_manage_event?(_actor, _event) do + defp can_manage_event?(%Actor{} = _actor, %Event{} = _event) do false end end diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex index c7b48c90..0ea7cb06 100644 --- a/lib/federation/activity_pub/types/actors.ex +++ b/lib/federation/activity_pub/types/actors.ex @@ -2,8 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do @moduledoc false alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, Follower, Member, MemberRole} - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.{Audience, Permission, Relay} + alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission, Relay} alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream.Convertible @@ -68,7 +67,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do @impl Entity @spec delete(Actor.t(), Actor.t(), boolean, map) :: - {:ok, ActivityStream.t(), Actor.t(), Actor.t()} + {:ok, ActivityStream.t(), Actor.t(), Actor.t()} | {:error, Ecto.Changeset.t()} def delete( %Actor{ followers_url: followers_url, @@ -245,7 +244,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do Mobilizon.Actors.get_default_member_role(group) == :member && role == :member -> {:accept, - ActivityPub.accept( + Actions.Accept.accept( :join, member, true, @@ -282,7 +281,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do Logger.debug("Target doesn't manually approves followers, we can accept right away") {:accept, - ActivityPub.accept( + Actions.Accept.accept( :follow, follower, true, diff --git a/lib/federation/activity_pub/types/comments.ex b/lib/federation/activity_pub/types/comments.ex index 9aabfa58..c8794b9a 100644 --- a/lib/federation/activity_pub/types/comments.ex +++ b/lib/federation/activity_pub/types/comments.ex @@ -70,7 +70,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do @impl Entity @spec delete(Comment.t(), Actor.t(), boolean, map()) :: - {:ok, ActivityStream.t(), Actor.t(), Comment.t()} | {:error, Ecto.Changeset.t()} + {:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Comment.t()} def delete( %Comment{url: url, id: comment_id}, %Actor{} = actor, @@ -208,7 +208,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do end end - @spec event_allows_commenting?(%{actor_id: String.t() | integer, event: Event.t()}) :: boolean + @spec event_allows_commenting?(%{ + required(:actor_id) => String.t() | integer, + required(:event) => Event.t() | nil, + optional(atom) => any() + }) :: boolean defp event_allows_commenting?(%{ actor_id: actor_id, event: %Event{ diff --git a/lib/federation/activity_pub/types/discussions.ex b/lib/federation/activity_pub/types/discussions.ex index dd1bfde3..ec82e093 100644 --- a/lib/federation/activity_pub/types/discussions.ex +++ b/lib/federation/activity_pub/types/discussions.ex @@ -17,94 +17,110 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do @behaviour Entity @impl Entity - @spec create(map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()} + @spec create(map(), map()) :: + {:ok, Discussion.t(), ActivityStream.t()} + | {:error, :discussion_not_found | :last_comment_not_found | Ecto.Changeset.t()} def create(%{discussion_id: discussion_id} = args, additional) when not is_nil(discussion_id) do - with args <- prepare_args(args), - %Discussion{} = discussion <- Discussions.get_discussion(discussion_id), - {:ok, %Discussion{last_comment_id: last_comment_id} = discussion} <- - Discussions.reply_to_discussion(discussion, args), - {:ok, _} <- - DiscussionActivity.insert_activity(discussion, - subject: "discussion_replied", - actor_id: Map.get(args, :creator_id, args.actor_id) - ), - %Comment{} = last_comment <- Discussions.get_comment_with_preload(last_comment_id), - :ok <- maybe_publish_graphql_subscription(discussion), - comment_as_data <- Convertible.model_to_as(last_comment), - audience <- - Audience.get_audience(discussion), - create_data <- - make_create_data(comment_as_data, Map.merge(audience, additional)) do - {:ok, discussion, create_data} + args = prepare_args(args) + + case Discussions.get_discussion(discussion_id) do + %Discussion{} = discussion -> + case Discussions.reply_to_discussion(discussion, args) do + {:ok, %Discussion{last_comment_id: last_comment_id} = discussion} -> + DiscussionActivity.insert_activity(discussion, + subject: "discussion_replied", + actor_id: Map.get(args, :creator_id, args.actor_id) + ) + + case Discussions.get_comment_with_preload(last_comment_id) do + %Comment{} = last_comment -> + maybe_publish_graphql_subscription(discussion) + comment_as_data = Convertible.model_to_as(last_comment) + audience = Audience.get_audience(discussion) + create_data = make_create_data(comment_as_data, Map.merge(audience, additional)) + {:ok, discussion, create_data} + + nil -> + {:error, :last_comment_not_found} + end + + {:error, _, %Ecto.Changeset{} = err, _} -> + {:error, err} + end + + nil -> + {:error, :discussion_not_found} end end @impl Entity - @spec create(map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()} def create(args, additional) do - with args <- prepare_args(args), - {:ok, %Discussion{} = discussion} <- - Discussions.create_discussion(args), - {:ok, _} <- - DiscussionActivity.insert_activity(discussion, subject: "discussion_created"), - discussion_as_data <- Convertible.model_to_as(discussion), - audience <- - Audience.get_audience(discussion), - create_data <- - make_create_data(discussion_as_data, Map.merge(audience, additional)) do - {:ok, discussion, create_data} + args = prepare_args(args) + + case Discussions.create_discussion(args) do + {:ok, %Discussion{} = discussion} -> + DiscussionActivity.insert_activity(discussion, subject: "discussion_created") + discussion_as_data = Convertible.model_to_as(discussion) + audience = Audience.get_audience(discussion) + create_data = make_create_data(discussion_as_data, Map.merge(audience, additional)) + {:ok, discussion, create_data} + + {:error, _, %Ecto.Changeset{} = err, _} -> + {:error, err} end end @impl Entity - @spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()} + @spec update(Discussion.t(), map(), map()) :: + {:ok, Discussion.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()} def update(%Discussion{} = old_discussion, args, additional) do - with {:ok, %Discussion{} = new_discussion} <- - Discussions.update_discussion(old_discussion, args), - {:ok, _} <- - DiscussionActivity.insert_activity(new_discussion, - subject: "discussion_renamed", - old_discussion: old_discussion - ), - {:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"), - discussion_as_data <- Convertible.model_to_as(new_discussion), - audience <- - Audience.get_audience(new_discussion), - update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do - {:ok, new_discussion, update_data} - else - err -> - Logger.error("Something went wrong while creating an update activity") - Logger.debug(inspect(err)) - err + case Discussions.update_discussion(old_discussion, args) do + {:ok, %Discussion{} = new_discussion} -> + DiscussionActivity.insert_activity(new_discussion, + subject: "discussion_renamed", + old_discussion: old_discussion + ) + + Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}") + discussion_as_data = Convertible.model_to_as(new_discussion) + audience = Audience.get_audience(new_discussion) + update_data = make_update_data(discussion_as_data, Map.merge(audience, additional)) + {:ok, new_discussion, update_data} + + {:error, %Ecto.Changeset{} = err} -> + {:error, err} end end @impl Entity @spec delete(Discussion.t(), Actor.t(), boolean, map()) :: - {:ok, ActivityStream.t(), Actor.t(), Discussion.t()} + {:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Discussion.t()} def delete( %Discussion{actor: group, url: url} = discussion, %Actor{} = actor, _local, _additionnal ) do - with {:ok, _} <- Discussions.delete_discussion(discussion), - {:ok, _} <- - DiscussionActivity.insert_activity(discussion, - subject: "discussion_deleted", - moderator: actor - ) do - # This is just fake - activity_data = %{ - "type" => "Delete", - "actor" => actor.url, - "object" => Convertible.model_to_as(discussion), - "id" => url <> "/delete", - "to" => [group.members_url] - } + case Discussions.delete_discussion(discussion) do + {:error, _, %Ecto.Changeset{} = err, _} -> + {:error, err} - {:ok, activity_data, actor, discussion} + {:ok, %{comments: {_, _}}} -> + DiscussionActivity.insert_activity(discussion, + subject: "discussion_deleted", + moderator: actor + ) + + # This is just fake + activity_data = %{ + "type" => "Delete", + "actor" => actor.url, + "object" => Convertible.model_to_as(discussion), + "id" => url <> "/delete", + "to" => [group.members_url] + } + + {:ok, activity_data, actor, discussion} end end diff --git a/lib/federation/activity_pub/types/entity.ex b/lib/federation/activity_pub/types/entity.ex index 0c4e233b..c42b4cae 100644 --- a/lib/federation/activity_pub/types/entity.ex +++ b/lib/federation/activity_pub/types/entity.ex @@ -15,7 +15,7 @@ alias Mobilizon.Federation.ActivityPub.Types.{ } alias Mobilizon.Actors.{Actor, Member} -alias Mobilizon.Events.{Event, Participant} +alias Mobilizon.Events.Event alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Posts.Post @@ -28,27 +28,15 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Entity do @moduledoc """ ActivityPub entity behaviour """ - @type t :: %{id: String.t(), url: String.t()} - - @type entities :: - Actor.t() - | Member.t() - | Event.t() - | Participant.t() - | Comment.t() - | Discussion.t() - | Post.t() - | Resource.t() - | Todo.t() - | TodoList.t() + @type t :: %{required(:id) => any(), optional(:url) => String.t(), optional(atom()) => any()} @callback create(data :: any(), additionnal :: map()) :: {:ok, t(), ActivityStream.t()} | {:error, any()} - @callback update(struct :: t(), attrs :: map(), additionnal :: map()) :: + @callback update(structure :: t(), attrs :: map(), additionnal :: map()) :: {:ok, t(), ActivityStream.t()} | {:error, any()} - @callback delete(struct :: t(), Actor.t(), local :: boolean(), map()) :: + @callback delete(structure :: t(), actor :: Actor.t(), local :: boolean(), additionnal :: map()) :: {:ok, ActivityStream.t(), Actor.t(), t()} | {:error, any()} end @@ -57,47 +45,61 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do ActivityPub entity Managable protocol. """ - @spec update(Entity.t(), map(), map()) :: - {:ok, Entity.t(), ActivityStream.t()} | {:error, any()} @doc """ Updates a `Managable` entity with the appropriate attributes and returns the updated entity and an activitystream representation for it """ + @spec update(Entity.t(), map(), map()) :: + {:ok, Entity.t(), ActivityStream.t()} | {:error, any()} def update(entity, attrs, additionnal) + @doc "Deletes an entity and returns the activitystream representation for it" @spec delete(Entity.t(), Actor.t(), boolean(), map()) :: {:ok, ActivityStream.t(), Actor.t(), Entity.t()} | {:error, any()} - @doc "Deletes an entity and returns the activitystream representation for it" def delete(entity, actor, local, additionnal) end defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do - @type group_role :: :member | :moderator | :administrator | nil - - @spec group_actor(Entity.t()) :: Actor.t() | nil @doc "Returns an eventual group for the entity" + @spec group_actor(Entity.t()) :: Actor.t() | nil def group_actor(entity) - @spec actor(Entity.t()) :: Actor.t() | nil @doc "Returns the actor for the entity" + @spec actor(Entity.t()) :: Actor.t() | nil def actor(entity) + @doc """ + Returns the list of permissions for an entity + """ @spec permissions(Entity.t()) :: Permission.t() def permissions(entity) end defimpl Managable, for: Event do + @spec update(Event.t(), map, map) :: + {:error, atom() | Ecto.Changeset.t()} | {:ok, Event.t(), ActivityStream.t()} defdelegate update(entity, attrs, additionnal), to: Events + + @spec delete(entity :: Event.t(), actor :: Actor.t(), local :: boolean(), additionnal :: map()) :: + {:ok, ActivityStream.t(), Actor.t(), Event.t()} | {:error, atom() | Ecto.Changeset.t()} defdelegate delete(entity, actor, local, additionnal), to: Events end defimpl Ownable, for: Event do + @spec group_actor(Event.t()) :: Actor.t() | nil defdelegate group_actor(entity), to: Events + @spec actor(Event.t()) :: Actor.t() | nil defdelegate actor(entity), to: Events + @spec permissions(Event.t()) :: Permission.t() defdelegate permissions(entity), to: Events end defimpl Managable, for: Comment do + @spec update(Comment.t(), map, map) :: + {:error, Ecto.Changeset.t()} | {:ok, Comment.t(), ActivityStream.t()} defdelegate update(entity, attrs, additionnal), to: Comments + + @spec delete(Comment.t(), Actor.t(), boolean, map) :: + {:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Comment.t()} defdelegate delete(entity, actor, local, additionnal), to: Comments end diff --git a/lib/federation/activity_pub/types/events.ex b/lib/federation/activity_pub/types/events.ex index 04812424..b2a22229 100644 --- a/lib/federation/activity_pub/types/events.ex +++ b/lib/federation/activity_pub/types/events.ex @@ -4,9 +4,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do alias Mobilizon.Actors.Actor alias Mobilizon.Events, as: EventsManager alias Mobilizon.Events.{Event, Participant, ParticipantRole} - alias Mobilizon.Federation.{ActivityPub, ActivityStream} - alias Mobilizon.Federation.ActivityPub.{Audience, Permission} + alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission} alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.GraphQL.API.Utils, as: APIUtils @@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do {:error, _step, %Ecto.Changeset{} = err, _} -> {:error, err} - {:error, err} -> + {:error, %Ecto.Changeset{} = err} -> {:error, err} end end @@ -89,11 +89,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do Share.delete_all_by_uri(event.url) {:ok, Map.merge(activity_data, audience), actor, event} - {:error, err} -> + {:error, %Ecto.Changeset{} = err} -> {:error, err} end - {:error, err} -> + {:error, %Ecto.Changeset{} = err} -> {:error, err} end end @@ -166,11 +166,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do @spec check_attendee_capacity?(Event.t()) :: boolean defp check_attendee_capacity?(%Event{options: options} = event) do - with maximum_attendee_capacity <- - Map.get(options, :maximum_attendee_capacity) || 0 do - maximum_attendee_capacity == 0 || - Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity - end + maximum_attendee_capacity = Map.get(options, :maximum_attendee_capacity) || 0 + + maximum_attendee_capacity == 0 || + Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity end # Set the participant to approved if the default role for new participants is :participant @@ -211,7 +210,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do Mobilizon.Events.get_default_participant_role(event) == :participant && role == :participant -> {:accept, - ActivityPub.accept( + Actions.Accept.accept( :join, participant, true, diff --git a/lib/federation/activity_pub/types/members.ex b/lib/federation/activity_pub/types/members.ex index 39c75ac8..8195fe84 100644 --- a/lib/federation/activity_pub/types/members.ex +++ b/lib/federation/activity_pub/types/members.ex @@ -2,7 +2,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do @moduledoc false alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, Member, MemberRole} - alias Mobilizon.Federation.{ActivityPub, ActivityStream} + alias Mobilizon.Federation.ActivityPub.Actions + alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Service.Activity.Member, as: MemberActivity alias Mobilizon.Web.Endpoint @@ -74,7 +75,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do _additionnal ) do Logger.debug("Deleting a member") - ActivityPub.leave(group, actor, local, %{force_member_removal: true}) + Actions.Leave.leave(group, actor, local, %{force_member_removal: true}) end @spec actor(Member.t()) :: Actor.t() | nil diff --git a/lib/federation/activity_pub/types/reports.ex b/lib/federation/activity_pub/types/reports.ex index 2d957777..ddcfd34a 100644 --- a/lib/federation/activity_pub/types/reports.ex +++ b/lib/federation/activity_pub/types/reports.ex @@ -1,46 +1,53 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do @moduledoc false - alias Mobilizon.{Actors, Discussions, Reports} + alias Mobilizon.{Actors, Discussions, Events, Reports} alias Mobilizon.Actors.Actor + alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Reports.Report alias Mobilizon.Service.Formatter.HTML require Logger - @spec flag(map(), boolean(), map()) :: {Report.t(), ActivityStream.t()} + @spec flag(map(), boolean(), map()) :: + {:ok, Report.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()} def flag(args, local \\ false, _additional \\ %{}) do - with {:build_args, args} <- {:build_args, prepare_args_for_report(args)}, - {:create_report, {:ok, %Report{} = report}} <- - {:create_report, Reports.create_report(args)}, - report_as_data <- Convertible.model_to_as(report), - cc <- if(local, do: [report.reported.url], else: []), - report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}) do - {report, report_as_data} + with {:ok, %Report{} = report} <- args |> prepare_args_for_report() |> Reports.create_report() do + report_as_data = Convertible.model_to_as(report) + cc = if(local, do: [report.reported.url], else: []) + report_as_data = Map.merge(report_as_data, %{"to" => [], "cc" => cc}) + {:ok, report, report_as_data} end end @spec prepare_args_for_report(map()) :: map() defp prepare_args_for_report(args) do - with {:reporter, %Actor{} = reporter_actor} <- - {:reporter, Actors.get_actor!(args.reporter_id)}, - {:reported, %Actor{} = reported_actor} <- - {:reported, Actors.get_actor!(args.reported_id)}, - content <- HTML.strip_tags(args.content), - event <- Discussions.get_comment(Map.get(args, :event_id)), - {:get_report_comments, comments} <- - {:get_report_comments, - Discussions.list_comments_by_actor_and_ids( - reported_actor.id, - Map.get(args, :comments_ids, []) - )} do - Map.merge(args, %{ - reporter: reporter_actor, - reported: reported_actor, - content: content, - event: event, - comments: comments - }) - end + %Actor{} = reporter_actor = Actors.get_actor!(args.reporter_id) + %Actor{} = reported_actor = Actors.get_actor!(args.reported_id) + content = HTML.strip_tags(args.content) + + event_id = Map.get(args, :event_id) + + event = + if is_nil(event_id) do + nil + else + {:ok, %Event{} = event} = Events.get_event(event_id) + event + end + + comments = + Discussions.list_comments_by_actor_and_ids( + reported_actor.id, + Map.get(args, :comments_ids, []) + ) + + Map.merge(args, %{ + reporter: reporter_actor, + reported: reported_actor, + content: content, + event: event, + comments: comments + }) end end diff --git a/lib/federation/activity_pub/types/resources.ex b/lib/federation/activity_pub/types/resources.ex index 07bfe2fa..9d5438b7 100644 --- a/lib/federation/activity_pub/types/resources.ex +++ b/lib/federation/activity_pub/types/resources.ex @@ -17,7 +17,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do @behaviour Entity @impl Entity - @spec create(map(), map()) :: {:ok, Resource.t(), ActivityStream.t()} + @spec create(map(), map()) :: + {:ok, Resource.t(), ActivityStream.t()} + | {:error, Ecto.Changeset.t() | :creator_not_found | :group_not_found} def create(%{type: type} = args, additional) do args = case type do @@ -37,17 +39,18 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do with {:ok, %Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <- Resources.create_resource(args), - {:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_created"), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - %Actor{url: creator_url} = creator <- Actors.get_actor(creator_id), - resource_as_data <- - Convertible.model_to_as(%{resource | actor: group, creator: creator}), - audience <- %{ - "to" => [group.members_url], - "cc" => [], - "actor" => creator_url, - "attributedTo" => [creator_url] - } do + {:ok, %Actor{} = group, %Actor{url: creator_url} = creator} <- + group_and_creator(group_id, creator_id) do + ResourceActivity.insert_activity(resource, subject: "resource_created") + resource_as_data = Convertible.model_to_as(%{resource | actor: group, creator: creator}) + + audience = %{ + "to" => [group.members_url], + "cc" => [], + "actor" => creator_url, + "attributedTo" => [creator_url] + } + create_data = case parent_id do nil -> @@ -60,15 +63,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do end {:ok, resource, create_data} - else - err -> - Logger.debug(inspect(err)) - err end end @impl Entity - @spec update(Resource.t(), map(), map()) :: {:ok, Resource.t(), ActivityStream.t()} + @spec update(Resource.t(), map(), map()) :: + {:ok, Resource.t(), ActivityStream.t()} + | {:error, Ecto.Changeset.t() | :creator_not_found | :group_not_found} def update( %Resource{parent_id: old_parent_id} = old_resource, %{parent_id: parent_id} = args, @@ -82,32 +83,35 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do def update(%Resource{} = old_resource, %{title: title} = _args, additional) do with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <- Resources.update_resource(old_resource, %{title: title}), - {:ok, _} <- - ResourceActivity.insert_activity(resource, - subject: "resource_renamed", - old_resource: old_resource - ), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - %Actor{url: creator_url} <- Actors.get_actor(creator_id), - resource_as_data <- - Convertible.model_to_as(%{resource | actor: group}), - audience <- %{ - "to" => [group.members_url], - "cc" => [], - "actor" => creator_url, - "attributedTo" => [creator_url] - }, - update_data <- - make_update_data(resource_as_data, Map.merge(audience, additional)) do + {:ok, %Actor{} = group, %Actor{url: creator_url}} <- + group_and_creator(group_id, creator_id) do + ResourceActivity.insert_activity(resource, + subject: "resource_renamed", + old_resource: old_resource + ) + + resource_as_data = Convertible.model_to_as(%{resource | actor: group}) + + audience = %{ + "to" => [group.members_url], + "cc" => [], + "actor" => creator_url, + "attributedTo" => [creator_url] + } + + update_data = make_update_data(resource_as_data, Map.merge(audience, additional)) {:ok, resource, update_data} - else - err -> - Logger.debug(inspect(err)) - err end end - @spec update(Resource.t(), map(), map()) :: {:ok, Resource.t(), ActivityStream.t()} + @spec move(Resource.t(), map(), map()) :: + {:ok, Resource.t(), ActivityStream.t()} + | {:error, + Ecto.Changeset.t() + | :creator_not_found + | :group_not_found + | :new_parent_not_found + | :old_parent_not_found} def move( %Resource{parent_id: old_parent_id} = old_resource, %{parent_id: _new_parent_id} = args, @@ -117,37 +121,34 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do %Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} = resource} <- Resources.update_resource(old_resource, args), - {:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_moved"), - old_parent <- Resources.get_resource(old_parent_id), - new_parent <- Resources.get_resource(new_parent_id), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - %Actor{url: creator_url} <- Actors.get_actor(creator_id), - resource_as_data <- - Convertible.model_to_as(%{resource | actor: group}), - audience <- %{ - "to" => [group.members_url], - "cc" => [], - "actor" => creator_url, - "attributedTo" => [creator_url] - }, - move_data <- - make_move_data( - resource_as_data, - old_parent, - new_parent, - Map.merge(audience, additional) - ) do + {:ok, old_parent, new_parent} <- parents(old_parent_id, new_parent_id), + {:ok, %Actor{} = group, %Actor{url: creator_url}} <- + group_and_creator(group_id, creator_id) do + ResourceActivity.insert_activity(resource, subject: "resource_moved") + resource_as_data = Convertible.model_to_as(%{resource | actor: group}) + + audience = %{ + "to" => [group.members_url], + "cc" => [], + "actor" => creator_url, + "attributedTo" => [creator_url] + } + + move_data = + make_move_data( + resource_as_data, + old_parent, + new_parent, + Map.merge(audience, additional) + ) + {:ok, resource, move_data} - else - err -> - Logger.debug(inspect(err)) - err end end @impl Entity @spec delete(Resource.t(), Actor.t(), boolean, map()) :: - {:ok, ActivityStream.t(), Actor.t(), Resource.t()} + {:ok, ActivityStream.t(), Actor.t(), Resource.t()} | {:error, Ecto.Changeset.t()} def delete( %Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource, %Actor{url: actor_url} = actor, @@ -165,10 +166,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do "to" => [members_url] } - with {:ok, _resource} <- Resources.delete_resource(resource), - {:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_deleted"), - {:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}") do - {:ok, activity_data, actor, resource} + case Resources.delete_resource(resource) do + {:ok, _resource} -> + ResourceActivity.insert_activity(resource, subject: "resource_deleted") + Cachex.del(:activity_pub, "resource_#{resource.id}") + {:ok, activity_data, actor, resource} + + {:error, %Ecto.Changeset{} = err} -> + {:error, err} end end @@ -183,4 +188,28 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do def permissions(%Resource{}) do %Permission{access: :member, create: :member, update: :member, delete: :member} end + + @spec group_and_creator(integer(), integer()) :: + {:ok, Actor.t(), Actor.t()} | {:error, :creator_not_found | :group_not_found} + defp group_and_creator(group_id, creator_id) do + case Actors.get_group_by_actor_id(group_id) do + {:ok, %Actor{} = group} -> + case Actors.get_actor(creator_id) do + %Actor{} = creator -> + {:ok, group, creator} + + nil -> + {:error, :creator_not_found} + end + + {:error, :group_not_found} -> + {:error, :group_not_found} + end + end + + @spec parents(String.t(), String.t()) :: + {:ok, Resource.t(), Resource.t()} + defp parents(old_parent_id, new_parent_id) do + {:ok, Resources.get_resource(old_parent_id), Resources.get_resource(new_parent_id)} + end end diff --git a/lib/federation/activity_pub/types/todo_lists.ex b/lib/federation/activity_pub/types/todo_lists.ex index 4ff9f1f9..fc2fa1bd 100644 --- a/lib/federation/activity_pub/types/todo_lists.ex +++ b/lib/federation/activity_pub/types/todo_lists.ex @@ -18,33 +18,32 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do | {:error, :group_not_found | Ecto.Changeset.t()} def create(args, additional) do with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}), - audience <- %{"to" => [group.members_url], "cc" => []}, - create_data <- - make_create_data(todo_list_as_data, Map.merge(audience, additional)) do + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do + todo_list_as_data = Convertible.model_to_as(%{todo_list | actor: group}) + audience = %{"to" => [group.members_url], "cc" => []} + create_data = make_create_data(todo_list_as_data, Map.merge(audience, additional)) {:ok, todo_list, create_data} end end @impl Entity - @spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), ActivityStream.t()} | any + @spec update(TodoList.t(), map, map) :: + {:ok, TodoList.t(), ActivityStream.t()} + | {:error, Ecto.Changeset.t() | :group_not_found} def update(%TodoList{} = old_todo_list, args, additional) do with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.update_todo_list(old_todo_list, args), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - todo_list_as_data <- - Convertible.model_to_as(%{todo_list | actor: group}), - audience <- %{"to" => [group.members_url], "cc" => []}, - update_data <- - make_update_data(todo_list_as_data, Map.merge(audience, additional)) do + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do + todo_list_as_data = Convertible.model_to_as(%{todo_list | actor: group}) + audience = %{"to" => [group.members_url], "cc" => []} + update_data = make_update_data(todo_list_as_data, Map.merge(audience, additional)) {:ok, todo_list, update_data} end end @impl Entity @spec delete(TodoList.t(), Actor.t(), boolean(), map()) :: - {:ok, ActivityStream.t(), Actor.t(), TodoList.t()} + {:ok, ActivityStream.t(), Actor.t(), TodoList.t()} | {:error, Ecto.Changeset.t()} def delete( %TodoList{url: url, actor: %Actor{url: group_url}} = todo_list, %Actor{url: actor_url} = actor, @@ -61,9 +60,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do "to" => [group_url] } - with {:ok, _todo_list} <- Todos.delete_todo_list(todo_list), - {:ok, true} <- Cachex.del(:activity_pub, "todo_list_#{todo_list.id}") do - {:ok, activity_data, actor, todo_list} + case Todos.delete_todo_list(todo_list) do + {:ok, _todo_list} -> + Cachex.del(:activity_pub, "todo_list_#{todo_list.id}") + {:ok, activity_data, actor, todo_list} + + {:error, %Ecto.Changeset{} = err} -> + {:error, err} end end diff --git a/lib/federation/activity_pub/types/todos.ex b/lib/federation/activity_pub/types/todos.ex index 3a3e0948..771b6486 100644 --- a/lib/federation/activity_pub/types/todos.ex +++ b/lib/federation/activity_pub/types/todos.ex @@ -1,5 +1,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do - @moduledoc false + @moduledoc """ + ActivityPub type handler for Todos + """ alias Mobilizon.{Actors, Todos} alias Mobilizon.Actors.Actor alias Mobilizon.Federation.ActivityPub.Permission @@ -13,41 +15,75 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do @behaviour Entity @impl Entity - @spec create(map(), map()) :: {:ok, Todo.t(), ActivityStream.t()} + @spec create(map(), map()) :: + {:ok, Todo.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t() | atom()} def create(args, additional) do with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <- Todos.create_todo(args), - %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id), - %Actor{} = creator <- Actors.get_actor(creator_id), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator}, - todo_as_data <- - Convertible.model_to_as(todo), - audience <- %{"to" => [group.members_url], "cc" => []}, - create_data <- - make_create_data(todo_as_data, Map.merge(audience, additional)) do + {:ok, %Actor{} = creator, %TodoList{} = todo_list, %Actor{} = group} <- + creator_todo_list_and_group(creator_id, todo_list_id) do + todo = %{todo | todo_list: %{todo_list | actor: group}, creator: creator} + todo_as_data = Convertible.model_to_as(todo) + audience = %{"to" => [group.members_url], "cc" => []} + create_data = make_create_data(todo_as_data, Map.merge(audience, additional)) {:ok, todo, create_data} end end @impl Entity - @spec update(Todo.t(), map, map) :: {:ok, Todo.t(), ActivityStream.t()} + @spec update(Todo.t(), map, map) :: + {:ok, Todo.t(), ActivityStream.t()} + | {:error, atom() | Ecto.Changeset.t()} def update(%Todo{} = old_todo, args, additional) do with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args), - %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - todo_as_data <- - Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}), - audience <- %{"to" => [group.members_url], "cc" => []}, - update_data <- - make_update_data(todo_as_data, Map.merge(audience, additional)) do + {:ok, %TodoList{} = todo_list, %Actor{} = group} <- todo_list_and_group(todo_list_id) do + todo_as_data = Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}) + audience = %{"to" => [group.members_url], "cc" => []} + update_data = make_update_data(todo_as_data, Map.merge(audience, additional)) {:ok, todo, update_data} end end + @spec creator_todo_list_and_group(integer(), String.t()) :: + {:ok, Actor.t(), TodoList.t(), Actor.t()} + | {:error, :creator_not_found | :group_not_found | :todo_list_not_found} + defp creator_todo_list_and_group(creator_id, todo_list_id) do + case Actors.get_actor(creator_id) do + %Actor{} = creator -> + case todo_list_and_group(todo_list_id) do + {:ok, %TodoList{} = todo_list, %Actor{} = group} -> + {:ok, creator, todo_list, group} + + {:error, err} -> + {:error, err} + end + + nil -> + {:error, :creator_not_found} + end + end + + @spec todo_list_and_group(String.t()) :: + {:ok, TodoList.t(), Actor.t()} | {:error, :group_not_found | :todo_list_not_found} + defp todo_list_and_group(todo_list_id) do + case Todos.get_todo_list(todo_list_id) do + %TodoList{actor_id: group_id} = todo_list -> + case Actors.get_group_by_actor_id(group_id) do + {:ok, %Actor{} = group} -> + {:ok, todo_list, group} + + {:error, :group_not_found} -> + {:error, :group_not_found} + end + + nil -> + {:error, :todo_list_not_found} + end + end + @impl Entity - @spec delete(Todo.t(), Actor.t(), boolean(), map()) :: - {:ok, ActivityStream.t(), Actor.t(), Todo.t()} + @spec delete(Todo.t(), Actor.t(), any(), any()) :: + {:ok, ActivityStream.t(), Actor.t(), Todo.t()} | {:error, Ecto.Changeset.t()} def delete( %Todo{url: url, creator: %Actor{url: group_url}} = todo, %Actor{url: actor_url} = actor, @@ -60,13 +96,17 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do "actor" => actor_url, "type" => "Delete", "object" => Convertible.model_to_as(url), - "id" => url <> "/delete", + "id" => "#{url}/delete", "to" => [group_url] } - with {:ok, _todo} <- Todos.delete_todo(todo), - {:ok, true} <- Cachex.del(:activity_pub, "todo_#{todo.id}") do - {:ok, activity_data, actor, todo} + case Todos.delete_todo(todo) do + {:ok, _todo} -> + Cachex.del(:activity_pub, "todo_#{todo.id}") + {:ok, activity_data, actor, todo} + + {:error, %Ecto.Changeset{} = err} -> + {:error, err} end end @@ -84,7 +124,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do end end - @spec permissions(TodoList.t()) :: Permission.t() + @spec permissions(Todo.t()) :: Permission.t() def permissions(%Todo{}) do %Permission{access: :member, create: :member, update: :member, delete: :member} end diff --git a/lib/federation/activity_pub/types/tombstones.ex b/lib/federation/activity_pub/types/tombstones.ex index 9535d08e..10d0a390 100644 --- a/lib/federation/activity_pub/types/tombstones.ex +++ b/lib/federation/activity_pub/types/tombstones.ex @@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do alias Mobilizon.Actors.Actor alias Mobilizon.Federation.ActivityPub.Permission + @spec actor(Tombstone.t()) :: Actor.t() | nil def actor(%Tombstone{actor: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id) def actor(%Tombstone{actor_id: actor_id}) when not is_nil(actor_id), @@ -11,8 +12,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do def actor(_), do: nil + @spec group_actor(any()) :: nil def group_actor(_), do: nil + @spec permissions(any()) :: Permission.t() def permissions(_) do %Permission{access: nil, create: nil, update: nil, delete: nil} end diff --git a/lib/federation/activity_pub/utils.ex b/lib/federation/activity_pub/utils.ex index 528fa14b..08879e01 100644 --- a/lib/federation/activity_pub/utils.ex +++ b/lib/federation/activity_pub/utils.ex @@ -12,8 +12,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do alias Mobilizon.Actors.Actor alias Mobilizon.Medias.Media - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay} + alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Federator, Relay} alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityStream.Converter alias Mobilizon.Federation.HTTPSignatures @@ -23,6 +22,26 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do @actor_types ["Group", "Person", "Application"] + # Wraps an object into an activity + @spec create_activity(map(), boolean()) :: {:ok, Activity.t()} + def create_activity(map, local) when is_map(map) do + with map <- lazy_put_activity_defaults(map) do + {:ok, + %Activity{ + data: map, + local: local, + actor: map["actor"], + recipients: get_recipients(map) + }} + end + end + + # Get recipients for an activity or object + @spec get_recipients(map()) :: list() + defp get_recipients(data) do + Map.get(data, "to", []) ++ Map.get(data, "cc", []) + end + # Some implementations send the actor URI as the actor field, others send the entire actor object, # so figure out what the actor's URI is based on what we have. @spec get_url(map() | String.t() | list(String.t()) | any()) :: String.t() | nil @@ -149,7 +168,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do %Activity{data: %{"object" => object}}, %Actor{url: attributed_to_url} ) - when is_binary(object) do + when is_binary(object) and is_binary(attributed_to_url) do do_maybe_relay_if_group_activity(object, attributed_to_url) end @@ -166,7 +185,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do case Actors.get_local_group_by_url(attributed_to) do %Actor{} = group -> - case ActivityPub.announce(group, object, id, true, false) do + case Actions.Announce.announce(group, object, id, true, false) do {:ok, _activity, _object} -> Logger.info("Forwarded activity to external members of the group") :ok @@ -564,6 +583,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do @doc """ Converts PEM encoded keys to a public key representation """ + @spec pem_to_public_key(String.t()) :: {:RSAPublicKey, any(), any()} def pem_to_public_key(pem) do [key_code] = :public_key.pem_decode(pem) key = :public_key.pem_entry_decode(key_code) @@ -577,6 +597,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do end end + @spec pem_to_public_key_pem(String.t()) :: String.t() def pem_to_public_key_pem(pem) do public_key = pem_to_public_key(pem) public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key) diff --git a/lib/federation/activity_stream.ex b/lib/federation/activity_stream.ex index f6d7a813..ee6b1212 100644 --- a/lib/federation/activity_stream.ex +++ b/lib/federation/activity_stream.ex @@ -3,5 +3,5 @@ defmodule Mobilizon.Federation.ActivityStream do The ActivityStream Type """ - @type t :: map() + @type t :: %{String.t() => String.t() | list(String.t()) | map() | nil} end diff --git a/lib/federation/activity_stream/converter/discussion.ex b/lib/federation/activity_stream/converter/discussion.ex index 9cfe4889..3d611d1d 100644 --- a/lib/federation/activity_stream/converter/discussion.ex +++ b/lib/federation/activity_stream/converter/discussion.ex @@ -46,7 +46,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do end @impl Converter - @spec as_to_model_data(map) :: map() | {:error, any()} + @spec as_to_model_data(map) :: map() | {:error, atom()} def as_to_model_data(%{"type" => "Note", "name" => name} = object) when is_valid_string(name) do case extract_actors(object) do %{actor_id: actor_id, creator_id: creator_id} -> @@ -57,7 +57,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do end end - @spec extract_actors(map()) :: %{actor_id: String.t(), creator_id: String.t()} | {:error, any()} + @spec extract_actors(map()) :: + %{actor_id: String.t(), creator_id: String.t()} | {:error, atom()} defp extract_actors(%{"actor" => creator_url, "attributedTo" => actor_url} = _object) when is_valid_string(creator_url) and is_valid_string(actor_url) do with {:ok, %Actor{id: creator_id, suspended: false}} <- diff --git a/lib/federation/activity_stream/converter/event.ex b/lib/federation/activity_stream/converter/event.ex index 79b58551..d9193efe 100644 --- a/lib/federation/activity_stream/converter/event.ex +++ b/lib/federation/activity_stream/converter/event.ex @@ -45,50 +45,51 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do Converts an AP object data to our internal data structure. """ @impl Converter - @spec as_to_model_data(map) :: map() | {:error, any()} | :error + @spec as_to_model_data(map) :: map() | {:error, atom()} def as_to_model_data(object) do - with {:ok, %Actor{id: actor_id}, attributed_to} <- - maybe_fetch_actor_and_attributed_to_id(object), - {:address, address_id} <- - {:address, get_address(object["location"])}, - {:tags, tags} <- {:tags, fetch_tags(object["tag"])}, - {: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 - %{ - title: object["name"], - description: description, - organizer_actor_id: actor_id, - attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id), - picture_id: picture_id, - medias: medias, - begins_on: object["startTime"], - ends_on: object["endTime"], - category: object["category"], - visibility: visibility, - 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"], - draft: object["draft"] == true, - url: object["id"], - uuid: object["uuid"], - tags: tags, - mentions: mentions, - physical_address_id: address_id, - updated_at: object["updated"], - publish_at: object["published"], - language: object["inLanguage"] - } - else - {:error, _err} -> - :error + case maybe_fetch_actor_and_attributed_to_id(object) do + {:ok, %Actor{id: actor_id}, attributed_to} -> + address_id = get_address(object["location"]) + tags = fetch_tags(object["tag"]) + mentions = fetch_mentions(object["tag"]) + visibility = get_visibility(object) + options = get_options(object) + metadata = get_metdata(object) + + [description: description, picture_id: picture_id, medias: medias] = + process_pictures(object, actor_id) + + %{ + title: object["name"], + description: description, + organizer_actor_id: actor_id, + attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id), + picture_id: picture_id, + medias: medias, + begins_on: object["startTime"], + ends_on: object["endTime"], + category: object["category"], + visibility: visibility, + 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"], + draft: object["draft"] == true, + url: object["id"], + uuid: object["uuid"], + tags: tags, + mentions: mentions, + physical_address_id: address_id, + updated_at: object["updated"], + publish_at: object["published"], + language: object["inLanguage"] + } + + {:error, err} -> + {:error, err} end end diff --git a/lib/federation/activity_stream/converter/event_metadata.ex b/lib/federation/activity_stream/converter/event_metadata.ex index eca14d16..eeee081b 100644 --- a/lib/federation/activity_stream/converter/event_metadata.ex +++ b/lib/federation/activity_stream/converter/event_metadata.ex @@ -4,9 +4,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do """ alias Mobilizon.Events.EventMetadata + alias Mobilizon.Federation.ActivityStream @property_value "PropertyValue" + @spec metadata_to_as(EventMetadata.t()) :: map() def metadata_to_as(%EventMetadata{type: :boolean, value: value, key: key}) when value in ["true", "false"] do %{ @@ -47,6 +49,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do ) end + @spec as_to_metadata(ActivityStream.t()) :: map() def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value}) when is_boolean(value) do %{type: :boolean, key: key, value: to_string(value)} diff --git a/lib/federation/activity_stream/converter/todo.ex b/lib/federation/activity_stream/converter/todo.ex index 09622a43..3d470f6f 100644 --- a/lib/federation/activity_stream/converter/todo.ex +++ b/lib/federation/activity_stream/converter/todo.ex @@ -66,6 +66,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do nil -> case ActivityPub.fetch_object_from_url(todo_list_url) do + {:ok, _, %TodoList{}} -> + as_to_model_data(object) + {:ok, %TodoList{}} -> as_to_model_data(object) diff --git a/lib/graphql/api/comments.ex b/lib/graphql/api/comments.ex index 9b7b659b..f8dd17b5 100644 --- a/lib/graphql/api/comments.ex +++ b/lib/graphql/api/comments.ex @@ -5,8 +5,7 @@ defmodule Mobilizon.GraphQL.API.Comments do alias Mobilizon.Actors.Actor alias Mobilizon.Discussions.Comment - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.Activity + alias Mobilizon.Federation.ActivityPub.{Actions, Activity} alias Mobilizon.GraphQL.API.Utils @doc """ @@ -15,7 +14,7 @@ defmodule Mobilizon.GraphQL.API.Comments do @spec create_comment(map) :: {:ok, Activity.t(), Comment.t()} | any def create_comment(args) do args = extract_pictures_from_comment_body(args) - ActivityPub.create(:comment, args, true) + Actions.Create.create(:comment, args, true) end @doc """ @@ -24,7 +23,7 @@ defmodule Mobilizon.GraphQL.API.Comments do @spec update_comment(Comment.t(), map()) :: {:ok, Activity.t(), Comment.t()} | any def update_comment(%Comment{} = comment, args) do args = extract_pictures_from_comment_body(args) - ActivityPub.update(comment, args, true) + Actions.Update.update(comment, args, true) end @doc """ @@ -32,7 +31,7 @@ defmodule Mobilizon.GraphQL.API.Comments do """ @spec delete_comment(Comment.t(), Actor.t()) :: {:ok, Activity.t(), Comment.t()} | any def delete_comment(%Comment{} = comment, %Actor{} = actor) do - ActivityPub.delete(comment, actor, true) + Actions.Delete.delete(comment, actor, true) end @doc """ @@ -42,7 +41,7 @@ defmodule Mobilizon.GraphQL.API.Comments do def create_discussion(args) do args = extract_pictures_from_comment_body(args) - ActivityPub.create( + Actions.Create.create( :discussion, args, true diff --git a/lib/graphql/api/events.ex b/lib/graphql/api/events.ex index 6de8f12d..d71a37f4 100644 --- a/lib/graphql/api/events.ex +++ b/lib/graphql/api/events.ex @@ -6,8 +6,7 @@ defmodule Mobilizon.GraphQL.API.Events do alias Mobilizon.Actors.Actor alias Mobilizon.Events.Event - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.{Activity, Utils} + alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Utils} alias Mobilizon.GraphQL.API.Utils, as: APIUtils @doc """ @@ -16,7 +15,7 @@ defmodule Mobilizon.GraphQL.API.Events do @spec create_event(map) :: {:ok, Activity.t(), Event.t()} | any def create_event(args) do # For now we don't federate drafts but it will be needed if we want to edit them as groups - ActivityPub.create(:event, prepare_args(args), should_federate(args)) + Actions.Create.create(:event, prepare_args(args), should_federate(args)) end @doc """ @@ -24,7 +23,7 @@ defmodule Mobilizon.GraphQL.API.Events do """ @spec update_event(map, Event.t()) :: {:ok, Activity.t(), Event.t()} | any def update_event(args, %Event{} = event) do - ActivityPub.update(event, prepare_args(args), should_federate(args)) + Actions.Update.update(event, prepare_args(args), should_federate(args)) end @doc """ @@ -32,7 +31,7 @@ defmodule Mobilizon.GraphQL.API.Events do """ @spec delete_event(Event.t(), Actor.t(), boolean()) :: {:ok, Activity.t(), Entity.t()} | any() def delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) do - ActivityPub.delete(event, actor, federate) + Actions.Delete.delete(event, actor, federate) end @spec prepare_args(map) :: map diff --git a/lib/graphql/api/follows.ex b/lib/graphql/api/follows.ex index 48925ead..0f75f7e8 100644 --- a/lib/graphql/api/follows.ex +++ b/lib/graphql/api/follows.ex @@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.API.Follows do alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, Follower} - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.{Actions, Activity} require Logger @@ -14,27 +14,27 @@ defmodule Mobilizon.GraphQL.API.Follows do Make an actor (`follower`) follow another (`followed`). """ @spec follow(follower :: Actor.t(), followed :: Actor.t()) :: - {:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()} + {:ok, Activity.t(), Mobilizon.Actors.Follower.t()} | {:error, String.t()} def follow(%Actor{} = follower, %Actor{} = followed) do - ActivityPub.follow(follower, followed) + Actions.Follow.follow(follower, followed) end @doc """ Make an actor (`follower`) unfollow another (`followed`). """ @spec unfollow(follower :: Actor.t(), followed :: Actor.t()) :: - {:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()} + {:ok, Activity.t(), Mobilizon.Actors.Follower.t()} | {:error, String.t()} def unfollow(%Actor{} = follower, %Actor{} = followed) do - ActivityPub.unfollow(follower, followed) + Actions.Follow.unfollow(follower, followed) end @doc """ Make an actor (`followed`) accept the follow from another (`follower`). """ @spec accept(follower :: Actor.t(), followed :: Actor.t()) :: - {:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()} + {:ok, Activity.t(), Mobilizon.Actors.Follower.t()} | {:error, String.t()} def accept(%Actor{url: follower_url} = follower, %Actor{url: followed_url} = followed) do Logger.debug( @@ -43,7 +43,7 @@ defmodule Mobilizon.GraphQL.API.Follows do case Actors.is_following(follower, followed) do %Follower{approved: false} = follow -> - ActivityPub.accept( + Actions.Accept.accept( :follow, follow, true @@ -61,7 +61,7 @@ defmodule Mobilizon.GraphQL.API.Follows do Make an actor (`followed`) reject the follow from another (`follower`). """ @spec reject(follower :: Actor.t(), followed :: Actor.t()) :: - {:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()} + {:ok, Activity.t(), Mobilizon.Actors.Follower.t()} | {:error, String.t()} def reject(%Actor{url: follower_url} = follower, %Actor{url: followed_url} = followed) do Logger.debug( @@ -73,7 +73,7 @@ defmodule Mobilizon.GraphQL.API.Follows do {:error, "Follow already accepted"} %Follower{} = follow -> - ActivityPub.reject( + Actions.Reject.reject( :follow, follow, true diff --git a/lib/graphql/api/groups.ex b/lib/graphql/api/groups.ex index c8c9a165..28919d5d 100644 --- a/lib/graphql/api/groups.ex +++ b/lib/graphql/api/groups.ex @@ -6,39 +6,35 @@ defmodule Mobilizon.GraphQL.API.Groups do alias Mobilizon.Actors alias Mobilizon.Actors.Actor - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.Activity + alias Mobilizon.Federation.ActivityPub.{Actions, Activity} alias Mobilizon.Service.Formatter.HTML @doc """ Create a group """ - @spec create_group(map) :: {:ok, Activity.t(), Actor.t()} | any + @spec create_group(map) :: + {:ok, Activity.t(), Actor.t()} + | {:error, String.t() | Ecto.Changeset.t()} def create_group(args) do - with preferred_username <- - args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(), - {:existing_group, nil} <- - {:existing_group, Actors.get_local_actor_by_name(preferred_username)}, - args <- args |> Map.put(:type, :Group), - {:ok, %Activity{} = activity, %Actor{} = group} <- - ActivityPub.create(:actor, args, true, %{"actor" => args.creator_actor.url}) do - {:ok, activity, group} - else - {:existing_group, _} -> - {:error, "A group with this name already exists"} + preferred_username = + args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim() + + args = args |> Map.put(:type, :Group) + + case Actors.get_local_actor_by_name(preferred_username) do + nil -> + Actions.Create.create(:actor, args, true, %{"actor" => args.creator_actor.url}) + + %Actor{} -> + {:error, "A profile or group with that name already exists"} end end - @spec create_group(map) :: {:ok, Activity.t(), Actor.t()} | any + @spec update_group(map) :: + {:ok, Activity.t(), Actor.t()} | {:error, :group_not_found | Ecto.Changeset.t()} def update_group(%{id: id} = args) do - with {:existing_group, {:ok, %Actor{type: :Group} = group}} <- - {:existing_group, Actors.get_group_by_actor_id(id)}, - {:ok, %Activity{} = activity, %Actor{} = group} <- - ActivityPub.update(group, args, true, %{"actor" => args.updater_actor.url}) do - {:ok, activity, group} - else - {:existing_group, _} -> - {:error, "A group with this name already exists"} + with {:ok, %Actor{type: :Group} = group} <- Actors.get_group_by_actor_id(id) do + Actions.Update.update(group, args, true, %{"actor" => args.updater_actor.url}) end end end diff --git a/lib/graphql/api/participations.ex b/lib/graphql/api/participations.ex index ae1f7237..5b2325d5 100644 --- a/lib/graphql/api/participations.ex +++ b/lib/graphql/api/participations.ex @@ -6,8 +6,7 @@ defmodule Mobilizon.GraphQL.API.Participations do alias Mobilizon.Actors.Actor alias Mobilizon.Events alias Mobilizon.Events.{Event, Participant} - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.Activity + alias Mobilizon.Federation.ActivityPub.{Actions, Activity} alias Mobilizon.Service.Notifications.Scheduler alias Mobilizon.Web.Email.Participation @@ -19,7 +18,7 @@ defmodule Mobilizon.GraphQL.API.Participations do {:error, :already_participant} {:error, :participant_not_found} -> - ActivityPub.join(event, actor, Map.get(args, :local, true), %{metadata: args}) + Actions.Join.join(event, actor, Map.get(args, :local, true), %{metadata: args}) end end @@ -27,7 +26,7 @@ defmodule Mobilizon.GraphQL.API.Participations do {:ok, Activity.t(), Participant.t()} | {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()} def leave(%Event{} = event, %Actor{} = actor, args \\ %{}), - do: ActivityPub.leave(event, actor, Map.get(args, :local, true), %{metadata: args}) + do: Actions.Leave.leave(event, actor, Map.get(args, :local, true), %{metadata: args}) @doc """ Update participation status @@ -52,15 +51,18 @@ defmodule Mobilizon.GraphQL.API.Participations do %Participant{} = participation, %Actor{} = moderator ) do - with {:ok, activity, %Participant{role: :participant} = participation} <- - ActivityPub.accept( - :join, - participation, - true, - %{"actor" => moderator.url} - ), - :ok <- Participation.send_emails_to_local_user(participation) do - {:ok, activity, participation} + case Actions.Accept.accept( + :join, + participation, + true, + %{"actor" => moderator.url} + ) do + {:ok, activity, %Participant{role: :participant} = participation} -> + Participation.send_emails_to_local_user(participation) + {:ok, activity, participation} + + {:error, err} -> + {:error, err} end end @@ -70,7 +72,7 @@ defmodule Mobilizon.GraphQL.API.Participations do %Actor{} = moderator ) do with {:ok, activity, %Participant{role: :rejected} = participation} <- - ActivityPub.reject( + Actions.Reject.reject( :join, participation, true, diff --git a/lib/graphql/api/reports.ex b/lib/graphql/api/reports.ex index 218467eb..c418594b 100644 --- a/lib/graphql/api/reports.ex +++ b/lib/graphql/api/reports.ex @@ -9,36 +9,29 @@ defmodule Mobilizon.GraphQL.API.Reports do alias Mobilizon.Reports.{Note, Report, ReportStatus} alias Mobilizon.Users.User - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.Activity + alias Mobilizon.Federation.ActivityPub.{Actions, Activity} @doc """ Create a report/flag on an actor, and optionally on an event or on comments. """ - @spec report(map()) :: {:ok, Activity.t(), Report.t()} | {:error, any()} + @spec report(map()) :: {:ok, Activity.t(), Report.t()} | {:error, Ecto.Changeset.t()} def report(args) do - case ActivityPub.flag(args, Map.get(args, :forward, false) == true) do - {:ok, %Activity{} = activity, %Report{} = report} -> - {:ok, activity, report} - - err -> - {:error, err} - end + Actions.Flag.flag(args, Map.get(args, :forward, false) == true) end @doc """ Update the state of a report """ @spec update_report_status(Actor.t(), Report.t(), ReportStatus.t()) :: - {:ok, Report.t()} | {:error, String.t()} + {:ok, Report.t()} | {:error, Ecto.Changeset.t() | String.t()} def update_report_status(%Actor{} = actor, %Report{} = report, state) do - with {:valid_state, true} <- - {:valid_state, ReportStatus.valid_value?(state)}, - {:ok, %Report{} = report} <- ReportsAction.update_report(report, %{"status" => state}), - {:ok, _} <- Admin.log_action(actor, "update", report) do - {:ok, report} + if ReportStatus.valid_value?(state) do + with {:ok, %Report{} = report} <- ReportsAction.update_report(report, %{"status" => state}) do + Admin.log_action(actor, "update", report) + {:ok, report} + end else - {:valid_state, false} -> {:error, "Unsupported state"} + {:error, "Unsupported state"} end end @@ -46,49 +39,52 @@ defmodule Mobilizon.GraphQL.API.Reports do Create a note on a report """ @spec create_report_note(Report.t(), Actor.t(), String.t()) :: - {:ok, Note.t()} | {:error, String.t()} + {:ok, Note.t()} | {:error, String.t() | Ecto.Changeset.t()} def create_report_note( %Report{id: report_id}, %Actor{id: moderator_id, user_id: user_id} = moderator, content ) do - with %User{role: role} <- Users.get_user!(user_id), - {:role, true} <- {:role, role in [:administrator, :moderator]}, - {:ok, %Note{} = note} <- - Mobilizon.Reports.create_note(%{ - "report_id" => report_id, - "moderator_id" => moderator_id, - "content" => content - }), - {:ok, _} <- Admin.log_action(moderator, "create", note) do - {:ok, note} + %User{role: role} = Users.get_user!(user_id) + + if role in [:administrator, :moderator] do + with {:ok, %Note{} = note} <- + Mobilizon.Reports.create_note(%{ + "report_id" => report_id, + "moderator_id" => moderator_id, + "content" => content + }), + {:ok, _} <- Admin.log_action(moderator, "create", note) do + {:ok, note} + end else - {:role, false} -> - {:error, "You need to be a moderator or an administrator to create a note on a report"} + {:error, "You need to be a moderator or an administrator to create a note on a report"} end end @doc """ Delete a report note """ - @spec delete_report_note(Note.t(), Actor.t()) :: {:ok, Note.t()} | {:error, String.t()} + @spec delete_report_note(Note.t(), Actor.t()) :: + {:ok, Note.t()} | {:error, Ecto.Changeset.t() | String.t()} def delete_report_note( %Note{moderator_id: note_moderator_id} = note, %Actor{id: moderator_id, user_id: user_id} = moderator ) do - with {:same_actor, true} <- {:same_actor, note_moderator_id == moderator_id}, - %User{role: role} <- Users.get_user!(user_id), - {:role, true} <- {:role, role in [:administrator, :moderator]}, - {:ok, %Note{} = note} <- - Mobilizon.Reports.delete_note(note), - {:ok, _} <- Admin.log_action(moderator, "delete", note) do - {:ok, note} - else - {:role, false} -> - {:error, "You need to be a moderator or an administrator to create a note on a report"} + if note_moderator_id == moderator_id do + %User{role: role} = Users.get_user!(user_id) - {:same_actor, false} -> - {:error, "You can only remove your own notes"} + if role in [:administrator, :moderator] do + with {:ok, %Note{} = note} <- + Mobilizon.Reports.delete_note(note), + {:ok, _} <- Admin.log_action(moderator, "delete", note) do + {:ok, note} + end + else + {:error, "You need to be a moderator or an administrator to create a note on a report"} + end + else + {:error, "You can only remove your own notes"} end end end diff --git a/lib/graphql/resolvers/activity.ex b/lib/graphql/resolvers/activity.ex index 6f7a4df2..56d5b7ad 100644 --- a/lib/graphql/resolvers/activity.ex +++ b/lib/graphql/resolvers/activity.ex @@ -5,6 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do import Mobilizon.Users.Guards alias Mobilizon.{Activities, Actors} + alias Mobilizon.Activities.Activity alias Mobilizon.Actors.Actor alias Mobilizon.Service.Activity.Utils alias Mobilizon.Storage.Page @@ -12,6 +13,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do require Logger + @spec group_activity(Actor.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Activity.t())} | {:error, :unauthorized | :unauthenticated} def group_activity(%Actor{type: :Group, id: group_id}, %{page: page, limit: limit} = args, %{ context: %{current_user: %User{role: role}, current_actor: %Actor{id: actor_id}} }) do diff --git a/lib/graphql/resolvers/actor.ex b/lib/graphql/resolvers/actor.ex index 7f064b5e..b8b91321 100644 --- a/lib/graphql/resolvers/actor.ex +++ b/lib/graphql/resolvers/actor.ex @@ -6,13 +6,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do import Mobilizon.Users.Guards alias Mobilizon.{Actors, Admin} alias Mobilizon.Actors.Actor - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.Service.Workers.Background alias Mobilizon.Users.User import Mobilizon.Web.Gettext, only: [dgettext: 2] require Logger + @spec refresh_profile(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t()} def refresh_profile(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) when is_admin(role) do case Actors.get_actor(id) do @@ -31,6 +33,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do end end + @spec suspend_profile(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t()} def suspend_profile(_parent, %{id: id}, %{ context: %{ current_user: %User{role: role}, @@ -39,28 +43,28 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do }) when is_moderator(role) do case Actors.get_actor_with_preload(id) do - %Actor{suspended: false} = actor -> - case actor do - # Suspend a group on this instance - %Actor{type: :Group, domain: nil} -> - Logger.debug("We're suspending a group on this very instance") - ActivityPub.delete(actor, moderator_actor, true, %{suspension: true}) - Admin.log_action(moderator_actor, "suspend", actor) - {:ok, actor} + # Suspend a group on this instance + %Actor{suspended: false, type: :Group, domain: nil} = actor -> + Logger.debug("We're suspending a group on this very instance") + Actions.Delete.delete(actor, moderator_actor, true, %{suspension: true}) + Admin.log_action(moderator_actor, "suspend", actor) + {:ok, actor} - # Delete a remote actor - %Actor{domain: domain} when not is_nil(domain) -> - Logger.debug("We're just deleting a remote instance") - Actors.delete_actor(actor, suspension: true) - Admin.log_action(moderator_actor, "suspend", actor) - {:ok, actor} + # Delete a remote actor + %Actor{suspended: false, domain: domain} = actor when not is_nil(domain) -> + Logger.debug("We're just deleting a remote instance") + Actors.delete_actor(actor, suspension: true) + Admin.log_action(moderator_actor, "suspend", actor) + {:ok, actor} - %Actor{domain: nil} -> - {:error, dgettext("errors", "No remote profile found with this ID")} - end + %Actor{suspended: false, domain: nil} -> + {:error, dgettext("errors", "No remote profile found with this ID")} %Actor{suspended: true} -> {:error, dgettext("errors", "Profile already suspended")} + + nil -> + {:error, dgettext("errors", "Profile not found")} end end @@ -68,6 +72,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do {:error, dgettext("errors", "Only moderators and administrators can suspend a profile")} end + @spec unsuspend_profile(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t()} def unsuspend_profile(_parent, %{id: id}, %{ context: %{ current_user: %User{role: role}, diff --git a/lib/graphql/resolvers/admin.ex b/lib/graphql/resolvers/admin.ex index 691230e8..efaccaad 100644 --- a/lib/graphql/resolvers/admin.ex +++ b/lib/graphql/resolvers/admin.ex @@ -6,14 +6,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do import Mobilizon.Users.Guards alias Mobilizon.{Actors, Admin, Config, Events} - alias Mobilizon.Actors.Actor + alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Admin.{ActionLog, Setting} alias Mobilizon.Cldr.Language alias Mobilizon.Config alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.Relay + alias Mobilizon.Federation.ActivityPub.{Actions, Relay} alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Service.Statistics alias Mobilizon.Storage.Page @@ -21,6 +20,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do import Mobilizon.Web.Gettext require Logger + @spec list_action_logs(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(ActionLog.t())} | {:error, String.t()} def list_action_logs( _parent, %{page: page, limit: limit}, @@ -38,10 +39,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do id: id, inserted_at: inserted_at } = action_log -> - with data when is_map(data) <- - transform_action_log(String.to_existing_atom(target_type), action, action_log) do - Map.merge(data, %{actor: actor, id: id, inserted_at: inserted_at}) - end + target_type + |> String.to_existing_atom() + |> transform_action_log(action, action_log) + |> Map.merge(%{actor: actor, id: id, inserted_at: inserted_at}) end) |> Enum.filter(& &1) @@ -53,6 +54,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do {:error, dgettext("errors", "You need to be logged-in and a moderator to list action logs")} end + @spec transform_action_log(module(), atom(), ActionLog.t()) :: map() defp transform_action_log( Report, :update, @@ -123,6 +125,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do end # Changes are stored as %{"key" => "value"} so we need to convert them back as struct + @spec convert_changes_to_struct(module(), map()) :: struct() defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do with data <- for({key, val} <- changes, into: %{}, do: {String.to_existing_atom(key), val}), data <- Map.put(data, :report, Mobilizon.Reports.get_report(data.report_id)) do @@ -143,6 +146,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do end # datetimes are not unserialized as DateTime/NaiveDateTime so we do it manually with changeset data + @spec process_eventual_type(Ecto.Changeset.t(), String.t(), String.t() | nil) :: + DateTime.t() | NaiveDateTime.t() | any() defp process_eventual_type(changeset, key, val) do cond do changeset[String.to_existing_atom(key)] == :utc_datetime and not is_nil(val) -> @@ -158,6 +163,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do end end + @spec get_list_of_languages(any(), any(), any()) :: {:ok, String.t()} | {:error, any()} def get_list_of_languages(_parent, %{codes: codes}, _resolution) when is_list(codes) do locale = Gettext.get_locale() locale = if Cldr.known_locale_name?(locale), do: locale, else: "en" @@ -187,6 +193,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do end end + @spec get_dashboard(any(), any(), Absinthe.Resolution.t()) :: + {:ok, map()} | {:error, String.t()} def get_dashboard(_parent, _args, %{context: %{current_user: %User{role: role}}}) when is_admin(role) do last_public_event_published = @@ -225,6 +233,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do )} end + @spec get_settings(any(), any(), Absinthe.Resolution.t()) :: {:ok, map()} | {:error, String.t()} def get_settings(_parent, _args, %{ context: %{current_user: %User{role: role}} }) @@ -237,6 +246,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do dgettext("errors", "You need to be logged-in and an administrator to access admin settings")} end + @spec save_settings(any(), map(), Absinthe.Resolution.t()) :: + {:ok, map()} | {:error, String.t()} def save_settings(_parent, args, %{ context: %{current_user: %User{role: role}} }) @@ -261,6 +272,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do dgettext("errors", "You need to be logged-in and an administrator to save admin settings")} end + @spec list_relay_followers(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Follower.t())} | {:error, :unauthorized | :unauthenticated} def list_relay_followers( _parent, %{page: page, limit: limit}, @@ -283,6 +296,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do {:error, :unauthenticated} end + @spec list_relay_followings(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Follower.t())} | {:error, :unauthorized | :unauthenticated} def list_relay_followings( _parent, %{page: page, limit: limit}, @@ -305,6 +320,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do {:error, :unauthenticated} end + @spec create_relay(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Follower.t()} | {:error, any()} def create_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}}) when is_admin(role) do case Relay.follow(address) do @@ -316,6 +333,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do end end + @spec remove_relay(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Follower.t()} | {:error, any()} def remove_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}}) when is_admin(role) do case Relay.unfollow(address) do @@ -327,6 +346,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do end end + @spec accept_subscription(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Follower.t()} | {:error, any()} def accept_subscription( _parent, %{address: address}, @@ -342,6 +363,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do end end + @spec reject_subscription(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Follower.t()} | {:error, any()} def reject_subscription( _parent, %{address: address}, @@ -357,7 +380,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do end end - @spec eventually_update_instance_actor(map()) :: :ok + @spec eventually_update_instance_actor(map()) :: :ok | {:error, :instance_actor_update_failure} defp eventually_update_instance_actor(admin_setting_args) do args = %{} new_instance_description = Map.get(admin_setting_args, :instance_description) @@ -382,7 +405,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do if args != %{} do %Actor{} = instance_actor = Relay.get_actor() - case ActivityPub.update(instance_actor, args, true) do + case Actions.Update.update(instance_actor, args, true) do {:ok, _activity, _actor} -> :ok diff --git a/lib/graphql/resolvers/comment.ex b/lib/graphql/resolvers/comment.ex index 386c3fa4..115b654e 100644 --- a/lib/graphql/resolvers/comment.ex +++ b/lib/graphql/resolvers/comment.ex @@ -14,10 +14,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do require Logger + @spec get_thread(any(), map(), Absinthe.Resolution.t()) :: {:ok, [CommentModel.t()]} def get_thread(_parent, %{id: thread_id}, _context) do {:ok, Discussions.get_thread_replies(thread_id)} end + @spec create_comment(any(), map(), Absinthe.Resolution.t()) :: + {:ok, CommentModel.t()} | {:error, :unauthorized | :not_found | any() | String.t()} def create_comment( _parent, %{event_id: event_id} = args, @@ -27,25 +30,28 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do } } ) do - with {:find_event, - {:ok, - %Event{ - options: %EventOptions{comment_moderation: comment_moderation}, - organizer_actor_id: organizer_actor_id - }}} <- - {:find_event, Events.get_event(event_id)}, - {:allowed, true} <- - {:allowed, comment_moderation != :closed || actor_id == organizer_actor_id}, - args <- Map.put(args, :actor_id, actor_id), - {:ok, _, %CommentModel{} = comment} <- - Comments.create_comment(args) do - {:ok, comment} - else - {:error, err} -> - {:error, err} + case Events.get_event(event_id) do + {:ok, + %Event{ + options: %EventOptions{comment_moderation: comment_moderation}, + organizer_actor_id: organizer_actor_id + }} -> + if comment_moderation != :closed || actor_id == organizer_actor_id do + args = Map.put(args, :actor_id, actor_id) - {:allowed, false} -> - {:error, :unauthorized} + case Comments.create_comment(args) do + {:ok, _, %CommentModel{} = comment} -> + {:ok, comment} + + {:error, err} -> + {:error, err} + end + else + {:error, :unauthorized} + end + + {:error, :event_not_found} -> + {:error, :not_found} end end @@ -53,6 +59,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do {:error, dgettext("errors", "You are not allowed to create a comment if not connected")} end + @spec update_comment(any(), map(), Absinthe.Resolution.t()) :: + {:ok, CommentModel.t()} | {:error, :unauthorized | :not_found | any() | String.t()} def update_comment( _parent, %{text: text, comment_id: comment_id}, @@ -62,11 +70,22 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do } } ) do - with %CommentModel{actor_id: comment_actor_id} = comment <- - Mobilizon.Discussions.get_comment_with_preload(comment_id), - true <- actor_id == comment_actor_id, - {:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do - {:ok, comment} + case Mobilizon.Discussions.get_comment_with_preload(comment_id) do + %CommentModel{actor_id: comment_actor_id} = comment -> + if actor_id == comment_actor_id do + case Comments.update_comment(comment, %{text: text}) do + {:ok, _, %CommentModel{} = comment} -> + {:ok, comment} + + {:error, err} -> + {:error, err} + end + else + {:error, dgettext("errors", "You are not the comment creator")} + end + + nil -> + {:error, :not_found} end end @@ -114,10 +133,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do {:error, dgettext("errors", "You are not allowed to delete a comment if not connected")} end + @spec do_delete_comment(CommentModel.t(), Actor.t()) :: + {:ok, CommentModel.t()} | {:error, any()} defp do_delete_comment(%CommentModel{} = comment, %Actor{} = actor) do - with {:ok, _, %CommentModel{} = comment} <- - Comments.delete_comment(comment, actor) do - {:ok, comment} + case Comments.delete_comment(comment, actor) do + {:ok, _, %CommentModel{} = comment} -> + {:ok, comment} + + {:error, err} -> + {:error, err} end end end diff --git a/lib/graphql/resolvers/config.ex b/lib/graphql/resolvers/config.ex index 170993bb..7b50a898 100644 --- a/lib/graphql/resolvers/config.ex +++ b/lib/graphql/resolvers/config.ex @@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do @doc """ Gets config. """ + @spec get_config(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()} def get_config(_parent, _params, %{context: %{ip: ip}}) do geolix = Geolix.lookup(ip) @@ -28,6 +29,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do {:ok, data} end + @spec terms(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()} def terms(_parent, %{locale: locale}, _resolution) do type = Config.instance_terms_type() @@ -41,6 +43,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do {:ok, %{body_html: body_html, type: type, url: url}} end + @spec privacy(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()} def privacy(_parent, %{locale: locale}, _resolution) do type = Config.instance_privacy_type() @@ -54,6 +57,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do {:ok, %{body_html: body_html, type: type, url: url}} end + @spec config_cache :: map() defp config_cache do case Cachex.fetch(:config, "full_config", fn _key -> case build_config_cache() do @@ -62,10 +66,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do end end) do {status, value} when status in [:ok, :commit] -> value - _err -> nil + _err -> %{} end end + @spec build_config_cache :: map() defp build_config_cache do %{ name: Config.instance_name(), diff --git a/lib/graphql/resolvers/discussion.ex b/lib/graphql/resolvers/discussion.ex index 7f127239..d42e5c13 100644 --- a/lib/graphql/resolvers/discussion.ex +++ b/lib/graphql/resolvers/discussion.ex @@ -6,12 +6,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do alias Mobilizon.{Actors, Discussions} alias Mobilizon.Actors.Actor alias Mobilizon.Discussions.{Comment, Discussion} - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.GraphQL.API.Comments alias Mobilizon.Storage.Page alias Mobilizon.Users.User import Mobilizon.Web.Gettext + @spec find_discussions_for_actor(Actor.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Discussion.t())} | {:error, :unauthenticated} def find_discussions_for_actor( %Actor{id: group_id}, %{page: page, limit: limit}, @@ -30,19 +32,33 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do end end - def find_discussions_for_actor(%Actor{}, _args, _resolution) do + def find_discussions_for_actor(%Actor{}, _args, %{ + context: %{ + current_user: %User{} + } + }) do {:ok, %Page{total: 0, elements: []}} end + def find_discussions_for_actor(%Actor{}, _args, _resolution), do: {:error, :unauthenticated} + + @spec get_discussion(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Discussion.t()} | {:error, :unauthorized | :discussion_not_found | String.t()} def get_discussion(_parent, %{id: id}, %{ context: %{ current_actor: %Actor{id: creator_id} } }) do - with %Discussion{actor_id: actor_id} = discussion <- - Discussions.get_discussion(id), - {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do - {:ok, discussion} + case Discussions.get_discussion(id) do + %Discussion{actor_id: actor_id} = discussion -> + if Actors.is_member?(creator_id, actor_id) do + {:ok, discussion} + else + {:error, :unauthorized} + end + + nil -> + {:error, :discussion_not_found} end end @@ -73,6 +89,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do def get_discussion(_parent, _args, _resolution), do: {:error, dgettext("errors", "You need to be logged-in to access discussions")} + @spec get_comments_for_discussion(Discussion.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Discussion.t())} def get_comments_for_discussion( %Discussion{id: discussion_id}, %{page: page, limit: limit}, @@ -81,6 +99,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do {:ok, Discussions.get_comments_for_discussion(discussion_id, page, limit)} end + @spec create_discussion(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Discussion.t()} + | {:error, Ecto.Changeset.t() | String.t() | :unauthorized | :unauthenticated} def create_discussion( _parent, %{title: title, text: text, actor_id: group_id}, @@ -90,27 +111,32 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do } } ) do - with {:member, true} <- {:member, Actors.is_member?(creator_id, group_id)}, - {:ok, _activity, %Discussion{} = discussion} <- - Comments.create_discussion(%{ + if Actors.is_member?(creator_id, group_id) do + case Comments.create_discussion(%{ title: title, text: text, actor_id: group_id, creator_id: creator_id, attributed_to_id: group_id }) do - {:ok, discussion} - else - {:error, type, err, _} when type in [:discussion, :comment] -> - {:error, err} + {:ok, _activity, %Discussion{} = discussion} -> + {:ok, discussion} - {:member, false} -> - {:error, :unauthorized} + {:error, %Ecto.Changeset{} = err} -> + {:error, err} + + {:error, _err} -> + {:error, dgettext("errors", "Error while creating a discussion")} + end + else + {:error, :unauthorized} end end def create_discussion(_, _, _), do: {:error, :unauthenticated} + @spec reply_to_discussion(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Discussion.t()} | {:error, :discussion_not_found | :unauthenticated} def reply_to_discussion( _parent, %{text: text, discussion_id: discussion_id}, @@ -150,7 +176,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do def reply_to_discussion(_, _, _), do: {:error, :unauthenticated} - @spec update_discussion(map(), map(), map()) :: {:ok, Discussion.t()} + @spec update_discussion(map(), map(), map()) :: + {:ok, Discussion.t()} | {:error, :unauthorized | :unauthenticated} def update_discussion( _parent, %{title: title, discussion_id: discussion_id}, @@ -164,7 +191,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do {:no_discussion, Discussions.get_discussion(discussion_id)}, {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)}, {:ok, _activity, %Discussion{} = discussion} <- - ActivityPub.update( + Actions.Update.update( discussion, %{ title: title @@ -179,6 +206,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do def update_discussion(_, _, _), do: {:error, :unauthenticated} + @spec delete_discussion(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Discussion.t()} | {:error, String.t() | :unauthorized | :unauthenticated} def delete_discussion(_parent, %{discussion_id: discussion_id}, %{ context: %{ current_user: %User{}, @@ -189,7 +218,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do {:no_discussion, Discussions.get_discussion(discussion_id)}, {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)}, {:ok, _activity, %Discussion{} = discussion} <- - ActivityPub.delete(discussion, actor) do + Actions.Delete.delete(discussion, actor) do {:ok, discussion} else {:no_discussion, _} -> diff --git a/lib/graphql/resolvers/event.ex b/lib/graphql/resolvers/event.ex index dca44e50..d5119bf6 100644 --- a/lib/graphql/resolvers/event.ex +++ b/lib/graphql/resolvers/event.ex @@ -21,6 +21,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do @event_max_limit 100 @number_of_related_events 3 + @spec organizer_for_event(Event.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t() | nil} | {:error, String.t()} def organizer_for_event( %Event{attributed_to_id: attributed_to_id, organizer_actor_id: organizer_actor_id}, _args, @@ -62,6 +64,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do end end + @spec list_events(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Event.t())} | {:error, :events_max_limit_reached} def list_events( _parent, %{page: page, limit: limit, order_by: order_by, direction: direction}, @@ -75,6 +79,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do {:error, :events_max_limit_reached} end + @spec find_private_event(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Event.t()} | {:error, :event_not_found} defp find_private_event( _parent, %{uuid: uuid}, @@ -106,6 +112,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do {:error, :event_not_found} end + @spec find_event(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Event.t()} | {:error, :event_not_found} def find_event(parent, %{uuid: uuid} = args, %{context: context} = resolution) do with {:has_event, %Event{} = event} <- {:has_event, Events.get_public_event_by_uuid_with_preload(uuid)}, @@ -132,6 +140,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do @doc """ List participants for event (through an event request) """ + @spec list_participants_for_event(Event.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Participant.t())} | {:error, String.t()} def list_participants_for_event( %Event{id: event_id} = event, %{page: page, limit: limit, roles: roles}, @@ -166,6 +176,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do {:ok, %{total: 0, elements: []}} end + @spec stats_participants(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, map()} def stats_participants( %Event{participant_stats: %EventParticipantStats{} = stats, id: event_id} = _event, _args, @@ -198,6 +209,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do @doc """ List related events """ + @spec list_related_events(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Event.t())} def list_related_events( %Event{tags: tags, organizer_actor: organizer_actor, uuid: uuid}, _args, @@ -239,11 +251,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do {:ok, events} end + @spec uniq_events(list(Event.t())) :: list(Event.t()) defp uniq_events(events), do: Enum.uniq_by(events, fn event -> event.uuid end) @doc """ Create an event """ + @spec create_event(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()} def create_event( _parent, %{organizer_actor_id: organizer_actor_id} = args, @@ -283,6 +298,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do @doc """ Update an event """ + @spec update_event(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()} def update_event( _parent, %{event_id: event_id} = args, @@ -327,6 +344,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do @doc """ Delete an event """ + @spec delete_event(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()} def delete_event( _parent, %{event_id: event_id}, @@ -365,6 +384,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do {:error, dgettext("errors", "You need to be logged-in to delete an event")} end + @spec do_delete_event(Event.t(), Actor.t(), boolean()) :: {:ok, map()} defp do_delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) when is_boolean(federate) do with {:ok, _activity, event} <- API.Events.delete_event(event, actor) do @@ -372,6 +392,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do end end + @spec is_organizer_group_member?(map()) :: boolean() defp is_organizer_group_member?(%{ attributed_to_id: attributed_to_id, organizer_actor_id: organizer_actor_id @@ -383,6 +404,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do defp is_organizer_group_member?(_), do: true + @spec verify_profile_change(map(), Event.t(), User.t(), Actor.t()) :: {:ok, map()} defp verify_profile_change( args, %Event{attributed_to: %Actor{}}, diff --git a/lib/graphql/resolvers/followers.ex b/lib/graphql/resolvers/followers.ex index c621da59..b7552054 100644 --- a/lib/graphql/resolvers/followers.ex +++ b/lib/graphql/resolvers/followers.ex @@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do import Mobilizon.Users.Guards alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, Follower} - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.Storage.Page alias Mobilizon.Users.User @@ -43,9 +43,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do {:member, Actors.is_moderator?(actor_id, group_id)}, {:ok, _activity, %Follower{} = follower} <- (if approved do - ActivityPub.accept(:follow, follower) + Actions.Accept.accept(:follow, follower) else - ActivityPub.reject(:follow, follower) + Actions.Reject.reject(:follow, follower) end) do {:ok, follower} else diff --git a/lib/graphql/resolvers/group.ex b/lib/graphql/resolvers/group.ex index c0d25bff..2ef9460f 100644 --- a/lib/graphql/resolvers/group.ex +++ b/lib/graphql/resolvers/group.ex @@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do import Mobilizon.Users.Guards alias Mobilizon.{Actors, Events} alias Mobilizon.Actors.{Actor, Member} - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.GraphQL.API alias Mobilizon.Users.User @@ -15,6 +15,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do require Logger + @spec find_group( + any, + %{:preferred_username => binary, optional(any) => any}, + Absinthe.Resolution.t() + ) :: + {:error, :group_not_found} | {:ok, Actor.t()} @doc """ Find a group """ @@ -27,29 +33,26 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do } } ) do - with {:group, {:ok, %Actor{id: group_id, suspended: false} = group}} <- - {:group, ActivityPubActor.find_or_make_group_from_nickname(name)}, - {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do - {:ok, group} - else - {:member, false} -> - find_group(parent, args, nil) + case ActivityPubActor.find_or_make_group_from_nickname(name) do + {:ok, %Actor{id: group_id, suspended: false} = group} -> + if Actors.is_member?(actor_id, group_id) do + {:ok, group} + else + find_group(parent, args, nil) + end - {:group, _} -> + {:error, _err} -> {:error, :group_not_found} - - _ -> - {:error, :unknown} end end def find_group(_parent, %{preferred_username: name}, _resolution) do - with {:ok, %Actor{suspended: false} = actor} <- - ActivityPubActor.find_or_make_group_from_nickname(name), - %Actor{} = actor <- restrict_fields_for_non_member_request(actor) do - {:ok, actor} - else - _ -> + case ActivityPubActor.find_or_make_group_from_nickname(name) do + {:ok, %Actor{suspended: false} = actor} -> + %Actor{} = actor = restrict_fields_for_non_member_request(actor) + {:ok, actor} + + {:error, _err} -> {:error, :group_not_found} end end @@ -57,13 +60,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do @doc """ Get a group """ + @spec get_group(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t()} def get_group(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do - with %Actor{type: :Group, suspended: suspended} = actor <- - Actors.get_actor_with_preload(id, true), - true <- suspended == false or is_moderator(role) do - {:ok, actor} - else - _ -> + case Actors.get_actor_with_preload(id, true) do + %Actor{type: :Group, suspended: suspended} = actor -> + if suspended == false or is_moderator(role) do + {:ok, actor} + else + {:error, dgettext("errors", "Group with ID %{id} not found", id: id)} + end + + nil -> {:error, dgettext("errors", "Group with ID %{id} not found", id: id)} end end @@ -71,6 +79,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do @doc """ Lists all groups """ + @spec list_groups(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Actor.t())} | {:error, String.t()} def list_groups( _parent, %{ @@ -95,6 +105,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do do: {:error, dgettext("errors", "You may not list groups unless moderator.")} # TODO Move me to somewhere cleaner + @spec save_attached_pictures(map()) :: map() defp save_attached_pictures(args) do Enum.reduce([:avatar, :banner], args, fn key, args -> if is_map(args) && Map.has_key?(args, key) && !is_nil(args[key][:media]) do @@ -113,6 +124,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do @doc """ Create a new group. The creator is automatically added as admin """ + @spec create_group(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t()} def create_group( _parent, args, @@ -145,6 +158,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do @doc """ Update a group. The creator is automatically added as admin """ + @spec update_group(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t()} def update_group( _parent, %{id: group_id} = args, @@ -154,22 +169,24 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do } } ) do - with {:administrator, true} <- - {:administrator, Actors.is_administrator?(updater_actor.id, group_id)}, - args when is_map(args) <- Map.put(args, :updater_actor, updater_actor), - {:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)}, - {:ok, _activity, %Actor{type: :Group} = group} <- - API.Groups.update_group(args) do - {:ok, group} + if Actors.is_administrator?(updater_actor.id, group_id) do + args = Map.put(args, :updater_actor, updater_actor) + + case save_attached_pictures(args) do + {:error, :file_too_large} -> + {:error, dgettext("errors", "The provided picture is too heavy")} + + map when is_map(map) -> + case API.Groups.update_group(args) do + {:ok, _activity, %Actor{type: :Group} = group} -> + {:ok, group} + + {:error, _err} -> + {:error, dgettext("errors", "Failed to update the group")} + end + end else - {:picture, {:error, :file_too_large}} -> - {:error, dgettext("errors", "The provided picture is too heavy")} - - {:error, err} when is_binary(err) -> - {:error, err} - - {:administrator, false} -> - {:error, dgettext("errors", "Profile is not administrator for the group")} + {:error, dgettext("errors", "Profile is not administrator for the group")} end end @@ -180,6 +197,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do @doc """ Delete an existing group """ + @spec delete_group(any(), map(), Absinthe.Resolution.t()) :: + {:ok, %{id: integer()}} | {:error, String.t()} def delete_group( _parent, %{group_id: group_id}, @@ -192,7 +211,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), {:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id), {:is_admin, true} <- {:is_admin, Member.is_administrator(member)}, - {:ok, _activity, group} <- ActivityPub.delete(group, actor, true) do + {:ok, _activity, group} <- Actions.Delete.delete(group, actor, true) do {:ok, %{id: group.id}} else {:error, :group_not_found} -> @@ -214,6 +233,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do @doc """ Join an existing group """ + @spec join_group(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Member.t()} | {:error, String.t()} def join_group(_parent, %{group_id: group_id} = args, %{ context: %{current_actor: %Actor{} = actor} }) do @@ -222,7 +243,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do {:error, :member_not_found} <- Actors.get_member(actor.id, group.id), {:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)}, {:ok, _activity, %Member{} = member} <- - ActivityPub.join(group, actor, true, args) do + Actions.Join.join(group, actor, true, args) do {:ok, member} else {:error, :group_not_found} -> @@ -243,6 +264,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do @doc """ Leave a existing group """ + @spec leave_group(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Member.t()} | {:error, String.t()} def leave_group( _parent, %{group_id: group_id}, @@ -253,7 +276,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do } ) do with {:group, %Actor{type: :Group} = group} <- {:group, Actors.get_actor(group_id)}, - {:ok, _activity, %Member{} = member} <- ActivityPub.leave(group, actor, true) do + {:ok, _activity, %Member{} = member} <- + Actions.Leave.leave(group, actor, true) do {:ok, member} else {:error, :member_not_found} -> @@ -262,7 +286,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do {:group, nil} -> {:error, dgettext("errors", "Group not found")} - {:is_not_only_admin, false} -> + {:error, :is_not_only_admin} -> {:error, dgettext("errors", "You can't leave this group because you are the only administrator")} end @@ -272,6 +296,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do {:error, dgettext("errors", "You need to be logged-in to leave a group")} end + @spec find_events_for_group(Actor.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Event.t())} def find_events_for_group( %Actor{id: group_id} = group, %{ @@ -320,16 +346,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do )} end + @spec restrict_fields_for_non_member_request(Actor.t()) :: Actor.t() defp restrict_fields_for_non_member_request(%Actor{} = group) do - Map.merge( - group, - %{ - followers: [], + %Actor{ + group + | followers: [], followings: [], organized_events: [], comments: [], feed_tokens: [] - } - ) + } end end diff --git a/lib/graphql/resolvers/member.ex b/lib/graphql/resolvers/member.ex index fe796a59..9c279325 100644 --- a/lib/graphql/resolvers/member.ex +++ b/lib/graphql/resolvers/member.ex @@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do import Mobilizon.Users.Guards alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, Member} - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Storage.Page alias Mobilizon.Users.User @@ -17,6 +17,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do If actor requesting is not part of the group, we only return the number of members, not members """ + @spec find_members_for_group(Actor.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Member.t())} def find_members_for_group( %Actor{id: group_id} = group, %{page: page, limit: limit, roles: roles}, @@ -47,11 +49,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do end def find_members_for_group(%Actor{} = group, _args, _resolution) do - with %Page{} = page <- Actors.list_members_for_group(group) do - {:ok, %Page{page | elements: []}} - end + %Page{} = page = Actors.list_members_for_group(group) + {:ok, %Page{page | elements: []}} end + @spec invite_member(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Member.t()} | {:error, String.t()} def invite_member( _parent, %{group_id: group_id, target_actor_username: target_actor_username}, @@ -68,7 +71,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do ActivityPubActor.find_or_make_actor_from_nickname(target_actor_username)}, {:existant, true} <- {:existant, check_member_not_existant_or_rejected(target_actor_id, group.id)}, - {:ok, _activity, %Member{} = member} <- ActivityPub.invite(group, actor, target_actor) do + {:ok, _activity, %Member{} = member} <- + Actions.Invite.invite(group, actor, target_actor) do {:ok, member} else {:error, :group_not_found} -> @@ -92,6 +96,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do end end + @spec accept_invitation(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Member.t()} | {:error, String.t()} def accept_invitation(_parent, %{id: member_id}, %{ context: %{current_actor: %Actor{id: actor_id}} }) do @@ -99,7 +105,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do Actors.get_member(member_id), {:is_same_actor, true} <- {:is_same_actor, member_actor_id == actor_id}, {:ok, _activity, %Member{} = member} <- - ActivityPub.accept( + Actions.Accept.accept( :invite, member, true @@ -111,6 +117,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do end end + @spec reject_invitation(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Member.t()} | {:error, String.t()} def reject_invitation(_parent, %{id: member_id}, %{ context: %{current_actor: %Actor{id: actor_id}} }) do @@ -118,7 +126,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do {:invitation_exists, Actors.get_member(member_id)}, {:is_same_actor, true} <- {:is_same_actor, member_actor_id == actor_id}, {:ok, _activity, %Member{} = member} <- - ActivityPub.reject( + Actions.Reject.reject( :invite, member, true @@ -133,12 +141,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do end end + @spec update_member(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Member.t()} | {:error, String.t()} def update_member(_parent, %{member_id: member_id, role: role}, %{ context: %{current_actor: %Actor{} = moderator} }) do with %Member{} = member <- Actors.get_member(member_id), {:ok, _activity, %Member{} = member} <- - ActivityPub.update(member, %{role: role}, true, %{moderator: moderator}) do + Actions.Update.update(member, %{role: role}, true, %{moderator: moderator}) do {:ok, member} else {:error, :member_not_found} -> @@ -156,6 +166,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do def update_member(_parent, _args, _resolution), do: {:error, "You must be logged-in to update a member"} + @spec remove_member(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Member.t()} | {:error, String.t()} def remove_member(_parent, %{member_id: member_id, group_id: group_id}, %{ context: %{current_actor: %Actor{id: moderator_id} = moderator} }) do @@ -164,7 +176,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do {:has_rights_to_remove, {:ok, %Member{role: role}}} when role in [:moderator, :administrator, :creator] <- {:has_rights_to_remove, Actors.get_member(moderator_id, group_id)}, - {:ok, _activity, %Member{}} <- ActivityPub.remove(member, group, moderator, true) do + {:ok, _activity, %Member{}} <- + Actions.Remove.remove(member, group, moderator, true) do {:ok, member} else %Member{role: :rejected} -> diff --git a/lib/graphql/resolvers/participant.ex b/lib/graphql/resolvers/participant.ex index f9035dde..df3ab9bf 100644 --- a/lib/graphql/resolvers/participant.ex +++ b/lib/graphql/resolvers/participant.ex @@ -16,6 +16,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do @doc """ Join an event for an regular or anonymous actor """ + @spec actor_join_event(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Participant.t()} | {:error, String.t()} def actor_join_event( _parent, %{actor_id: actor_id, event_id: event_id} = args, @@ -157,6 +159,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do @doc """ Leave an event for an anonymous actor """ + @spec actor_leave_event(any(), map(), Absinthe.Resolution.t()) :: + {:ok, map()} | {:error, String.t()} def actor_leave_event( _parent, %{actor_id: actor_id, event_id: event_id, token: token}, @@ -220,6 +224,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do {:error, dgettext("errors", "You need to be logged-in to leave an event")} end + @spec update_participation(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Participation.t()} | {:error, String.t()} def update_participation( _parent, %{id: participation_id, role: new_role}, diff --git a/lib/graphql/resolvers/person.ex b/lib/graphql/resolvers/person.ex index ff3a36f1..709927d4 100644 --- a/lib/graphql/resolvers/person.ex +++ b/lib/graphql/resolvers/person.ex @@ -12,7 +12,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do alias Mobilizon.Users.User import Mobilizon.Web.Gettext - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor require Logger @@ -21,6 +21,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do @doc """ Get a person """ + @spec get_person(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t() | :unauthorized} def get_person(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do with %Actor{suspended: suspended} = actor <- Actors.get_actor_with_preload(id, true), true <- suspended == false or is_moderator(role) do @@ -36,6 +38,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do @doc """ Find a person """ + @spec fetch_person(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t() | :unauthorized | :unauthenticated} def fetch_person(_parent, %{preferred_username: preferred_username}, %{ context: %{current_user: %User{} = user} }) do @@ -57,6 +61,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do def fetch_person(_parent, _args, _resolution), do: {:error, :unauthenticated} + @spec list_persons(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Actor.t())} | {:error, :unauthorized | :unauthenticated} def list_persons( _parent, %{ @@ -92,7 +98,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do Returns the current actor for the currently logged-in user """ @spec get_current_person(any, any, Absinthe.Resolution.t()) :: - {:error, :unauthenticated} | {:ok, Actor.t()} + {:error, :unauthenticated | :no_current_person} | {:ok, Actor.t()} def get_current_person(_parent, _args, %{context: %{current_actor: %Actor{} = actor}}) do {:ok, actor} end @@ -121,6 +127,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do @doc """ This function is used to create more identities from an existing user """ + @spec create_person(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t() | :unauthenticated} def create_person( _parent, %{preferred_username: _preferred_username} = args, @@ -148,6 +156,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do @doc """ This function is used to update an existing identity """ + @spec update_person(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t() | :unauthenticated} def update_person( _parent, %{id: id} = args, @@ -160,7 +170,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do {:ok, %Actor{} = actor} -> case save_attached_pictures(args) do args when is_map(args) -> - case ActivityPub.update(actor, args, true) do + case Actions.Update.update(actor, args, true) do {:ok, _activity, %Actor{} = actor} -> {:ok, actor} @@ -184,6 +194,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do @doc """ This function is used to delete an existing identity """ + @spec delete_person(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t() | :unauthenticated} def delete_person( _parent, %{id: id} = _args, @@ -225,6 +237,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do end end + @spec last_identity?(User.t()) :: boolean defp last_identity?(user) do length(Users.get_actors_for_user(user)) <= 1 end @@ -275,6 +288,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do @doc """ This function is used to register a person afterwards the user has been created (but not activated) """ + @spec register_person(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Actor.t()} | {:error, String.t()} def register_person(_parent, args, _resolution) do # When registering, email is assumed confirmed (unlike changing email) case Users.get_user_by_email(args.email, unconfirmed: false) do @@ -311,6 +326,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do @doc """ Returns the participations, optionally restricted to an event """ + @spec person_participations(Actor.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Participant.t())} | {:error, :unauthorized | String.t()} def person_participations( %Actor{id: actor_id} = person, %{event_id: event_id}, @@ -329,13 +346,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do def person_participations(%Actor{} = person, %{page: page, limit: limit}, %{ context: %{current_user: %User{} = user} }) do - with {:can_get_participations, true} <- - {:can_get_participations, user_can_access_person_details?(person, user)}, - %Page{} = page <- Events.list_event_participations_for_actor(person, page, limit) do + if user_can_access_person_details?(person, user) do + %Page{} = page = Events.list_event_participations_for_actor(person, page, limit) {:ok, page} else - {:can_get_participations, false} -> - {:error, dgettext("errors", "Profile is not owned by authenticated user")} + {:error, dgettext("errors", "Profile is not owned by authenticated user")} end end @@ -346,24 +361,23 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do def person_memberships(%Actor{id: actor_id} = person, %{group: group}, %{ context: %{current_user: %User{} = user} }) do - with {:can_get_memberships, true} <- - {:can_get_memberships, user_can_access_person_details?(person, user)}, - {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(group, :Group)}, - {:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id), - memberships <- %Page{ + if user_can_access_person_details?(person, user) do + with {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(group, :Group)}, + {:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id) do + {:ok, + %Page{ total: 1, elements: [Repo.preload(membership, [:actor, :parent, :invited_by])] - } do - {:ok, memberships} + }} + else + {:error, :member_not_found} -> + {:ok, %Page{total: 0, elements: []}} + + {:group, nil} -> + {:error, :group_not_found} + end else - {:error, :member_not_found} -> - {:ok, %Page{total: 0, elements: []}} - - {:group, nil} -> - {:error, :group_not_found} - - {:can_get_memberships, _} -> - {:error, dgettext("errors", "Profile is not owned by authenticated user")} + {:error, dgettext("errors", "Profile is not owned by authenticated user")} end end @@ -384,6 +398,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do end end + @spec user_for_person(Actor.t(), map(), Absinthe.Resolution.t()) :: + {:ok, User.t() | nil} | {:error, String.t() | nil} def user_for_person(%Actor{type: :Person, user_id: user_id}, _args, %{ context: %{current_user: %User{role: role}} }) @@ -402,6 +418,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do def user_for_person(_, _args, _resolution), do: {:error, nil} + @spec organized_events_for_person(Actor.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Event.t())} | {:error, :unauthorized} def organized_events_for_person( %Actor{} = person, %{page: page, limit: limit}, @@ -409,13 +427,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do context: %{current_user: %User{} = user} } ) do - with {:can_get_events, true} <- - {:can_get_events, user_can_access_person_details?(person, user)}, - %Page{} = page <- Events.list_organized_events_for_actor(person, page, limit) do + if user_can_access_person_details?(person, user) do + %Page{} = page = Events.list_organized_events_for_actor(person, page, limit) {:ok, page} else - {:can_get_events, false} -> - {:error, :unauthorized} + {:error, :unauthorized} end end diff --git a/lib/graphql/resolvers/post.ex b/lib/graphql/resolvers/post.ex index 25acc130..70ad40cd 100644 --- a/lib/graphql/resolvers/post.ex +++ b/lib/graphql/resolvers/post.ex @@ -6,8 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do import Mobilizon.Users.Guards alias Mobilizon.{Actors, Posts} alias Mobilizon.Actors.Actor - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.{Permission, Utils} + alias Mobilizon.Federation.ActivityPub.{Actions, Permission, Utils} alias Mobilizon.Posts.Post alias Mobilizon.Storage.Page alias Mobilizon.Users.User @@ -22,6 +21,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do Returns only if actor requesting is a member of the group """ + @spec find_posts_for_group(Actor.t(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(Post.t())} def find_posts_for_group( %Actor{id: group_id} = group, %{page: page, limit: limit} = args, @@ -32,13 +32,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do } } = _resolution ) do - with {:member, true} <- - {:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)}, - %Page{} = page <- Posts.get_posts_for_group(group, page, limit) do + if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do + %Page{} = page = Posts.get_posts_for_group(group, page, limit) {:ok, page} else - {:member, _} -> - find_posts_for_group(group, args, nil) + find_posts_for_group(group, args, nil) end end @@ -47,9 +45,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do %{page: page, limit: limit}, _resolution ) do - with %Page{} = page <- Posts.get_public_posts_for_group(group, page, limit) do - {:ok, page} - end + %Page{} = page = Posts.get_public_posts_for_group(group, page, limit) + {:ok, page} end def find_posts_for_group( @@ -60,6 +57,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do {:ok, %Page{total: 0, elements: []}} end + @spec get_post(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Post.t()} | {:error, :post_not_found} def get_post( parent, %{slug: slug}, @@ -101,6 +100,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do {:error, :post_not_found} end + @spec create_post(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Post.t()} | {:error, String.t()} def create_post( _parent, %{attributed_to_id: group_id} = args, @@ -118,7 +119,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do end), args <- extract_pictures_from_post_body(args, actor_id), {:ok, _, %Post{} = post} <- - ActivityPub.create( + Actions.Create.create( :post, args |> Map.put(:author_id, actor_id) @@ -140,6 +141,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do {:error, dgettext("errors", "You need to be logged-in to create posts")} end + @spec update_post(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Post.t()} | {:error, String.t()} def update_post( _parent, %{id: id} = args, @@ -159,7 +162,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do args <- extract_pictures_from_post_body(args, actor_id), {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %Post{} = post} <- - ActivityPub.update(post, args, true, %{"actor" => actor_url}) do + Actions.Update.update(post, args, true, %{"actor" => actor_url}) do {:ok, post} else {:uuid, :error} -> @@ -177,6 +180,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do {:error, dgettext("errors", "You need to be logged-in to update posts")} end + @spec delete_post(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Post.t()} | {:error, String.t()} def delete_post( _parent, %{id: post_id}, @@ -191,7 +196,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do {:post, Posts.get_post_with_preloads(post_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %Post{} = post} <- - ActivityPub.delete(post, actor) do + Actions.Delete.delete(post, actor) do {:ok, post} else {:uuid, :error} -> @@ -209,6 +214,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do {:error, dgettext("errors", "You need to be logged-in to delete posts")} end + @spec process_picture(map() | nil, Actor.t()) :: nil | map() defp process_picture(nil, _), do: nil defp process_picture(%{media_id: _picture_id} = args, _), do: args diff --git a/lib/graphql/resolvers/push_subscription.ex b/lib/graphql/resolvers/push_subscription.ex index dd2018db..5501fa8b 100644 --- a/lib/graphql/resolvers/push_subscription.ex +++ b/lib/graphql/resolvers/push_subscription.ex @@ -10,6 +10,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do @doc """ List all of an user's registered push subscriptions """ + @spec list_user_push_subscriptions(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(PushSubscription.t())} | {:error, :unauthenticated} def list_user_push_subscriptions(_parent, %{page: page, limit: limit}, %{ context: %{current_user: %User{id: user_id}} }) do @@ -22,6 +24,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do @doc """ Register a push subscription """ + @spec register_push_subscription(any(), map(), Absinthe.Resolution.t()) :: + {:ok, String.t()} | {:error, String.t()} def register_push_subscription(_parent, args, %{ context: %{current_user: %User{id: user_id}} }) do diff --git a/lib/graphql/resolvers/report.ex b/lib/graphql/resolvers/report.ex index 13114273..f5a0b8f1 100644 --- a/lib/graphql/resolvers/report.ex +++ b/lib/graphql/resolvers/report.ex @@ -13,6 +13,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do alias Mobilizon.GraphQL.API + @spec list_reports(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Report.t())} | {:error, String.t()} def list_reports( _parent, %{page: page, limit: limit, status: status}, @@ -26,6 +28,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do {:error, dgettext("errors", "You need to be logged-in and a moderator to list reports")} end + @spec get_report(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Report.t()} | {:error, String.t()} def get_report(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) when is_moderator(role) do case Mobilizon.Reports.get_report(id) do @@ -44,6 +48,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do @doc """ Create a report, either logged-in or anonymously """ + @spec create_report(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Report.t()} | {:error, String.t()} def create_report( _parent, args, @@ -80,6 +86,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do @doc """ Update a report's status """ + @spec update_report(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Report.t()} | {:error, String.t()} def update_report( _parent, %{report_id: report_id, status: status}, @@ -99,6 +107,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do {:error, dgettext("errors", "You need to be logged-in and a moderator to update a report")} end + @spec create_report_note(any(), map(), Absinthe.Resolution.t()) :: {:ok, Note.t()} def create_report_note( _parent, %{report_id: report_id, content: content}, @@ -112,6 +121,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do end end + @spec delete_report_note(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()} def delete_report_note( _parent, %{note_id: note_id}, diff --git a/lib/graphql/resolvers/resource.ex b/lib/graphql/resolvers/resource.ex index 8e3783a8..f10f35c8 100644 --- a/lib/graphql/resolvers/resource.ex +++ b/lib/graphql/resolvers/resource.ex @@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do alias Mobilizon.{Actors, Resources} alias Mobilizon.Actors.Actor - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.Resources.Resource alias Mobilizon.Resources.Resource.Metadata alias Mobilizon.Service.RichMedia.Parser @@ -21,6 +21,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do Returns only if actor requesting is a member of the group """ + @spec find_resources_for_group(Actor.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Resource.t())} def find_resources_for_group( %Actor{id: group_id} = group, %{page: page, limit: limit}, @@ -47,6 +49,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do {:ok, %Page{total: 0, elements: []}} end + @spec find_resources_for_parent(Resource.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Resource.t())} def find_resources_for_parent( %Resource{actor_id: group_id} = parent, %{page: page, limit: limit}, @@ -65,6 +69,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do def find_resources_for_parent(_parent, _args, _resolution), do: {:ok, %Page{total: 0, elements: []}} + @spec get_resource(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Resource.t()} | {:error, :group_not_found | :resource_not_found | String.t()} def get_resource( _parent, %{path: path, username: username}, @@ -90,6 +96,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do {:error, dgettext("errors", "You need to be logged-in to access resources")} end + @spec create_resource(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Resource.t()} | {:error, String.t()} def create_resource( _parent, %{actor_id: group_id} = args, @@ -103,7 +111,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do parent <- get_eventual_parent(args), {:own_check, true} <- {:own_check, check_resource_owned_by_group(parent, group_id)}, {:ok, _, %Resource{} = resource} <- - ActivityPub.create( + Actions.Create.create( :resource, args |> Map.put(:actor_id, group_id) @@ -128,6 +136,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do {:error, dgettext("errors", "You need to be logged-in to create resources")} end + @spec update_resource(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Resource.t()} | {:error, String.t()} def update_resource( _parent, %{id: resource_id} = args, @@ -137,18 +147,25 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do } } = _resolution ) do - with {:resource, %Resource{actor_id: group_id} = resource} <- - {:resource, Resources.get_resource_with_preloads(resource_id)}, - {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, - {:ok, _, %Resource{} = resource} <- - ActivityPub.update(resource, args, true, %{"actor" => actor_url}) do - {:ok, resource} - else - {:resource, _} -> - {:error, dgettext("errors", "Resource doesn't exist")} + case Resources.get_resource_with_preloads(resource_id) do + %Resource{actor_id: group_id} = resource -> + if Actors.is_member?(actor_id, group_id) do + case Actions.Update.update(resource, args, true, %{"actor" => actor_url}) do + {:ok, _, %Resource{} = resource} -> + {:ok, resource} - {:member, _} -> - {:error, dgettext("errors", "Profile is not member of group")} + {:error, %Ecto.Changeset{} = err} -> + {:error, err} + + {:error, err} when is_atom(err) -> + {:error, dgettext("errors", "Unknown error while updating resource")} + end + else + {:error, dgettext("errors", "Profile is not member of group")} + end + + nil -> + {:error, dgettext("errors", "Resource doesn't exist")} end end @@ -156,6 +173,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do {:error, dgettext("errors", "You need to be logged-in to update resources")} end + @spec delete_resource(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Resource.t()} | {:error, String.t()} def delete_resource( _parent, %{id: resource_id}, @@ -169,7 +188,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do {:resource, Resources.get_resource_with_preloads(resource_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %Resource{} = resource} <- - ActivityPub.delete(resource, actor) do + Actions.Delete.delete(resource, actor) do {:ok, resource} else {:resource, _} -> @@ -184,6 +203,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do {:error, dgettext("errors", "You need to be logged-in to delete resources")} end + @spec preview_resource_link(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Metadata.t()} | {:error, String.t() | :unknown_resource} def preview_resource_link( _parent, %{resource_url: resource_url}, @@ -211,6 +232,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do {:error, dgettext("errors", "You need to be logged-in to view a resource preview")} end + @spec proxyify_pictures(Metadata.t(), map(), Absinthe.Resolution.t()) :: + {:ok, String.t() | nil} | {:error, String.t()} def proxyify_pictures(%Metadata{} = metadata, _args, %{ definition: %{schema_node: %{name: name}} }) do diff --git a/lib/graphql/resolvers/search.ex b/lib/graphql/resolvers/search.ex index 9d3a68ff..6daf445b 100644 --- a/lib/graphql/resolvers/search.ex +++ b/lib/graphql/resolvers/search.ex @@ -2,12 +2,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do @moduledoc """ Handles the event-related GraphQL calls """ - + alias Mobilizon.Actors.Actor + alias Mobilizon.Events.Event alias Mobilizon.GraphQL.API.Search + alias Mobilizon.Storage.Page @doc """ Search persons """ + @spec search_persons(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Actor.t())} | {:error, String.t()} def search_persons(_parent, %{page: page, limit: limit} = args, _resolution) do Search.search_actors(Map.put(args, :minimum_visibility, :private), page, limit, :Person) end @@ -15,6 +19,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do @doc """ Search groups """ + @spec search_groups(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Actor.t())} | {:error, String.t()} def search_groups(_parent, %{page: page, limit: limit} = args, _resolution) do Search.search_actors(args, page, limit, :Group) end @@ -22,10 +28,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do @doc """ Search events """ + @spec search_events(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Event.t())} | {:error, String.t()} def search_events(_parent, %{page: page, limit: limit} = args, _resolution) do Search.search_events(args, page, limit) end + @spec interact(any(), map(), Absinthe.Resolution.t()) :: {:ok, struct} | {:error, :not_found} def interact(_parent, %{uri: uri}, _resolution) do Search.interact(uri) end diff --git a/lib/graphql/resolvers/statistics.ex b/lib/graphql/resolvers/statistics.ex index c451cde4..c199c76c 100644 --- a/lib/graphql/resolvers/statistics.ex +++ b/lib/graphql/resolvers/statistics.ex @@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Statistics do @doc """ Gets config. """ + @spec get_statistics(any(), any(), any()) :: {:ok, map()} def get_statistics(_parent, _params, _context) do {:ok, %{ diff --git a/lib/graphql/resolvers/tag.ex b/lib/graphql/resolvers/tag.ex index df67eb9c..8257187e 100644 --- a/lib/graphql/resolvers/tag.ex +++ b/lib/graphql/resolvers/tag.ex @@ -6,7 +6,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do alias Mobilizon.{Events, Posts} alias Mobilizon.Events.{Event, Tag} alias Mobilizon.Posts.Post + alias Mobilizon.Storage.Page + @spec list_tags(any(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(Tag.t())} def list_tags(_parent, %{page: page, limit: limit} = args, _resolution) do filter = Map.get(args, :filter) tags = Mobilizon.Events.list_tags(filter, page, limit) @@ -19,6 +21,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do From an event or a struct with an url """ + @spec list_tags_for_event(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Tag.t())} def list_tags_for_event(%Event{id: id}, _args, _resolution) do {:ok, Events.list_tags_for_event(id)} end @@ -33,6 +36,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do @doc """ Retrieve the list of tags for a post """ + @spec list_tags_for_post(Post.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Tag.t())} def list_tags_for_post(%Post{id: id}, _args, _resolution) do {:ok, Posts.list_tags_for_post(id)} end @@ -50,9 +54,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do @doc """ Retrieve the list of related tags for a parent tag """ + @spec list_tags_for_post(Tag.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Tag.t())} def get_related_tags(%Tag{} = tag, _args, _resolution) do - with tags <- Events.list_tag_neighbors(tag) do - {:ok, tags} - end + {:ok, Events.list_tag_neighbors(tag)} end end diff --git a/lib/graphql/resolvers/todos.ex b/lib/graphql/resolvers/todos.ex index 13e4cd16..f8a269ea 100644 --- a/lib/graphql/resolvers/todos.ex +++ b/lib/graphql/resolvers/todos.ex @@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do alias Mobilizon.{Actors, Todos} alias Mobilizon.Actors.Actor - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.Storage.Page alias Mobilizon.Todos.{Todo, TodoList} import Mobilizon.Web.Gettext @@ -17,6 +17,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do Returns only if actor requesting is a member of the group """ + @spec find_todo_lists_for_group(Actor.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(TodoList.t())} def find_todo_lists_for_group( %Actor{id: group_id} = group, _args, @@ -39,6 +41,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do {:ok, %Page{total: 0, elements: []}} end + @spec find_todo_lists_for_group(TodoList.t(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(Todo.t())} | {:error, String.t()} def find_todos_for_todo_list( %TodoList{actor_id: group_id} = todo_list, _args, @@ -55,6 +59,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do end end + @spec get_todo_list(any(), map(), Absinthe.Resolution.t()) :: + {:ok, TodoList.t()} | {:error, String.t()} def get_todo_list( _parent, %{id: todo_list_id}, @@ -78,6 +84,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do end end + @spec create_todo_list(any(), map(), Absinthe.Resolution.t()) :: + {:ok, TodoList.t()} | {:error, String.t()} def create_todo_list( _parent, %{group_id: group_id} = args, @@ -87,7 +95,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do ) do with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %TodoList{} = todo_list} <- - ActivityPub.create(:todo_list, Map.put(args, :actor_id, group_id), true, %{}) do + Actions.Create.create( + :todo_list, + Map.put(args, :actor_id, group_id), + true, + %{} + ) do {:ok, todo_list} else {:actor, nil} -> @@ -110,7 +123,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do # {:todo_list, Todos.get_todo_list(todo_list_id)}, # {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, # {:ok, _, %TodoList{} = todo} <- - # ActivityPub.update_todo_list(todo_list, actor, true, %{}) do + # Actions.Update.update_todo_list(todo_list, actor, true, %{}) do # {:ok, todo} # else # {:todo_list, _} -> @@ -133,7 +146,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do # {:todo_list, Todos.get_todo_list(todo_list_id)}, # {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, # {:ok, _, %TodoList{} = todo} <- - # ActivityPub.delete_todo_list(todo_list, actor, true, %{}) do + # Actions.Delete.delete_todo_list(todo_list, actor, true, %{}) do # {:ok, todo} # else # {:todo_list, _} -> @@ -144,6 +157,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do # end # end + @spec get_todo(any(), map(), Absinthe.Resolution.t()) :: {:ok, Todo.t()} | {:error, String.t()} def get_todo( _parent, %{id: todo_id}, @@ -169,6 +183,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do end end + @spec create_todo(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Todo.t()} | {:error, String.t()} def create_todo( _parent, %{todo_list_id: todo_list_id} = args, @@ -180,7 +196,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do {:todo_list, Todos.get_todo_list(todo_list_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %Todo{} = todo} <- - ActivityPub.create(:todo, Map.put(args, :creator_id, actor_id), true, %{}) do + Actions.Create.create( + :todo, + Map.put(args, :creator_id, actor_id), + true, + %{} + ) do {:ok, todo} else {:actor, nil} -> @@ -194,6 +215,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do end end + @spec update_todo(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Todo.t()} | {:error, String.t()} def update_todo( _parent, %{id: todo_id} = args, @@ -207,7 +230,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do {:todo_list, Todos.get_todo_list(todo_list_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %Todo{} = todo} <- - ActivityPub.update(todo, args, true, %{}) do + Actions.Update.update(todo, args, true, %{}) do {:ok, todo} else {:actor, nil} -> @@ -238,7 +261,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do # {:todo_list, Todos.get_todo_list(todo_list_id)}, # {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, # {:ok, _, %Todo{} = todo} <- - # ActivityPub.delete_todo(todo, actor, true, %{}) do + # Actions.Delete.delete_todo(todo, actor, true, %{}) do # {:ok, todo} # else # {:todo_list, _} -> diff --git a/lib/graphql/resolvers/user.ex b/lib/graphql/resolvers/user.ex index b9287529..d24cfdab 100644 --- a/lib/graphql/resolvers/user.ex +++ b/lib/graphql/resolvers/user.ex @@ -7,8 +7,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do alias Mobilizon.{Actors, Admin, Config, Events, Users} alias Mobilizon.Actors.Actor - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.Relay + alias Mobilizon.Federation.ActivityPub.{Actions, Relay} alias Mobilizon.Service.Auth.Authenticator alias Mobilizon.Storage.{Page, Repo} alias Mobilizon.Users.{Setting, User} @@ -21,6 +20,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do @doc """ Find an user by its ID """ + @spec find_user(any(), map(), Absinthe.Resolution.t()) :: {:ok, User.t()} | {:error, String.t()} def find_user(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) when is_moderator(role) do with {:ok, %User{} = user} <- Users.get_user_with_actors(id) do @@ -44,6 +44,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do @doc """ List instance users """ + @spec list_users(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Page.t(User.t())} | {:error, :unauthorized} def list_users( _parent, %{email: email, page: page, limit: limit, sort: sort, direction: direction}, @@ -60,6 +62,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do @doc """ Login an user. Returns a token and the user """ + @spec login_user(any(), map(), Absinthe.Resolution.t()) :: + {:ok, map()} | {:error, :user_not_found | String.t()} def login_user(_parent, %{email: email, password: password}, %{context: context}) do with {:ok, %{ @@ -88,6 +92,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do @doc """ Refresh a token """ + @spec refresh_token(any(), map(), Absinthe.Resolution.t()) :: + {:ok, map()} | {:error, String.t()} def refresh_token(_parent, %{refresh_token: refresh_token}, _resolution) do with {:ok, user, _claims} <- Auth.Guardian.resource_from_token(refresh_token), {:ok, _old, {exchanged_token, _claims}} <- @@ -106,6 +112,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do {:error, dgettext("errors", "You need to have an existing token to get a refresh token")} end + @spec logout(any(), map(), Absinthe.Resolution.t()) :: + {:ok, String.t()} + | {:error, :token_not_found | :unable_to_logout | :unauthenticated | :invalid_argument} def logout(_parent, %{refresh_token: refresh_token}, %{context: %{current_user: %User{}}}) do with {:ok, _claims} <- Auth.Guardian.decode_and_verify(refresh_token, %{"typ" => "refresh"}), {:ok, _claims} <- Auth.Guardian.revoke(refresh_token) do @@ -134,7 +143,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do - create the user - send a validation email to the user """ - @spec create_user(any, %{email: String.t()}, any) :: tuple + @spec create_user(any, %{email: String.t()}, any) :: {:ok, User.t()} | {:error, String.t()} def create_user(_parent, %{email: email} = args, _resolution) do with :registration_ok <- check_registration_config(email), :not_deny_listed <- check_registration_denylist(email), @@ -161,7 +170,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do end end - @spec check_registration_config(String.t()) :: atom + @spec check_registration_config(String.t()) :: + :registration_ok | :registration_closed | :not_allowlisted defp check_registration_config(email) do cond do Config.instance_registrations_open?() -> @@ -523,7 +533,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do :ok <- Enum.each(actors, fn actor -> actor_performing = Keyword.get(options, :actor_performing, actor) - ActivityPub.delete(actor, actor_performing, true) + Actions.Delete.delete(actor, actor_performing, true) end), # Delete user {:ok, user} <- diff --git a/lib/graphql/resolvers/users/activity_settings.ex b/lib/graphql/resolvers/users/activity_settings.ex index f3a76e2e..df78f371 100644 --- a/lib/graphql/resolvers/users/activity_settings.ex +++ b/lib/graphql/resolvers/users/activity_settings.ex @@ -4,10 +4,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do """ alias Mobilizon.Users - alias Mobilizon.Users.User + alias Mobilizon.Users.{ActivitySetting, User} require Logger + @spec user_activity_settings(any(), map(), Absinthe.Resolution.t()) :: + {:ok, list(ActivitySetting.t())} | {:error, :unauthenticated} def user_activity_settings(_parent, _args, %{context: %{current_user: %User{} = user}}) do {:ok, Users.activity_settings_for_user(user)} end @@ -16,6 +18,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do {:error, :unauthenticated} end + @spec upsert_user_activity_setting(any(), map(), Absinthe.Resolution.t()) :: + {:ok, ActivitySetting.t()} | {:error, :unauthenticated} def upsert_user_activity_setting(_parent, args, %{context: %{current_user: %User{id: user_id}}}) do Users.create_activity_setting(Map.put(args, :user_id, user_id)) end diff --git a/lib/graphql/schema.ex b/lib/graphql/schema.ex index 4331be26..5ce92d9e 100644 --- a/lib/graphql/schema.ex +++ b/lib/graphql/schema.ex @@ -194,6 +194,7 @@ defmodule Mobilizon.GraphQL.Schema do import_fields(:discussion_subscriptions) end + @spec middleware(list(module()), any(), map()) :: list(module()) def middleware(middleware, _field, %{identifier: type}) when type in [:query, :mutation] do [CurrentActorProvider] ++ middleware ++ [ErrorHandler] end diff --git a/lib/mix/tasks/mobilizon/common.ex b/lib/mix/tasks/mobilizon/common.ex index cee4f6b8..e390c256 100644 --- a/lib/mix/tasks/mobilizon/common.ex +++ b/lib/mix/tasks/mobilizon/common.ex @@ -9,6 +9,7 @@ defmodule Mix.Tasks.Mobilizon.Common do """ require Logger + @spec start_mobilizon :: any() def start_mobilizon do if mix_task?(), do: Mix.Task.run("app.config") @@ -21,10 +22,12 @@ defmodule Mix.Tasks.Mobilizon.Common do {:ok, _} = Application.ensure_all_started(:mobilizon) end + @spec get_option(Keyword.t(), atom(), String.t(), String.t() | nil, String.t() | nil) :: any() def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do Keyword.get(options, opt) || shell_prompt(prompt, defval, defname) end + @spec shell_prompt(String.t(), String.t() | nil, String.t() | nil) :: String.t() def shell_prompt(prompt, defval \\ nil, defname \\ nil) do prompt_message = "#{prompt} [#{defname || defval}] " @@ -48,6 +51,7 @@ defmodule Mix.Tasks.Mobilizon.Common do end end + @spec shell_yes?(String.t()) :: boolean() def shell_yes?(message) do if mix_shell?(), do: Mix.shell().yes?("Continue?"), @@ -75,10 +79,13 @@ defmodule Mix.Tasks.Mobilizon.Common do end @doc "Performs a safe check whether `Mix.shell/0` is available (does not raise if Mix is not loaded)" + @spec mix_shell? :: boolean def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0) + @spec mix_task? :: boolean def mix_task?, do: :erlang.function_exported(Mix.Task, :run, 1) + @spec escape_sh_path(String.t()) :: String.t() def escape_sh_path(path) do ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') end @@ -97,6 +104,7 @@ defmodule Mix.Tasks.Mobilizon.Common do end end + @spec show_subtasks_for_module(module()) :: :ok def show_subtasks_for_module(module_name) do tasks = list_subtasks_for_module(module_name) @@ -107,7 +115,7 @@ defmodule Mix.Tasks.Mobilizon.Common do end) end - @spec list_subtasks_for_module(atom()) :: list({String.t(), String.t()}) + @spec list_subtasks_for_module(module()) :: list({String.t(), String.t()}) def list_subtasks_for_module(module_name) do Application.load(:mobilizon) {:ok, modules} = :application.get_key(:mobilizon, :modules) @@ -121,10 +129,12 @@ defmodule Mix.Tasks.Mobilizon.Common do |> Enum.map(&format_module/1) end + @spec format_module(module()) :: {String.t(), String.t() | nil} defp format_module(module) do {format_name(to_string(module)), shortdoc(module)} end + @spec format_name(String.t()) :: String.t() defp format_name("Elixir.Mix.Tasks.Mobilizon." <> task_name) do String.downcase(task_name) end diff --git a/lib/mix/tasks/mobilizon/create_bot.ex b/lib/mix/tasks/mobilizon/create_bot.ex index a01d0f59..3cc0ba13 100644 --- a/lib/mix/tasks/mobilizon/create_bot.ex +++ b/lib/mix/tasks/mobilizon/create_bot.ex @@ -13,11 +13,12 @@ defmodule Mix.Tasks.Mobilizon.CreateBot do require Logger @shortdoc "Create bot" + @spec run(list(String.t())) :: Bot.t() | :ok def run([email, name, summary, type, url]) do start_mobilizon() with {:ok, %User{} = user} <- Users.get_user_by_email(email, activated: true), - actor <- Actors.register_bot(%{name: name, summary: summary}), + {:ok, actor} <- Actors.register_bot(%{name: name, summary: summary}), {:ok, %Bot{} = bot} <- Actors.create_bot(%{ "type" => type, diff --git a/lib/mix/tasks/mobilizon/site_map.ex b/lib/mix/tasks/mobilizon/site_map.ex index d35c9499..05acd000 100644 --- a/lib/mix/tasks/mobilizon/site_map.ex +++ b/lib/mix/tasks/mobilizon/site_map.ex @@ -11,6 +11,7 @@ defmodule Mix.Tasks.Mobilizon.SiteMap do @preferred_cli_env "prod" @shortdoc "Generates a new Sitemap" + @spec run(list(String.t())) :: :ok def run(["generate"]) do start_mobilizon() diff --git a/lib/mobilizon/activities/activities.ex b/lib/mobilizon/activities/activities.ex index 7d7e7580..3989463b 100644 --- a/lib/mobilizon/activities/activities.ex +++ b/lib/mobilizon/activities/activities.ex @@ -70,6 +70,7 @@ defmodule Mobilizon.Activities do [%Activity{}, ...] """ + @spec list_activities :: list(Activity.t()) def list_activities do Repo.all(Activity) end @@ -161,6 +162,7 @@ defmodule Mobilizon.Activities do ** (Ecto.NoResultsError) """ + @spec get_activity!(integer()) :: Activity.t() def get_activity!(id), do: Repo.get!(Activity, id) @doc """ @@ -175,6 +177,7 @@ defmodule Mobilizon.Activities do {:error, %Ecto.Changeset{}} """ + @spec create_activity(map()) :: {:ok, Activity.t()} | {:error, Ecto.Changeset.t()} def create_activity(attrs \\ %{}) do %Activity{} |> Activity.changeset(attrs) @@ -186,10 +189,13 @@ defmodule Mobilizon.Activities do Repo.preload(activity, @activity_preloads) end + @spec object_types :: list(String.t()) def object_types, do: @object_type + @spec subjects :: list(String.t()) def subjects, do: @subjects + @spec activity_types :: list(String.t()) def activity_types, do: @activity_types @spec filter_object_type(Query.t(), atom() | nil) :: Query.t() diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index f4db670d..ff0df5af 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -23,6 +23,7 @@ defmodule Mobilizon.Actors.Actor do require Logger @type t :: %__MODULE__{ + id: integer(), url: String.t(), outbox_url: String.t(), inbox_url: String.t(), diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index fb903c9e..0213df66 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -299,6 +299,7 @@ defmodule Mobilizon.Actors do @delete_actor_default_options [reserve_username: true, suspension: false] + @spec delete_actor(Actor.t(), Keyword.t()) :: {:error, Ecto.Changeset.t()} | {:ok, Oban.Job.t()} def delete_actor(%Actor{} = actor, options \\ @delete_actor_default_options) do delete_actor_options = Keyword.merge(@delete_actor_default_options, options) @@ -533,7 +534,7 @@ defmodule Mobilizon.Actors do |> Repo.one() end - @spec get_actor_by_followers_url(String.t()) :: Actor.t() + @spec get_actor_by_followers_url(String.t()) :: Actor.t() | nil def get_actor_by_followers_url(followers_url) do Actor |> where([q], q.followers_url == ^followers_url) diff --git a/lib/mobilizon/actors/member.ex b/lib/mobilizon/actors/member.ex index 34703c76..42436147 100644 --- a/lib/mobilizon/actors/member.ex +++ b/lib/mobilizon/actors/member.ex @@ -12,6 +12,8 @@ defmodule Mobilizon.Actors.Member do alias Mobilizon.Web.Endpoint @type t :: %__MODULE__{ + id: String.t(), + url: String.t(), role: MemberRole.t(), parent: Actor.t(), actor: Actor.t(), diff --git a/lib/mobilizon/addresses/address.ex b/lib/mobilizon/addresses/address.ex index 7ccad919..edd02ac8 100644 --- a/lib/mobilizon/addresses/address.ex +++ b/lib/mobilizon/addresses/address.ex @@ -73,6 +73,7 @@ defmodule Mobilizon.Addresses.Address do put_change(changeset, :url, url) end + @spec coords(nil | t) :: nil | {float, float} def coords(nil), do: nil def coords(%__MODULE__{} = address) do @@ -81,6 +82,7 @@ defmodule Mobilizon.Addresses.Address do end end + @spec representation(nil | t) :: nil | String.t() def representation(nil), do: nil def representation(%__MODULE__{} = address) do diff --git a/lib/mobilizon/cli.ex b/lib/mobilizon/cli.ex index 91d105d3..56016063 100644 --- a/lib/mobilizon/cli.ex +++ b/lib/mobilizon/cli.ex @@ -9,6 +9,7 @@ defmodule Mobilizon.CLI do """ alias Mix.Tasks.Mobilizon.Ecto.{Migrate, Rollback} + @spec run(String.t()) :: any() def run(args) do [task | args] = String.split(args) @@ -43,11 +44,13 @@ defmodule Mobilizon.CLI do end end - def migrate(args) do + @spec migrate(String.t()) :: any() + defp migrate(args) do Migrate.run(args) end - def rollback(args) do + @spec rollback(String.t()) :: any() + defp rollback(args) do Rollback.run(args) end end diff --git a/lib/mobilizon/config.ex b/lib/mobilizon/config.ex index 9a26ae4d..0f51d151 100644 --- a/lib/mobilizon/config.ex +++ b/lib/mobilizon/config.ex @@ -302,9 +302,9 @@ defmodule Mobilizon.Config do def instance_event_creation_enabled?, do: :mobilizon |> Application.get_env(:events) |> Keyword.get(:creation) - @spec anonymous_actor_id :: binary | integer + @spec anonymous_actor_id :: integer def anonymous_actor_id, do: get_cached_value(:anonymous_actor_id) - @spec relay_actor_id :: binary | integer + @spec relay_actor_id :: integer def relay_actor_id, do: get_cached_value(:relay_actor_id) @spec admin_settings :: map def admin_settings, do: get_cached_value(:admin_config) diff --git a/lib/mobilizon/discussions/comment.ex b/lib/mobilizon/discussions/comment.ex index eed8f7ac..6ae10d27 100644 --- a/lib/mobilizon/discussions/comment.ex +++ b/lib/mobilizon/discussions/comment.ex @@ -20,6 +20,7 @@ defmodule Mobilizon.Discussions.Comment do @type t :: %__MODULE__{ text: String.t(), url: String.t(), + id: integer(), local: boolean, visibility: CommentVisibility.t(), uuid: Ecto.UUID.t(), diff --git a/lib/mobilizon/discussions/discussion.ex b/lib/mobilizon/discussions/discussion.ex index c6c98fab..5d5f513e 100644 --- a/lib/mobilizon/discussions/discussion.ex +++ b/lib/mobilizon/discussions/discussion.ex @@ -4,6 +4,7 @@ defmodule Mobilizon.Discussions.Discussion.TitleSlug do """ use EctoAutoslugField.Slug, from: [:title, :id], to: :slug + @spec build_slug([String.t()], Ecto.Changeset.t()) :: String.t() def build_slug([title, id], %Ecto.Changeset{valid?: true}) do [title, ShortUUID.encode!(id)] |> Enum.join("-") @@ -31,6 +32,7 @@ defmodule Mobilizon.Discussions.Discussion do import Mobilizon.Web.Gettext, only: [dgettext: 2] @type t :: %__MODULE__{ + id: String.t(), creator: Actor.t(), actor: Actor.t(), title: String.t(), diff --git a/lib/mobilizon/discussions/discussions.ex b/lib/mobilizon/discussions/discussions.ex index 9dccca78..cc4d1581 100644 --- a/lib/mobilizon/discussions/discussions.ex +++ b/lib/mobilizon/discussions/discussions.ex @@ -377,8 +377,8 @@ defmodule Mobilizon.Discussions do @doc """ Creates a discussion. """ - @spec create_discussion(map) :: {:ok, Comment.t()} | {:error, Changeset.t()} - def create_discussion(attrs \\ %{}) do + @spec create_discussion(map()) :: {:ok, Discussion.t()} | {:error, atom(), Changeset.t(), map()} + def create_discussion(attrs) do with {:ok, %{comment: %Comment{} = _comment, discussion: %Discussion{} = discussion}} <- Multi.new() |> Multi.insert( @@ -412,19 +412,26 @@ defmodule Mobilizon.Discussions do @doc """ Create a response to a discussion """ - @spec reply_to_discussion(Discussion.t(), map()) :: {:ok, Discussion.t()} + @spec reply_to_discussion(Discussion.t(), map()) :: + {:ok, Discussion.t()} | {:error, atom(), Ecto.Changeset.t(), map()} def reply_to_discussion(%Discussion{id: discussion_id} = discussion, attrs \\ %{}) do + attrs = + Map.merge(attrs, %{ + discussion_id: discussion_id, + actor_id: Map.get(attrs, :creator_id, Map.get(attrs, :actor_id)) + }) + + changeset = + Comment.changeset( + %Comment{}, + attrs + ) + with {:ok, %{comment: %Comment{} = comment, discussion: %Discussion{} = discussion}} <- Multi.new() |> Multi.insert( :comment, - Comment.changeset( - %Comment{}, - Map.merge(attrs, %{ - discussion_id: discussion_id, - actor_id: Map.get(attrs, :creator_id, attrs.actor_id) - }) - ) + changeset ) |> Multi.update(:discussion, fn %{comment: %Comment{id: comment_id}} -> Discussion.changeset( @@ -435,7 +442,7 @@ defmodule Mobilizon.Discussions do |> Repo.transaction(), # Discussion is not updated %Comment{} = comment <- Repo.preload(comment, @comment_preloads) do - {:ok, Map.put(discussion, :last_comment, comment)} + {:ok, %Discussion{discussion | last_comment: comment}} end end @@ -453,7 +460,8 @@ defmodule Mobilizon.Discussions do @doc """ Delete a discussion. """ - @spec delete_discussion(Discussion.t()) :: {:ok, Discussion.t()} | {:error, Changeset.t()} + @spec delete_discussion(Discussion.t()) :: + {:ok, %{comments: {integer() | nil, any()}}} | {:error, :comments, Changeset.t(), map()} def delete_discussion(%Discussion{id: discussion_id}) do Multi.new() |> Multi.delete_all(:comments, fn _ -> @@ -463,7 +471,7 @@ defmodule Mobilizon.Discussions do |> Repo.transaction() end - @spec public_comments_for_actor_query(String.t() | integer()) :: [Comment.t()] + @spec public_comments_for_actor_query(String.t() | integer()) :: Ecto.Query.t() defp public_comments_for_actor_query(actor_id) do Comment |> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility) @@ -471,7 +479,7 @@ defmodule Mobilizon.Discussions do |> preload_for_comment() end - @spec public_replies_for_thread_query(String.t() | integer()) :: [Comment.t()] + @spec public_replies_for_thread_query(String.t() | integer()) :: Ecto.Query.t() defp public_replies_for_thread_query(comment_id) do Comment |> where([c], c.origin_comment_id == ^comment_id and c.visibility in ^@public_visibility) diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index 27eee288..6c726cb7 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -35,7 +35,7 @@ defmodule Mobilizon.Events.Event do alias Mobilizon.Web.Router.Helpers, as: Routes @type t :: %__MODULE__{ - id: String.t(), + id: integer(), url: String.t(), local: boolean, begins_on: DateTime.t(), @@ -47,16 +47,16 @@ defmodule Mobilizon.Events.Event do draft: boolean, visibility: EventVisibility.t(), join_options: JoinOptions.t(), - publish_at: DateTime.t(), + publish_at: DateTime.t() | nil, uuid: Ecto.UUID.t(), - online_address: String.t(), + online_address: String.t() | nil, phone_address: String.t(), category: String.t(), options: EventOptions.t(), organizer_actor: Actor.t(), attributed_to: Actor.t() | nil, - physical_address: Address.t(), - picture: Media.t(), + physical_address: Address.t() | nil, + picture: Media.t() | nil, media: [Media.t()], tracks: [Track.t()], sessions: [Session.t()], diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 2e7d9a70..f543d12b 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -282,7 +282,7 @@ defmodule Mobilizon.Events do # We start by inserting the event and then insert a first participant if the event is not a draft @spec do_create_event(map) :: - {:ok, Event.t()} + {:ok, %{insert: Event.t(), write: Participant.t() | nil}} | {:error, Changeset.t()} | {:error, :update | :write, Changeset.t(), map()} defp do_create_event(attrs) do @@ -368,7 +368,7 @@ defmodule Mobilizon.Events do Deletes an event. Raises an exception if it fails. """ - @spec delete_event(Event.t()) :: Event.t() + @spec delete_event!(Event.t()) :: Event.t() def delete_event!(%Event{} = event), do: Repo.delete!(event) @doc """ @@ -457,6 +457,7 @@ defmodule Mobilizon.Events do @spec list_organized_events_for_group( Actor.t(), + EventVisibility.t(), DateTime.t() | nil, DateTime.t() | nil, integer | nil, diff --git a/lib/mobilizon/events/participant.ex b/lib/mobilizon/events/participant.ex index c5c162cb..a6eb24f9 100644 --- a/lib/mobilizon/events/participant.ex +++ b/lib/mobilizon/events/participant.ex @@ -15,6 +15,7 @@ defmodule Mobilizon.Events.Participant do alias Mobilizon.Web.Endpoint @type t :: %__MODULE__{ + id: String.t(), role: ParticipantRole.t(), url: String.t(), event: Event.t(), diff --git a/lib/mobilizon/posts/post.ex b/lib/mobilizon/posts/post.ex index bbf43586..a1969371 100644 --- a/lib/mobilizon/posts/post.ex +++ b/lib/mobilizon/posts/post.ex @@ -4,6 +4,7 @@ defmodule Mobilizon.Posts.Post.TitleSlug do """ use EctoAutoslugField.Slug, from: [:title, :id], to: :slug + @spec build_slug([String.t()], any()) :: String.t() | nil def build_slug([title, id], _changeset) do [title, ShortUUID.encode!(id)] |> Enum.join("-") @@ -31,6 +32,7 @@ defmodule Mobilizon.Posts.Post do import Mobilizon.Web.Gettext @type t :: %__MODULE__{ + id: String.t(), url: String.t(), local: boolean, slug: String.t(), diff --git a/lib/mobilizon/reports/report.ex b/lib/mobilizon/reports/report.ex index b9c449a5..4d958cba 100644 --- a/lib/mobilizon/reports/report.ex +++ b/lib/mobilizon/reports/report.ex @@ -15,6 +15,7 @@ defmodule Mobilizon.Reports.Report do alias Mobilizon.Web.Endpoint @type t :: %__MODULE__{ + id: integer(), content: String.t(), status: ReportStatus.t(), url: String.t(), diff --git a/lib/mobilizon/resources/resource.ex b/lib/mobilizon/resources/resource.ex index 83a84bfc..6fdb13e7 100644 --- a/lib/mobilizon/resources/resource.ex +++ b/lib/mobilizon/resources/resource.ex @@ -13,6 +13,7 @@ defmodule Mobilizon.Resources.Resource do alias Mobilizon.Resources.Resource.Metadata @type t :: %__MODULE__{ + id: String.t(), title: String.t(), summary: String.t(), url: String.t(), diff --git a/lib/mobilizon/storage/repo.ex b/lib/mobilizon/storage/repo.ex index 3b209a3c..67796d3d 100644 --- a/lib/mobilizon/storage/repo.ex +++ b/lib/mobilizon/storage/repo.ex @@ -10,6 +10,7 @@ defmodule Mobilizon.Storage.Repo do @doc """ Dynamically loads the repository url from the DATABASE_URL environment variable. """ + @spec init(any(), any()) :: any() def init(_, opts) do {:ok, opts} end diff --git a/lib/mobilizon/todos/todo.ex b/lib/mobilizon/todos/todo.ex index fbc4be92..50dee500 100644 --- a/lib/mobilizon/todos/todo.ex +++ b/lib/mobilizon/todos/todo.ex @@ -10,6 +10,8 @@ defmodule Mobilizon.Todos.Todo do alias Mobilizon.Todos.TodoList @type t :: %__MODULE__{ + id: String.t(), + url: String.t(), status: boolean(), title: String.t(), due_date: DateTime.t(), diff --git a/lib/mobilizon/todos/todo_list.ex b/lib/mobilizon/todos/todo_list.ex index 9f27ac28..49a406b1 100644 --- a/lib/mobilizon/todos/todo_list.ex +++ b/lib/mobilizon/todos/todo_list.ex @@ -10,6 +10,8 @@ defmodule Mobilizon.Todos.TodoList do alias Mobilizon.Todos.Todo @type t :: %__MODULE__{ + id: String.t(), + url: String.t(), title: String.t(), todos: [Todo.t()], actor: Actor.t(), diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex index 7a8d2fd2..e54beea3 100644 --- a/lib/mobilizon/users/users.ex +++ b/lib/mobilizon/users/users.ex @@ -107,7 +107,7 @@ defmodule Mobilizon.Users do @doc """ Get an user by its activation token. """ - @spec get_user_by_activation_token(String.t()) :: Actor.t() | nil + @spec get_user_by_activation_token(String.t()) :: User.t() | nil def get_user_by_activation_token(token) do token |> user_by_activation_token_query() @@ -117,7 +117,7 @@ defmodule Mobilizon.Users do @doc """ Get an user by its reset password token. """ - @spec get_user_by_reset_password_token(String.t()) :: Actor.t() | nil + @spec get_user_by_reset_password_token(String.t()) :: User.t() | nil def get_user_by_reset_password_token(token) do token |> user_by_reset_password_token_query() diff --git a/lib/service/activity/member.ex b/lib/service/activity/member.ex index b0e7d997..704a55e6 100644 --- a/lib/service/activity/member.ex +++ b/lib/service/activity/member.ex @@ -40,7 +40,7 @@ defmodule Mobilizon.Service.Activity.Member do Actors.get_member(member_id) end - @spec get_author(Member.t(), Member.t() | nil) :: String.t() | integer() + @spec get_author(Member.t(), Member.t() | nil) :: integer() defp get_author(%Member{actor_id: actor_id}, options) do moderator = Keyword.get(options, :moderator) diff --git a/lib/service/error_page.ex b/lib/service/error_page.ex index bca368c9..de8b224a 100644 --- a/lib/service/error_page.ex +++ b/lib/service/error_page.ex @@ -3,10 +3,12 @@ defmodule Mobilizon.Service.ErrorPage do Render an error page """ + @spec init :: :ok | {:error, File.posix()} def init do render_error_page() end + @spec render_error_page :: :ok | {:error, File.posix()} defp render_error_page do content = Phoenix.View.render_to_string(Mobilizon.Web.ErrorView, "500.html", conn: %Plug.Conn{}) diff --git a/lib/service/formatter/formatter.ex b/lib/service/formatter/formatter.ex index 0249d761..e30b1a99 100644 --- a/lib/service/formatter/formatter.ex +++ b/lib/service/formatter/formatter.ex @@ -65,6 +65,7 @@ defmodule Mobilizon.Service.Formatter do end end + @spec hashtag_handler(String.t(), String.t(), any(), map()) :: {String.t(), map()} def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do tag = String.downcase(tag) url = "#{Endpoint.url()}/tag/#{tag}" @@ -100,6 +101,7 @@ defmodule Mobilizon.Service.Formatter do @doc """ Escapes a special characters in mention names. """ + @spec mentions_escape(String.t(), Keyword.t()) :: String.t() def mentions_escape(text, options \\ []) do options = Keyword.merge(options, @@ -111,6 +113,11 @@ defmodule Mobilizon.Service.Formatter do Linkify.link(text, options) end + @spec html_escape( + {text :: String.t(), mentions :: list(), hashtags :: list()}, + type :: String.t() + ) :: {String.t(), list(), list()} + @spec html_escape(text :: String.t(), type :: String.t()) :: String.t() def html_escape({text, mentions, hashtags}, type) do {html_escape(text, type), mentions, hashtags} end @@ -131,6 +138,7 @@ defmodule Mobilizon.Service.Formatter do |> Enum.join("") end + @spec truncate(String.t(), non_neg_integer(), String.t()) :: String.t() def truncate(text, max_length \\ 200, omission \\ "...") do # Remove trailing whitespace text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}") @@ -143,6 +151,7 @@ defmodule Mobilizon.Service.Formatter do end end + @spec linkify_opts :: Keyword.t() defp linkify_opts do Mobilizon.Config.get(__MODULE__) ++ [ @@ -186,5 +195,6 @@ defmodule Mobilizon.Service.Formatter do |> (&" #{&1}").() end + @spec tag_text_strip(String.t()) :: String.t() defp tag_text_strip(tag), do: tag |> String.trim("#") |> String.downcase() end diff --git a/lib/service/http/activity_pub.ex b/lib/service/http/activity_pub.ex index db9683d9..57e4ea83 100644 --- a/lib/service/http/activity_pub.ex +++ b/lib/service/http/activity_pub.ex @@ -9,6 +9,7 @@ defmodule Mobilizon.Service.HTTP.ActivityPub do recv_timeout: 20_000 ] + @spec client(Keyword.t()) :: Tesla.Client.t() def client(options \\ []) do headers = Keyword.get(options, :headers, []) adapter = Application.get_env(:tesla, __MODULE__, [])[:adapter] || Tesla.Adapter.Hackney @@ -27,10 +28,12 @@ defmodule Mobilizon.Service.HTTP.ActivityPub do Tesla.client(middleware, {adapter, opts}) end + @spec get(Tesla.Client.t(), String.t()) :: Tesla.Env.t() def get(client, url) do Tesla.get(client, url) end + @spec post(Tesla.Client.t(), String.t(), map() | String.t()) :: Tesla.Env.t() def post(client, url, data) do Tesla.post(client, url, data) end diff --git a/lib/service/notifications/scheduler.ex b/lib/service/notifications/scheduler.ex index f063a596..5f942ff1 100644 --- a/lib/service/notifications/scheduler.ex +++ b/lib/service/notifications/scheduler.ex @@ -19,7 +19,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do require Logger - @spec trigger_notifications_for_participant(Participant.t()) :: {:ok, nil} + @spec trigger_notifications_for_participant(Participant.t()) :: {:ok, Oban.Job.t() | nil} def trigger_notifications_for_participant(%Participant{} = participant) do before_event_notification(participant) on_day_notification(participant) @@ -27,6 +27,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do {:ok, nil} end + @spec before_event_notification(Participant.t()) :: {:ok, nil} def before_event_notification(%Participant{ id: participant_id, event: %Event{begins_on: begins_on}, @@ -46,6 +47,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do def before_event_notification(_), do: {:ok, nil} + @spec on_day_notification(Participant.t()) :: {:ok, Oban.Job.t() | nil | String.t()} def on_day_notification(%Participant{ event: %Event{begins_on: begins_on}, actor: %Actor{user_id: user_id} @@ -90,6 +92,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do def on_day_notification(_), do: {:ok, nil} + @spec weekly_notification(Participant.t()) :: {:ok, Oban.Job.t() | nil | String.t()} def weekly_notification(%Participant{ event: %Event{begins_on: begins_on}, actor: %Actor{user_id: user_id} @@ -144,6 +147,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do def weekly_notification(_), do: {:ok, nil} + @spec pending_participation_notification(Event.t(), Keyword.t()) :: {:ok, Oban.Job.t() | nil} def pending_participation_notification(event, options \\ []) def pending_participation_notification( diff --git a/lib/service/notifier/notifier.ex b/lib/service/notifier/notifier.ex index 9c9140ae..a4d9fd9a 100644 --- a/lib/service/notifier/notifier.ex +++ b/lib/service/notifier/notifier.ex @@ -18,6 +18,7 @@ defmodule Mobilizon.Service.Notifier do @callback send(User.t(), list(Activity.t()), Keyword.t()) :: {:ok, any()} | {:error, String.t()} + @spec notify(User.t(), Activity.t(), Keyword.t()) :: :ok def notify(%User{} = user, %Activity{} = activity, opts \\ []) do Enum.each(providers(opts), & &1.send(user, activity, opts)) end diff --git a/lib/service/rich_media/parsers/meta_tags_parser.ex b/lib/service/rich_media/parsers/meta_tags_parser.ex index 39f81732..e552ddd9 100644 --- a/lib/service/rich_media/parsers/meta_tags_parser.ex +++ b/lib/service/rich_media/parsers/meta_tags_parser.ex @@ -8,6 +8,8 @@ defmodule Mobilizon.Service.RichMedia.Parsers.MetaTagsParser do Module to parse meta tags data in HTML pages """ + @spec parse(String.t(), map(), String.t(), String.t(), atom(), atom(), list(atom())) :: + {:ok, map()} | {:error, String.t()} def parse( html, data, @@ -35,10 +37,12 @@ defmodule Mobilizon.Service.RichMedia.Parsers.MetaTagsParser do end end + @spec get_elements(String.t(), atom(), String.t()) :: Floki.html_tree() defp get_elements(html, key_name, prefix) do html |> Floki.parse_document!() |> Floki.find("meta[#{to_string(key_name)}^='#{prefix}:']") end + @spec normalize_attributes(Floki.html_node(), String.t(), atom(), atom(), list(atom())) :: map() defp normalize_attributes(html_node, prefix, key_name, value_name, allowed_attributes) do {_tag, attributes, _children} = html_node @@ -55,6 +59,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.MetaTagsParser do end end + @spec maybe_put_title(map(), String.t()) :: map() defp maybe_put_title(%{title: _} = meta, _), do: meta defp maybe_put_title(meta, html) when meta != %{} do @@ -66,6 +71,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.MetaTagsParser do defp maybe_put_title(meta, _), do: meta + @spec maybe_put_description(map(), String.t()) :: map() defp maybe_put_description(%{description: _} = meta, _), do: meta defp maybe_put_description(meta, html) when meta != %{} do diff --git a/lib/service/rich_media/parsers/oembed_parser.ex b/lib/service/rich_media/parsers/oembed_parser.ex index 56727747..bebfdec5 100644 --- a/lib/service/rich_media/parsers/oembed_parser.ex +++ b/lib/service/rich_media/parsers/oembed_parser.ex @@ -15,6 +15,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OEmbed do ssl: [{:versions, [:"tlsv1.2"]}] ] + @spec parse(String.t(), map()) :: {:ok, map()} | {:error, String.t()} def parse(html, _data) do Logger.debug("Using OEmbed parser") diff --git a/lib/service/rich_media/parsers/ogp.ex b/lib/service/rich_media/parsers/ogp.ex index 531f79bf..07c89619 100644 --- a/lib/service/rich_media/parsers/ogp.ex +++ b/lib/service/rich_media/parsers/ogp.ex @@ -30,6 +30,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OGP do :"image:alt" ] + @spec parse(String.t(), map()) :: {:ok, map()} def parse(html, data) do Logger.debug("Using OpenGraph card parser") @@ -49,6 +50,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OGP do end end + @spec transform_tags(map()) :: map() defp transform_tags(data) do data |> Enum.reject(fn {_, v} -> is_nil(v) end) diff --git a/lib/service/site_map.ex b/lib/service/site_map.ex index b17ccc75..d25820f0 100644 --- a/lib/service/site_map.ex +++ b/lib/service/site_map.ex @@ -10,6 +10,7 @@ defmodule Mobilizon.Service.SiteMap do @default_static_frequency :monthly + @spec generate_sitemap :: {:ok, :ok} def generate_sitemap do static_routes = [ {Routes.page_url(Endpoint, :index, []), :daily}, @@ -60,6 +61,7 @@ defmodule Mobilizon.Service.SiteMap do end # Sometimes we use naive datetimes + @spec check_date_time(any()) :: DateTime.t() | nil defp check_date_time(%NaiveDateTime{} = datetime), do: DateTime.from_naive!(datetime, "Etc/UTC") defp check_date_time(%DateTime{} = datetime), do: datetime defp check_date_time(_), do: nil diff --git a/lib/service/statistics/statistics.ex b/lib/service/statistics/statistics.ex index 99494726..6d3e2136 100644 --- a/lib/service/statistics/statistics.ex +++ b/lib/service/statistics/statistics.ex @@ -6,6 +6,7 @@ defmodule Mobilizon.Service.Statistics do alias Mobilizon.{Actors, Discussions, Events, Users} alias Mobilizon.Federation.ActivityPub.Relay + @spec get_cached_value(String.t()) :: any() | nil def get_cached_value(key) do case Cachex.fetch(:statistics, key, fn key -> case create_cache(key) do diff --git a/lib/web/auth/error_handler.ex b/lib/web/auth/error_handler.ex index edf760bb..8a27a6e5 100644 --- a/lib/web/auth/error_handler.ex +++ b/lib/web/auth/error_handler.ex @@ -5,6 +5,7 @@ defmodule Mobilizon.Web.Auth.ErrorHandler do import Plug.Conn # sobelow_skip ["XSS.SendResp"] + @spec auth_error(Plug.Conn.t(), any(), any()) :: Plug.Conn.t() def auth_error(conn, {type, _reason}, _opts) do body = Jason.encode!(%{message: to_string(type)}) send_resp(conn, 401, body) diff --git a/lib/web/auth/guardian.ex b/lib/web/auth/guardian.ex index da1f23c0..9dd50ee6 100644 --- a/lib/web/auth/guardian.ex +++ b/lib/web/auth/guardian.ex @@ -15,6 +15,7 @@ defmodule Mobilizon.Web.Auth.Guardian do require Logger + @spec subject_for_token(any(), any()) :: {:ok, String.t()} | {:error, :unknown_resource} def subject_for_token(%User{} = user, _claims) do {:ok, "User:" <> to_string(user.id)} end @@ -45,30 +46,35 @@ defmodule Mobilizon.Web.Auth.Guardian do {:error, :no_claims} end + @spec after_encode_and_sign(any(), any(), any(), any()) :: {:ok, String.t()} def after_encode_and_sign(resource, claims, token, _options) do with {:ok, _} <- Guardian.DB.after_encode_and_sign(resource, claims["typ"], claims, token) do {:ok, token} end end + @spec on_verify(any(), any(), any()) :: {:ok, any()} def on_verify(claims, token, _options) do with {:ok, _} <- Guardian.DB.on_verify(claims, token) do {:ok, claims} end end + @spec on_revoke(any(), any(), any()) :: {:ok, any()} def on_revoke(claims, token, _options) do with {:ok, _} <- Guardian.DB.on_revoke(claims, token) do {:ok, claims} end end + @spec on_refresh({any(), any()}, {any(), any()}, any()) :: {:ok, {any(), any()}, {any(), any()}} def on_refresh({old_token, old_claims}, {new_token, new_claims}, _options) do with {:ok, _, _} <- Guardian.DB.on_refresh({old_token, old_claims}, {new_token, new_claims}) do {:ok, {old_token, old_claims}, {new_token, new_claims}} end end + @spec on_exchange(any(), any(), any()) :: {:ok, {any(), any()}, {any(), any()}} def on_exchange(old_stuff, new_stuff, options), do: on_refresh(old_stuff, new_stuff, options) # def build_claims(claims, _resource, opts) do diff --git a/lib/web/controllers/auth_controller.ex b/lib/web/controllers/auth_controller.ex index e841ae7e..49a12b5a 100644 --- a/lib/web/controllers/auth_controller.ex +++ b/lib/web/controllers/auth_controller.ex @@ -9,6 +9,7 @@ defmodule Mobilizon.Web.AuthController do plug(Ueberauth) + @spec request(Plug.Conn.t(), map()) :: Plug.Conn.t() def request(conn, %{"provider" => provider_name} = _params) do case provider_config(provider_name) do {:ok, provider_config} -> @@ -20,6 +21,7 @@ defmodule Mobilizon.Web.AuthController do end end + @spec callback(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, any()} def callback( %{assigns: %{ueberauth_failure: fails}} = conn, %{"provider" => provider} = _params @@ -85,6 +87,7 @@ defmodule Mobilizon.Web.AuthController do # Github only give public emails as part of the user profile, # so we explicitely request all user emails and filter on the primary one + @spec email_from_ueberauth(Ueberauth.Auth.t()) :: String.t() | nil defp email_from_ueberauth(%Ueberauth.Auth{ strategy: Ueberauth.Strategy.Github, extra: %Ueberauth.Auth.Extra{raw_info: %{user: %{"emails" => emails}}} @@ -100,6 +103,7 @@ defmodule Mobilizon.Web.AuthController do defp email_from_ueberauth(_), do: nil + @spec provider_config(String.t()) :: {:ok, any()} | {:error, :not_supported | :unknown_error} defp provider_config(provider_name) do with ueberauth when is_list(ueberauth) <- Application.get_env(:ueberauth, Ueberauth), providers when is_list(providers) <- Keyword.get(ueberauth, :providers), diff --git a/lib/web/controllers/fallback_controller.ex b/lib/web/controllers/fallback_controller.ex index f92fb231..879d25a8 100644 --- a/lib/web/controllers/fallback_controller.ex +++ b/lib/web/controllers/fallback_controller.ex @@ -6,6 +6,7 @@ defmodule Mobilizon.Web.FallbackController do """ use Mobilizon.Web, :controller + @spec call(Plug.Conn.t(), {:error, :not_found}) :: Plug.Conn.t() def call(conn, {:error, :not_found}) do conn |> put_status(:not_found) diff --git a/lib/web/controllers/feed_controller.ex b/lib/web/controllers/feed_controller.ex index 4db698fe..cf464626 100644 --- a/lib/web/controllers/feed_controller.ex +++ b/lib/web/controllers/feed_controller.ex @@ -7,6 +7,7 @@ defmodule Mobilizon.Web.FeedController do action_fallback(Mobilizon.Web.FallbackController) alias Mobilizon.Config + @spec instance(Plug.Conn.t(), map()) :: Plug.Conn.t() def instance(conn, %{"format" => format}) do if Config.get([:instance, :enable_instance_feeds], false) do return_data(conn, format, "instance", Config.instance_name()) @@ -15,6 +16,7 @@ defmodule Mobilizon.Web.FeedController do end end + @spec actor(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found} def actor(conn, %{"format" => format, "name" => name}) do return_data(conn, format, "actor_" <> name, name) end @@ -23,14 +25,17 @@ defmodule Mobilizon.Web.FeedController do {:error, :not_found} end + @spec event(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found} def event(conn, %{"uuid" => uuid, "format" => "ics"}) do return_data(conn, "ics", "event_" <> uuid, "event") end + @spec instance(Plug.Conn.t(), map()) :: Plug.Conn.t() def event(_conn, _) do {:error, :not_found} end + @spec going(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found} def going(conn, %{"token" => token, "format" => format}) do return_data(conn, format, "token_" <> token, "events") end @@ -39,6 +44,8 @@ defmodule Mobilizon.Web.FeedController do {:error, :not_found} end + @spec return_data(Plug.Conn.t(), String.t(), String.t(), String.t()) :: + Plug.Conn.t() | {:error, :not_found} defp return_data(conn, "atom", type, filename) do case Cachex.fetch(:feed, type) do {status, data} when status in [:commit, :ok] -> diff --git a/lib/web/controllers/media_proxy_controller.ex b/lib/web/controllers/media_proxy_controller.ex index b60affac..71f95c42 100644 --- a/lib/web/controllers/media_proxy_controller.ex +++ b/lib/web/controllers/media_proxy_controller.ex @@ -10,6 +10,7 @@ defmodule Mobilizon.Web.MediaProxyController do alias Plug.Conn # sobelow_skip ["XSS.SendResp"] + @spec remote(Plug.Conn.t(), map()) :: Plug.Conn.t() def remote(conn, %{"sig" => sig64, "url" => url64}) do with {_, true} <- {:enabled, MediaProxy.enabled?()}, {:ok, url} <- MediaProxy.decode_url(sig64, url64), @@ -27,6 +28,7 @@ defmodule Mobilizon.Web.MediaProxyController do end end + @spec media_proxy_opts :: Keyword.t() defp media_proxy_opts do Config.get([:media_proxy, :proxy_opts], []) end diff --git a/lib/web/controllers/node_info_controller.ex b/lib/web/controllers/node_info_controller.ex index 67cd636c..e5165e33 100644 --- a/lib/web/controllers/node_info_controller.ex +++ b/lib/web/controllers/node_info_controller.ex @@ -15,6 +15,7 @@ defmodule Mobilizon.Web.NodeInfoController do @node_info_supported_versions ["2.0", "2.1"] @node_info_schema_uri "http://nodeinfo.diaspora.software/ns/schema/" + @spec schemas(Plug.Conn.t(), any) :: Plug.Conn.t() def schemas(conn, _params) do links = @node_info_supported_versions @@ -31,6 +32,7 @@ defmodule Mobilizon.Web.NodeInfoController do end # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.1/schema.json + @spec nodeinfo(Plug.Conn.t(), any()) :: Plug.Conn.t() def nodeinfo(conn, %{"version" => version}) when version in @node_info_supported_versions do response = %{ version: version, diff --git a/lib/web/controllers/page_controller.ex b/lib/web/controllers/page_controller.ex index eaab4b59..cc00560a 100644 --- a/lib/web/controllers/page_controller.ex +++ b/lib/web/controllers/page_controller.ex @@ -14,15 +14,26 @@ defmodule Mobilizon.Web.PageController do plug(:put_layout, false) action_fallback(Mobilizon.Web.FallbackController) + @spec my_events(Plug.Conn.t(), any) :: Plug.Conn.t() defdelegate my_events(conn, params), to: PageController, as: :index + @spec create_event(Plug.Conn.t(), any) :: Plug.Conn.t() defdelegate create_event(conn, params), to: PageController, as: :index + @spec list_events(Plug.Conn.t(), any) :: Plug.Conn.t() defdelegate list_events(conn, params), to: PageController, as: :index + @spec edit_event(Plug.Conn.t(), any) :: Plug.Conn.t() defdelegate edit_event(conn, params), to: PageController, as: :index + @spec moderation_report(Plug.Conn.t(), any) :: Plug.Conn.t() defdelegate moderation_report(conn, params), to: PageController, as: :index + @spec participation_email_confirmation(Plug.Conn.t(), any) :: Plug.Conn.t() defdelegate participation_email_confirmation(conn, params), to: PageController, as: :index + @spec user_email_validation(Plug.Conn.t(), any) :: Plug.Conn.t() defdelegate user_email_validation(conn, params), to: PageController, as: :index + @spec my_groups(Plug.Conn.t(), any) :: Plug.Conn.t() defdelegate my_groups(conn, params), to: PageController, as: :index + @typep object_type :: + :actor | :event | :comment | :resource | :post | :discussion | :todo_list | :todo + @spec index(Plug.Conn.t(), any) :: Plug.Conn.t() def index(conn, _params), do: render(conn, :index) @@ -62,38 +73,45 @@ defmodule Mobilizon.Web.PageController do render_or_error(conn, &checks?/3, status, :discussion, discussion) end - def resources(conn, %{"name" => _name}) do - handle_collection_route(conn, :resources) - end - - def posts(conn, %{"name" => _name}) do - handle_collection_route(conn, :posts) - end - - def discussions(conn, %{"name" => _name}) do - handle_collection_route(conn, :discussions) - end - - def events(conn, %{"name" => _name}) do - handle_collection_route(conn, :events) - end - - def todos(conn, %{"name" => _name}) do - handle_collection_route(conn, :todos) - end - - @spec todo_list(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t() + @spec todo_list(Plug.Conn.t(), map) :: Plug.Conn.t() | {:error, :not_found} def todo_list(conn, %{"uuid" => uuid}) do {status, todo_list} = Cache.get_todo_list_by_uuid_with_preload(uuid) render_or_error(conn, &checks?/3, status, :todo_list, todo_list) end - @spec todo(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t() + @spec todo(Plug.Conn.t(), map) :: Plug.Conn.t() | {:error, :not_found} def todo(conn, %{"uuid" => uuid}) do {status, todo} = Cache.get_todo_by_uuid_with_preload(uuid) render_or_error(conn, &checks?/3, status, :todo, todo) end + @typep collections :: :resources | :posts | :discussions | :events | :todos + + @spec resources(Plug.Conn.t(), map()) :: Plug.Conn.t() + def resources(conn, %{"name" => _name}) do + handle_collection_route(conn, :resources) + end + + @spec posts(Plug.Conn.t(), map()) :: Plug.Conn.t() + def posts(conn, %{"name" => _name}) do + handle_collection_route(conn, :posts) + end + + @spec discussions(Plug.Conn.t(), map()) :: Plug.Conn.t() + def discussions(conn, %{"name" => _name}) do + handle_collection_route(conn, :discussions) + end + + @spec events(Plug.Conn.t(), map()) :: Plug.Conn.t() + def events(conn, %{"name" => _name}) do + handle_collection_route(conn, :events) + end + + @spec todos(Plug.Conn.t(), map()) :: Plug.Conn.t() + def todos(conn, %{"name" => _name}) do + handle_collection_route(conn, :todos) + end + @spec interact(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found} def interact(conn, %{"uri" => uri}) do case ActivityPub.fetch_object_from_url(uri) do @@ -103,6 +121,7 @@ defmodule Mobilizon.Web.PageController do end end + @spec handle_collection_route(Plug.Conn.t(), collections()) :: Plug.Conn.t() defp handle_collection_route(conn, collection) do case get_format(conn) do "html" -> @@ -113,6 +132,8 @@ defmodule Mobilizon.Web.PageController do end end + @spec render_or_error(Plug.Conn.t(), function(), cache_status(), object_type(), any()) :: + Plug.Conn.t() | {:error, :not_found} defp render_or_error(conn, check_fn, status, object_type, object) do case check_fn.(conn, status, object) do true -> @@ -136,12 +157,17 @@ defmodule Mobilizon.Web.PageController do end end + @spec is_visible?(map) :: boolean() defp is_visible?(%{visibility: v}), do: v in [:public, :unlisted] defp is_visible?(%Tombstone{}), do: true defp is_visible?(_), do: true + @spec ok_status?(cache_status) :: boolean() defp ok_status?(status), do: status in [:ok, :commit] + @typep cache_status :: :ok | :commit | :ignore + + @spec ok_status_and_is_visible?(Plug.Conn.t(), cache_status, map()) :: boolean() defp ok_status_and_is_visible?(_conn, status, o), do: ok_status?(status) and is_visible?(o) @@ -158,9 +184,11 @@ defmodule Mobilizon.Web.PageController do end end + @spec is_local?(map()) :: boolean | :remote defp is_local?(%{local: local}), do: if(local, do: true, else: :remote) defp is_local?(_), do: false + @spec maybe_add_noindex_header(Plug.Conn.t(), map()) :: Plug.Conn.t() defp maybe_add_noindex_header(conn, %{visibility: visibility}) when visibility != :public do put_resp_header(conn, "x-robots-tag", "noindex") @@ -168,6 +196,7 @@ defmodule Mobilizon.Web.PageController do defp maybe_add_noindex_header(conn, _), do: conn + @spec is_person?(Actor.t()) :: boolean() defp is_person?(%Actor{type: :Person}), do: true defp is_person?(_), do: false end diff --git a/lib/web/controllers/web_finger_controller.ex b/lib/web/controllers/web_finger_controller.ex index b0aec252..ecc2e878 100644 --- a/lib/web/controllers/web_finger_controller.ex +++ b/lib/web/controllers/web_finger_controller.ex @@ -17,6 +17,7 @@ defmodule Mobilizon.Web.WebFingerController do @doc """ Provides /.well-known/host-meta """ + @spec host_meta(Plug.Conn.t(), any()) :: Plug.Conn.t() | no_return def host_meta(conn, _params) do xml = WebFinger.host_meta() @@ -28,6 +29,7 @@ defmodule Mobilizon.Web.WebFingerController do @doc """ Provides /.well-known/webfinger """ + @spec webfinger(Plug.Conn.t(), any()) :: Plug.Conn.t() | no_return def webfinger(conn, %{"resource" => resource}) do case WebFinger.webfinger(resource, "JSON") do {:ok, response} -> json(conn, response) diff --git a/lib/web/email/event.ex b/lib/web/email/event.ex index b49a3baa..120f1d57 100644 --- a/lib/web/email/event.ex +++ b/lib/web/email/event.ex @@ -49,6 +49,7 @@ defmodule Mobilizon.Web.Email.Event do |> render(:event_updated) end + @spec calculate_event_diff_and_send_notifications(Event.t(), Event.t(), map()) :: {:ok, :ok} def calculate_event_diff_and_send_notifications( %Event{} = old_event, %Event{id: event_id} = event, @@ -75,6 +76,12 @@ defmodule Mobilizon.Web.Email.Event do end end + @spec send_notification_for_event_update_to_participant( + {Participant.t(), Actor.t(), User.t() | nil, Setting.t() | nil}, + Event.t(), + Event.t(), + MapSet.t() + ) :: Bamboo.Email.t() defp send_notification_for_event_update_to_participant( {%Participant{} = _participant, %Actor{} = actor, %User{locale: locale, email: email} = _user, %Setting{timezone: timezone}}, @@ -131,6 +138,15 @@ defmodule Mobilizon.Web.Email.Event do ) end + @spec do_send_notification_for_event_update_to_participant( + String.t(), + Actor.t(), + Event.t(), + Event.t(), + MapSet.t(), + String.t(), + String.t() + ) :: Bamboo.Email.t() defp do_send_notification_for_event_update_to_participant( email, actor, diff --git a/lib/web/email/notification.ex b/lib/web/email/notification.ex index 5800bf0f..c2e83f7f 100644 --- a/lib/web/email/notification.ex +++ b/lib/web/email/notification.ex @@ -34,6 +34,8 @@ defmodule Mobilizon.Web.Email.Notification do |> render(:before_event_notification) end + @spec on_day_notification(User.t(), list(Participant.t()), pos_integer(), String.t()) :: + Bamboo.Email.t() def on_day_notification( %User{email: email, settings: %Setting{timezone: timezone}}, participations, @@ -58,6 +60,8 @@ defmodule Mobilizon.Web.Email.Notification do |> render(:on_day_notification) end + @spec weekly_notification(User.t(), list(Participant.t()), pos_integer(), String.t()) :: + Bamboo.Email.t() def weekly_notification( %User{email: email, settings: %Setting{timezone: timezone}}, participations, @@ -82,6 +86,7 @@ defmodule Mobilizon.Web.Email.Notification do |> render(:notification_each_week) end + @spec pending_participation_notification(User.t(), Event.t(), pos_integer()) :: Bamboo.Email.t() def pending_participation_notification( %User{locale: locale, email: email}, %Event{} = event, diff --git a/lib/web/media_proxy.ex b/lib/web/media_proxy.ex index a7c9fe4f..9ac3ebe0 100644 --- a/lib/web/media_proxy.ex +++ b/lib/web/media_proxy.ex @@ -11,6 +11,7 @@ defmodule Mobilizon.Web.MediaProxy do @base64_opts [padding: false] + @spec url(String.t() | nil) :: String.t() | nil def url(url) when is_nil(url) or url == "", do: nil def url("/" <> _ = url), do: url @@ -27,10 +28,12 @@ defmodule Mobilizon.Web.MediaProxy do not local?(url) end + @spec enabled? :: boolean() def enabled?, do: Config.get([:media_proxy, :enabled], false) def local?(url), do: String.starts_with?(url, Web.Endpoint.url()) + @spec base64_sig64(String.t()) :: {String.t(), String.t()} defp base64_sig64(url) do base64 = Base.url_encode64(url, @base64_opts) @@ -42,12 +45,14 @@ defmodule Mobilizon.Web.MediaProxy do {base64, sig64} end + @spec encode_url(String.t()) :: String.t() def encode_url(url) do {base64, sig64} = base64_sig64(url) build_url(sig64, base64, filename(url)) end + @spec decode_url(String.t(), String.t()) :: {:ok, String.t()} | {:error, :invalid_signature} def decode_url(sig, url) do with {:ok, sig} <- Base.url_decode64(sig, @base64_opts), signature when signature == sig <- signed_url(url) do @@ -57,10 +62,12 @@ defmodule Mobilizon.Web.MediaProxy do end end + @spec signed_url(String.t()) :: String.t() defp signed_url(url) do sha_hmac(Config.get([Web.Endpoint, :secret_key_base]), url) end + @spec sha_hmac(String.t(), String.t()) :: String.t() @compile {:no_warn_undefined, {:crypto, :mac, 4}} @compile {:no_warn_undefined, {:crypto, :hmac, 3}} defp sha_hmac(key, url) do @@ -73,14 +80,17 @@ defmodule Mobilizon.Web.MediaProxy do end end + @spec filename(String.t()) :: String.t() | nil def filename(url_or_path) do if path = URI.parse(url_or_path).path, do: Path.basename(path) end + @spec base_url :: String.t() def base_url do Web.Endpoint.url() end + @spec proxy_url(String.t(), String.t(), String.t(), String.t() | nil) :: String.t() defp proxy_url(path, sig_base64, url_base64, filename) do [ base_url(), @@ -93,10 +103,13 @@ defmodule Mobilizon.Web.MediaProxy do |> Path.join() end + @spec build_url(String.t(), String.t(), String.t() | nil) :: String.t() def build_url(sig_base64, url_base64, filename \\ nil) do proxy_url("proxy", sig_base64, url_base64, filename) end + @spec verify_request_path_and_url(Plug.Conn.t() | String.t(), String.t()) :: + :ok | {:wrong_filename, String.t()} def verify_request_path_and_url( %Plug.Conn{params: %{"filename" => _}, request_path: request_path}, url @@ -116,6 +129,7 @@ defmodule Mobilizon.Web.MediaProxy do def verify_request_path_and_url(_, _), do: :ok + @spec basename_matches?(String.t(), String.t()) :: boolean() defp basename_matches?(path, filename) do basename = Path.basename(path) basename == filename or URI.decode(basename) == filename or URI.encode(basename) == filename diff --git a/lib/web/plugs/detect_locale_plug.ex b/lib/web/plugs/detect_locale_plug.ex index 66c339ac..0c68c9e8 100644 --- a/lib/web/plugs/detect_locale_plug.ex +++ b/lib/web/plugs/detect_locale_plug.ex @@ -11,19 +11,23 @@ defmodule Mobilizon.Web.Plugs.DetectLocalePlug do import Plug.Conn, only: [get_req_header: 2, assign: 3] alias Mobilizon.Web.Gettext, as: GettextBackend + @spec init(any()) :: any() def init(_), do: nil + @spec call(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t() def call(conn, _) do locale = get_locale_from_header(conn) assign(conn, :detected_locale, locale) end + @spec get_locale_from_header(Plug.Conn.t()) :: String.t() defp get_locale_from_header(conn) do conn |> extract_accept_language() |> Enum.find(&supported_locale?/1) end + @spec extract_accept_language(Plug.Conn.t()) :: list(String.t()) defp extract_accept_language(conn) do case get_req_header(conn, "accept-language") do [value | _] -> @@ -40,12 +44,14 @@ defmodule Mobilizon.Web.Plugs.DetectLocalePlug do end end + @spec supported_locale?(String.t()) :: boolean() defp supported_locale?(locale) do GettextBackend |> Gettext.known_locales() |> Enum.member?(locale) end + @spec parse_language_option(String.t()) :: %{tag: String.t(), quality: float()} defp parse_language_option(string) do captures = Regex.named_captures(~r/^\s?(?[\w\-]+)(?:;q=(?[\d\.]+))?$/i, string) @@ -58,6 +64,7 @@ defmodule Mobilizon.Web.Plugs.DetectLocalePlug do %{tag: captures["tag"], quality: quality} end + @spec ensure_language_fallbacks(list(String.t())) :: list(String.t()) defp ensure_language_fallbacks(tags) do Enum.flat_map(tags, fn tag -> [language | _] = String.split(tag, "-") diff --git a/lib/web/plugs/federating.ex b/lib/web/plugs/federating.ex index d63b69ce..99911e3c 100644 --- a/lib/web/plugs/federating.ex +++ b/lib/web/plugs/federating.ex @@ -10,10 +10,12 @@ defmodule Mobilizon.Web.Plugs.Federating do import Plug.Conn + @spec init(any()) :: any() def init(options) do options end + @spec call(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t() def call(conn, _opts) do if Mobilizon.Config.get([:instance, :federating]) do conn diff --git a/lib/web/plugs/http_security_plug.ex b/lib/web/plugs/http_security_plug.ex index 93824246..468dd89e 100644 --- a/lib/web/plugs/http_security_plug.ex +++ b/lib/web/plugs/http_security_plug.ex @@ -13,8 +13,10 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do require Logger + @spec init(any()) :: any() def init(opts), do: opts + @spec call(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t() def call(conn, options \\ []) do if Config.get([:http_security, :enabled]) do conn diff --git a/lib/web/plugs/mapped_signature_to_identity.ex b/lib/web/plugs/mapped_signature_to_identity.ex index 548f3ac0..0ae8d252 100644 --- a/lib/web/plugs/mapped_signature_to_identity.ex +++ b/lib/web/plugs/mapped_signature_to_identity.ex @@ -18,6 +18,7 @@ defmodule Mobilizon.Web.Plugs.MappedSignatureToIdentity do require Logger + @spec init(any()) :: any() def init(options), do: options @spec key_id_from_conn(Plug.Conn.t()) :: String.t() | nil @@ -42,6 +43,7 @@ defmodule Mobilizon.Web.Plugs.MappedSignatureToIdentity do end end + @spec call(Plug.Conn.t(), any()) :: Plug.Conn.t() def call(%{assigns: %{actor: _}} = conn, _opts), do: conn # if this has payload make sure it is signed by the same actor that made it diff --git a/lib/web/plugs/uploaded_media.ex b/lib/web/plugs/uploaded_media.ex index 150386f2..192890cf 100644 --- a/lib/web/plugs/uploaded_media.ex +++ b/lib/web/plugs/uploaded_media.ex @@ -21,6 +21,7 @@ defmodule Mobilizon.Web.Plugs.UploadedMedia do # no slashes @path "media" + @spec init(any()) :: map() def init(_opts) do static_plug_opts = [] @@ -31,6 +32,7 @@ defmodule Mobilizon.Web.Plugs.UploadedMedia do %{static_plug_opts: static_plug_opts} end + @spec call(Plug.Conn.t(), map()) :: Plug.Conn.t() def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do conn = case fetch_query_params(conn) do diff --git a/test/federation/activity_pub/activity_pub_test.exs b/test/federation/activity_pub/activity_pub_test.exs index aa72aae8..59e6986d 100644 --- a/test/federation/activity_pub/activity_pub_test.exs +++ b/test/federation/activity_pub/activity_pub_test.exs @@ -16,7 +16,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.Utils + alias Mobilizon.Federation.ActivityPub.{Actions, Utils} alias Mobilizon.Federation.HTTPSignatures.Signature alias Mobilizon.Service.HTTP.ActivityPub.Mock @@ -117,7 +117,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do test "it creates a delete activity and deletes the original event" do event = insert(:event) event = Events.get_public_event_by_url_with_preload!(event.url) - {:ok, delete, _} = ActivityPub.delete(event, event.organizer_actor) + {:ok, delete, _} = Actions.Delete.delete(event, event.organizer_actor) assert delete.data["type"] == "Delete" assert delete.data["actor"] == event.organizer_actor.url @@ -133,7 +133,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do lazy_put_activity_defaults: fn args -> args end do event = insert(:event) event = Events.get_public_event_by_url_with_preload!(event.url) - {:ok, delete, _} = ActivityPub.delete(event, event.organizer_actor, false) + {:ok, delete, _} = Actions.Delete.delete(event, event.organizer_actor, false) assert delete.data["type"] == "Delete" assert delete.data["actor"] == event.organizer_actor.url @@ -151,7 +151,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do comment = insert(:comment) comment = Discussions.get_comment_from_url_with_preload!(comment.url) assert is_nil(Discussions.get_comment_from_url(comment.url).deleted_at) - {:ok, delete, _} = ActivityPub.delete(comment, comment.actor) + {:ok, delete, _} = Actions.Delete.delete(comment, comment.actor) assert delete.data["type"] == "Delete" assert delete.data["actor"] == comment.actor.url @@ -169,7 +169,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do actor = insert(:actor) actor_data = %{summary: @updated_actor_summary} - {:ok, update, _} = ActivityPub.update(actor, actor_data, false) + {:ok, update, _} = Actions.Update.update(actor, actor_data, false) assert update.data["actor"] == actor.url assert update.data["to"] == [@activity_pub_public_audience] @@ -185,7 +185,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do event = insert(:event, organizer_actor: actor) event_data = %{begins_on: @updated_start_time} - {:ok, update, _} = ActivityPub.update(event, event_data) + {:ok, update, _} = Actions.Update.update(event, event_data) assert update.data["actor"] == actor.url assert update.data["to"] == [@activity_pub_public_audience] @@ -200,7 +200,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do group = insert(:group) member = insert(:member, parent: group) moderator = insert(:actor) - {:ok, activity, _member} = ActivityPub.remove(member, group, moderator, true) + {:ok, activity, _member} = Actions.Remove.remove(member, group, moderator, true) assert activity.data["type"] == "Remove" assert activity.data["actor"] == moderator.url assert activity.data["to"] == [group.members_url] @@ -217,9 +217,14 @@ defmodule Mobilizon.Federation.ActivityPubTest do group = insert(:group) {:ok, create_data, %TodoList{url: todo_list_url}} = - ActivityPub.create(:todo_list, %{title: @todo_list_title, actor_id: group.id}, true, %{ - "actor" => actor.url - }) + Actions.Create.create( + :todo_list, + %{title: @todo_list_title, actor_id: group.id}, + true, + %{ + "actor" => actor.url + } + ) assert create_data.local assert create_data.data["object"]["id"] == todo_list_url @@ -242,7 +247,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do todo_list = insert(:todo_list) {:ok, create_data, %Todo{url: todo_url}} = - ActivityPub.create( + Actions.Create.create( :todo, %{title: @todo_title, todo_list_id: todo_list.id, creator_id: actor.id}, true, @@ -272,7 +277,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do group = insert(:group) {:ok, create_data, %Resource{url: url}} = - ActivityPub.create( + Actions.Create.create( :resource, %{ title: @resource_title, @@ -307,7 +312,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do group = insert(:group) {:ok, create_data, %Resource{url: url}} = - ActivityPub.create( + Actions.Create.create( :resource, %{ title: @folder_title, @@ -341,7 +346,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do insert(:resource, type: :folder, resource_url: nil, actor: group) {:ok, create_data, %Resource{url: url}} = - ActivityPub.create( + Actions.Create.create( :resource, %{ title: @resource_title, @@ -388,7 +393,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do ) {:ok, update_data, %Resource{url: url}} = - ActivityPub.update( + Actions.Update.update( resource, %{ title: @updated_resource_title @@ -430,7 +435,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do insert(:resource, type: :folder, resource_url: nil, actor: group) {:ok, update_data, %Resource{url: url}} = - ActivityPub.move( + Actions.Move.move( :resource, resource, %{ @@ -473,7 +478,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do ) {:ok, update_data, %Resource{url: url}} = - ActivityPub.delete( + Actions.Delete.delete( resource, actor, true diff --git a/test/federation/activity_pub/transmogrifier/follow_test.exs b/test/federation/activity_pub/transmogrifier/follow_test.exs index 10a2666c..6d7f7545 100644 --- a/test/federation/activity_pub/transmogrifier/follow_test.exs +++ b/test/federation/activity_pub/transmogrifier/follow_test.exs @@ -6,7 +6,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do alias Mobilizon.Actors alias Mobilizon.Actors.Follower alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier} + alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Transmogrifier} describe "handle incoming follow requests" do test "it works only for groups" do @@ -80,7 +80,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do refute Actors.is_following(follower, followed) - {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) + {:ok, follow_activity, _} = Actions.Follow.follow(follower, followed) assert Actors.is_following(follower, followed) follow_object_id = follow_activity.data["id"] @@ -112,7 +112,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do refute Actors.is_following(follower, followed) - {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) + {:ok, follow_activity, _} = Actions.Follow.follow(follower, followed) assert Actors.is_following(follower, followed) follow_object_id = follow_activity.data["id"] @@ -145,7 +145,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do follower = insert(:actor) followed = insert(:group, manually_approves_followers: true) - {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) + {:ok, follow_activity, _} = Actions.Follow.follow(follower, followed) accept_data = File.read!("test/fixtures/mastodon-accept-activity.json") @@ -207,7 +207,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do follower = insert(:actor) followed = insert(:group) - {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) + {:ok, follow_activity, _} = Actions.Follow.follow(follower, followed) assert Actors.is_following(follower, followed) diff --git a/test/federation/activity_pub/transmogrifier/join_test.exs b/test/federation/activity_pub/transmogrifier/join_test.exs index 3948bf40..a65ce7c1 100644 --- a/test/federation/activity_pub/transmogrifier/join_test.exs +++ b/test/federation/activity_pub/transmogrifier/join_test.exs @@ -7,7 +7,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.JoinTest do alias Mobilizon.Events alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.Transmogrifier + alias Mobilizon.Federation.ActivityPub.{Actions, Transmogrifier} describe "handle incoming join activities" do @join_message "I want to get in!" @@ -48,7 +48,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.JoinTest do %Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted) {:ok, join_activity, participation} = - ActivityPub.join(event, participant_actor, false, %{metadata: %{role: :not_approved}}) + Actions.Join.join(event, participant_actor, false, %{ + metadata: %{role: :not_approved} + }) accept_data = File.read!("test/fixtures/mastodon-accept-activity.json") @@ -113,7 +115,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.JoinTest do %Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted) - {:ok, join_activity, participation} = ActivityPub.join(event, participant_actor) + {:ok, join_activity, participation} = Actions.Join.join(event, participant_actor) reject_data = File.read!("test/fixtures/mastodon-reject-activity.json") diff --git a/test/federation/activity_pub/transmogrifier/leave_test.exs b/test/federation/activity_pub/transmogrifier/leave_test.exs index c72f5471..48452653 100644 --- a/test/federation/activity_pub/transmogrifier/leave_test.exs +++ b/test/federation/activity_pub/transmogrifier/leave_test.exs @@ -5,8 +5,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.LeaveTest do alias Mobilizon.Actors.Actor alias Mobilizon.Events alias Mobilizon.Events.{Event, Participant} - alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.Transmogrifier + alias Mobilizon.Federation.ActivityPub.{Actions, Transmogrifier} describe "handle incoming leave activities on events" do test "it accepts Leave activities" do @@ -19,7 +18,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.LeaveTest do organizer_participation = %Participant{} = insert(:participant, event: event, actor: organizer, role: :creator) - {:ok, _join_activity, _participation} = ActivityPub.join(event, participant_actor) + {:ok, _join_activity, _participation} = Actions.Join.join(event, participant_actor) join_data = File.read!("test/fixtures/mobilizon-leave-activity.json") diff --git a/test/graphql/resolvers/group_test.exs b/test/graphql/resolvers/group_test.exs index e99d7bef..f8fc1089 100644 --- a/test/graphql/resolvers/group_test.exs +++ b/test/graphql/resolvers/group_test.exs @@ -57,7 +57,7 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) assert hd(json_response(res, 200)["errors"])["message"] == - "A group with this name already exists" + "A profile or group with that name already exists" end end diff --git a/test/graphql/resolvers/resource_test.exs b/test/graphql/resolvers/resource_test.exs index 65dc4b73..b561f87b 100644 --- a/test/graphql/resolvers/resource_test.exs +++ b/test/graphql/resolvers/resource_test.exs @@ -598,7 +598,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ResourceTest do } ) - assert is_nil(res["errors"]) + assert res["errors"] == nil assert res["data"]["updateResource"]["path"] == "#{root_folder.path}/#{@updated_resource_title}"