From de047c8939311dbba36bae8e8ce19ad00e556a47 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 10 Sep 2021 11:27:59 +0200 Subject: [PATCH 01/17] Various typespec and compilation improvements Signed-off-by: Thomas Citharel --- lib/federation/activity_pub/activity_pub.ex | 68 ++++++++++--- lib/federation/activity_pub/actor.ex | 2 +- lib/federation/activity_pub/audience.ex | 6 +- lib/federation/activity_pub/permission.ex | 8 +- lib/federation/activity_pub/transmogrifier.ex | 2 +- lib/federation/activity_pub/types/actors.ex | 35 +++++-- lib/federation/activity_pub/types/comments.ex | 16 ++- .../activity_pub/types/discussions.ex | 14 ++- lib/federation/activity_pub/types/entity.ex | 16 ++- lib/federation/activity_pub/types/events.ex | 39 +++++--- lib/federation/activity_pub/types/members.ex | 18 +++- lib/federation/activity_pub/types/posts.ex | 9 ++ lib/federation/activity_pub/types/reports.ex | 3 + .../activity_pub/types/resources.ex | 9 ++ .../activity_pub/types/todo_lists.ex | 9 +- lib/federation/activity_pub/types/todos.ex | 8 +- .../activity_stream/converter/actor.ex | 6 +- .../activity_stream/converter/comment.ex | 2 +- .../activity_stream/converter/converter.ex | 2 +- .../activity_stream/converter/discussion.ex | 30 +++--- .../activity_stream/converter/event.ex | 2 +- .../activity_stream/converter/member.ex | 1 + .../activity_stream/converter/post.ex | 21 ++-- .../activity_stream/converter/resource.ex | 7 +- .../activity_stream/converter/todo.ex | 2 +- .../activity_stream/converter/todo_list.ex | 2 +- lib/federation/http_signatures/signature.ex | 14 ++- lib/federation/web_finger/web_finger.ex | 10 +- lib/graphql/api/reports.ex | 16 +-- lib/graphql/api/utils.ex | 2 +- lib/graphql/resolvers/comment.ex | 2 +- lib/graphql/resolvers/event/utils.ex | 10 +- lib/graphql/resolvers/member.ex | 4 +- lib/graphql/resolvers/todos.ex | 9 ++ lib/graphql/resolvers/user.ex | 83 ++++++++++------ lib/graphql/schema/tag.ex | 1 + lib/mix/tasks/mobilizon/actors/refresh.ex | 3 - lib/mix/tasks/mobilizon/actors/utils.ex | 2 +- lib/mix/tasks/mobilizon/common.ex | 2 +- lib/mix/tasks/mobilizon/instance.ex | 1 + lib/mix/tasks/mobilizon/users/modify.ex | 13 ++- lib/mix/tasks/mobilizon/users/new.ex | 7 ++ lib/mix/tasks/mobilizon/users/show.ex | 13 ++- lib/mobilizon/actors/actors.ex | 2 +- lib/mobilizon/config.ex | 5 +- lib/mobilizon/discussions/discussions.ex | 3 +- lib/mobilizon/events/event.ex | 10 +- lib/mobilizon/events/events.ex | 19 +++- lib/mobilizon/events/participant.ex | 19 +--- lib/mobilizon/events/participant_metadata.ex | 36 +++++++ lib/mobilizon/medias/media.ex | 2 +- lib/service/activity/renderer/renderer.ex | 13 ++- lib/service/activity/utils.ex | 2 +- lib/service/geospatial/nominatim.ex | 4 +- lib/service/geospatial/provider.ex | 3 +- lib/service/guards.ex | 28 +++++- .../language_detection/language_detection.ex | 2 +- lib/web/auth/context.ex | 5 +- lib/web/cache/cache.ex | 11 ++- lib/web/channels/graphql_socket.ex | 2 + lib/web/email/activity.ex | 2 +- lib/web/email/mailer.ex | 2 + lib/web/email/participation.ex | 4 - lib/web/email/user.ex | 6 +- lib/web/mobilizon_web.ex | 9 +- lib/web/plugs/http_security_plug.ex | 10 +- lib/web/plugs/http_signatures.ex | 1 + lib/web/plugs/uploaded_media.ex | 18 +++- lib/web/proxy/reverse_proxy.ex | 4 + .../activity/_comment_activity_item.html.eex | 8 +- .../activity/_comment_activity_item.text.eex | 8 +- .../_discussion_activity_item.html.eex | 8 +- .../_discussion_activity_item.text.eex | 8 +- .../activity/_event_activity_item.html.eex | 8 +- .../activity/_event_activity_item.text.eex | 8 +- .../activity/_group_activity_item.html.eex | 4 +- .../activity/_group_activity_item.text.eex | 4 +- .../activity/_post_activity_item.html.eex | 4 +- .../activity/_post_activity_item.text.eex | 4 +- .../activity/_resource_activity_item.html.eex | 12 +-- .../activity/_resource_activity_item.text.eex | 12 +-- ...nymous_participation_confirmation.html.eex | 2 +- ...nymous_participation_confirmation.text.eex | 2 +- .../email/before_event_notification.html.eex | 2 +- .../email/before_event_notification.text.eex | 2 +- .../email/email_anonymous_activity.html.eex | 4 +- .../email/email_anonymous_activity.text.eex | 2 +- .../email/email_changed_new.html.eex | 2 +- .../email/email_changed_new.text.eex | 2 +- .../email/email_direct_activity.html.eex | 8 +- .../email/email_direct_activity.text.eex | 2 +- .../event_participation_approved.html.eex | 2 +- .../event_participation_approved.text.eex | 2 +- .../event_participation_confirmed.html.eex | 2 +- .../event_participation_confirmed.text.eex | 2 +- .../templates/email/event_updated.html.eex | 2 +- .../templates/email/event_updated.text.eex | 2 +- lib/web/templates/email/group_invite.html.eex | 2 +- lib/web/templates/email/group_invite.text.eex | 2 +- .../email/notification_each_week.html.eex | 4 +- .../email/notification_each_week.text.eex | 4 +- .../email/on_day_notification.html.eex | 4 +- .../email/on_day_notification.text.eex | 4 +- ...ending_participation_notification.html.eex | 2 +- ...ending_participation_notification.text.eex | 2 +- lib/web/templates/email/report.html.eex | 2 +- lib/web/templates/email/report.text.eex | 2 +- lib/web/upload/filter/analyze_metadata.ex | 2 +- lib/web/upload/filter/anonymize_filename.ex | 14 ++- lib/web/upload/filter/blurhash.ex | 2 +- lib/web/upload/filter/dedupe.ex | 1 + lib/web/upload/filter/exiftool.ex | 2 +- lib/web/upload/filter/filter.ex | 5 +- lib/web/upload/filter/mogrify.ex | 2 +- lib/web/upload/filter/optimize.ex | 14 +-- lib/web/upload/filter/resize.ex | 3 + lib/web/upload/mime.ex | 4 +- lib/web/upload/upload.ex | 97 ++++++++++++++----- lib/web/upload/uploader/local.ex | 57 +++++++++-- lib/web/upload/uploader/uploader.ex | 7 +- lib/web/views/activity_pub/actor_view.ex | 17 +++- lib/web/views/utils.ex | 3 +- mix.exs | 1 + test/tasks/users_test.exs | 4 +- test/web/upload/upload_test.exs | 5 +- 125 files changed, 790 insertions(+), 357 deletions(-) create mode 100644 lib/mobilizon/events/participant_metadata.ex diff --git a/lib/federation/activity_pub/activity_pub.ex b/lib/federation/activity_pub/activity_pub.ex index 94a65a962..caf110f81 100644 --- a/lib/federation/activity_pub/activity_pub.ex +++ b/lib/federation/activity_pub/activity_pub.ex @@ -41,7 +41,7 @@ defmodule Mobilizon.Federation.ActivityPub do alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor - alias Mobilizon.Federation.ActivityPub.Types.{Managable, Ownable} + alias Mobilizon.Federation.ActivityPub.Types.{Entity, Managable, Ownable} alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.HTTPSignatures.Signature @@ -56,11 +56,9 @@ defmodule Mobilizon.Federation.ActivityPub do @public_ap_adress "https://www.w3.org/ns/activitystreams#Public" - @doc """ - Wraps an object into an activity - """ + # Wraps an object into an activity @spec create_activity(map(), boolean()) :: {:ok, Activity.t()} - def create_activity(map, local \\ true) when is_map(map) do + defp create_activity(map, local) when is_map(map) do with map <- lazy_put_activity_defaults(map) do {:ok, %Activity{ @@ -168,7 +166,7 @@ defmodule Mobilizon.Federation.ActivityPub do * Federates (asynchronously) the activity * Returns the activity """ - @spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any() + @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)) @@ -206,7 +204,8 @@ defmodule Mobilizon.Federation.ActivityPub do * Federates (asynchronously) the activity * Returns the activity """ - @spec update(struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any() + @spec update(Entity.entities(), map(), boolean, map()) :: + {:ok, Activity.t(), Entity.entities()} | any() def update(old_entity, args, local \\ false, additional \\ %{}) do Logger.debug("updating an activity") Logger.debug(inspect(args)) @@ -224,6 +223,12 @@ defmodule Mobilizon.Federation.ActivityPub do 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") @@ -246,6 +251,8 @@ defmodule Mobilizon.Federation.ActivityPub do 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 @@ -266,6 +273,8 @@ defmodule Mobilizon.Federation.ActivityPub do end end + @spec announce(Actor.t(), ActivityStream.t(), String.t() | nil, boolean, boolean) :: + {:ok, Activity.t(), ActivityStream.t()} def announce( %Actor{} = actor, object, @@ -286,6 +295,8 @@ defmodule Mobilizon.Federation.ActivityPub do 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, @@ -306,6 +317,8 @@ defmodule Mobilizon.Federation.ActivityPub do @doc """ Make an actor follow another """ + @spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean, map) :: + {:ok, Activity.t(), Follower.t()} | {:error, String.t()} def follow( %Actor{} = follower, %Actor{} = followed, @@ -336,7 +349,8 @@ defmodule Mobilizon.Federation.ActivityPub do @doc """ Make an actor unfollow another """ - @spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any() + @spec unfollow(Actor.t(), Actor.t(), String.t() | nil, boolean()) :: + {:ok, Activity.t(), Follower.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 @@ -357,6 +371,7 @@ defmodule Mobilizon.Federation.ActivityPub do 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), @@ -369,6 +384,9 @@ defmodule Mobilizon.Federation.ActivityPub do end end + @spec join(Event.t(), Actor.t(), boolean, map) :: + {:ok, Activity.t(), Participant.t()} | {:maximum_attendee_capacity, any} + @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 @@ -397,6 +415,8 @@ defmodule Mobilizon.Federation.ActivityPub do end end + @spec leave(Event.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Participant.t()} + @spec leave(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()} def leave(object, actor, local \\ true, additional \\ %{}) @doc """ @@ -462,6 +482,7 @@ defmodule Mobilizon.Federation.ActivityPub do 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}, @@ -502,7 +523,7 @@ defmodule Mobilizon.Federation.ActivityPub do ) 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)}, + 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, @@ -538,7 +559,8 @@ defmodule Mobilizon.Federation.ActivityPub do end end - defp is_able_to_invite(%Actor{domain: actor_domain, id: actor_id}, %Actor{ + @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 @@ -547,12 +569,17 @@ defmodule Mobilizon.Federation.ActivityPub do true else # If local group, we'll send the invite - with {:ok, %Member{} = admin_member} <- Actors.get_member(actor_id, group_id) do - Member.is_administrator(admin_member) + 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)) @@ -572,6 +599,7 @@ defmodule Mobilizon.Federation.ActivityPub do 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), @@ -615,6 +643,7 @@ defmodule Mobilizon.Federation.ActivityPub do 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 @@ -678,6 +707,8 @@ defmodule Mobilizon.Federation.ActivityPub do @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) @@ -711,7 +742,7 @@ defmodule Mobilizon.Federation.ActivityPub do @doc """ Return all public activities (events & comments) for an actor """ - @spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map() + @spec fetch_public_activities_for_actor(Actor.t(), pos_integer(), pos_integer()) :: map() def fetch_public_activities_for_actor(%Actor{id: actor_id} = actor, page \\ 1, limit \\ 10) do %Actor{id: relay_actor_id} = Relay.get_actor() @@ -769,6 +800,8 @@ defmodule Mobilizon.Federation.ActivityPub do 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}), @@ -792,7 +825,10 @@ defmodule Mobilizon.Federation.ActivityPub do end end - @spec accept_join(Participant.t(), map) :: {:ok, Participant.t(), Activity.t()} | any + @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}), @@ -820,7 +856,6 @@ defmodule Mobilizon.Federation.ActivityPub do end end - @spec accept_join(Member.t(), map) :: {:ok, Member.t(), Activity.t()} | any defp accept_join(%Member{} = member, additional) do with {:ok, %Member{} = member} <- Actors.update_member(member, %{role: :member}), @@ -854,6 +889,8 @@ defmodule Mobilizon.Federation.ActivityPub do 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, @@ -881,6 +918,7 @@ defmodule Mobilizon.Federation.ActivityPub do 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 diff --git a/lib/federation/activity_pub/actor.ex b/lib/federation/activity_pub/actor.ex index 3ac28f57d..cd02fa4d2 100644 --- a/lib/federation/activity_pub/actor.ex +++ b/lib/federation/activity_pub/actor.ex @@ -108,7 +108,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do {:ok, url} when is_binary(url) -> make_actor_from_url(url, preload) - _e -> + {:error, _e} -> {:error, "No ActivityPub URL found in WebFinger"} end end diff --git a/lib/federation/activity_pub/audience.ex b/lib/federation/activity_pub/audience.ex index 2b6a6be5b..f88325086 100644 --- a/lib/federation/activity_pub/audience.ex +++ b/lib/federation/activity_pub/audience.ex @@ -99,6 +99,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do } end + @spec get_to_and_cc(Actor.t(), list(), :direct | :private | :public | :unlisted | {:list, any}) :: + {list(), list()} @doc """ Determines the full audience based on mentions for an audience @@ -118,7 +120,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do * `to` : the mentioned actors and the eventual actor we're replying to * `cc` : none """ - @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} def get_to_and_cc(%Actor{} = actor, mentions, :public) do to = [@ap_public | mentions] cc = [actor.followers_url] @@ -128,7 +129,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do {to, cc} end - @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do to = [actor.followers_url | mentions] cc = [@ap_public] @@ -138,7 +138,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do {to, cc} end - @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} def get_to_and_cc(%Actor{} = actor, mentions, :private) do {to, cc} = get_to_and_cc(actor, mentions, :direct) @@ -147,7 +146,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do {to, cc} end - @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} def get_to_and_cc(_actor, mentions, :direct) do {mentions, []} end diff --git a/lib/federation/activity_pub/permission.ex b/lib/federation/activity_pub/permission.ex index 41a14afca..2d43c6ebb 100644 --- a/lib/federation/activity_pub/permission.ex +++ b/lib/federation/activity_pub/permission.ex @@ -13,6 +13,8 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do @member_roles [:member, :moderator, :administrator] + @type object :: %{id: String.t(), url: String.t()} + @doc """ Check that actor can access the object """ @@ -66,8 +68,8 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do @spec can_manage_group_object?( existing_object_permissions(), - Actor.t(), - any() + %Actor{url: String.t()}, + object() ) :: boolean() defp can_manage_group_object?(permission, %Actor{url: actor_url} = actor, object) do if Ownable.group_actor(object) != nil do @@ -94,7 +96,7 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do end end - @spec activity_actor_is_group_member?(Actor.t(), Entity.t(), atom()) :: boolean() + @spec activity_actor_is_group_member?(Actor.t(), object(), atom()) :: boolean() defp activity_actor_is_group_member?( %Actor{id: actor_id, url: actor_url}, object, diff --git a/lib/federation/activity_pub/transmogrifier.ex b/lib/federation/activity_pub/transmogrifier.ex index 0ebd70a04..cd2c96f7a 100644 --- a/lib/federation/activity_pub/transmogrifier.ex +++ b/lib/federation/activity_pub/transmogrifier.ex @@ -951,7 +951,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do defp do_handle_incoming_reject_invite(invite_object, %Actor{} = actor_rejecting) do with {:invite, {:ok, %Member{role: :invited, actor_id: actor_id} = member}} <- {:invite, get_member(invite_object)}, - {:same_actor, true} <- {:same_actor, actor_rejecting.id === actor_id}, + {:same_actor, true} <- {:same_actor, actor_rejecting.id == actor_id}, {:ok, activity, member} <- ActivityPub.reject(:invite, member, false) do {:ok, activity, member} diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex index f7d7d4d95..0dd34cb63 100644 --- a/lib/federation/activity_pub/types/actors.ex +++ b/lib/federation/activity_pub/types/actors.ex @@ -1,10 +1,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do @moduledoc false alias Mobilizon.Actors - alias Mobilizon.Actors.{Actor, Follower, Member} + alias Mobilizon.Actors.{Actor, Follower, Member, MemberRole} alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Audience, Permission, Relay} alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.Service.Activity.Group, as: GroupActivity @@ -17,7 +18,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do @behaviour Entity @impl Entity - @spec create(map(), map()) :: {:ok, map()} + @spec create(map(), map()) :: {:ok, Actor.t(), ActivityStream.t()} def create(args, additional) do with args <- prepare_args_for_actor(args), {:ok, %Actor{} = actor} <- Actors.create_actor(args), @@ -35,7 +36,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do end @impl Entity - @spec update(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any + @spec update(Actor.t(), map, map) :: {:ok, Actor.t(), ActivityStream.t()} def update(%Actor{} = old_actor, args, additional) do with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args), {:ok, _} <- @@ -57,6 +58,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do @public_ap "https://www.w3.org/ns/activitystreams#Public" @impl Entity + @spec delete(Actor.t(), Actor.t(), boolean, map) :: + {:ok, ActivityStream.t(), Actor.t(), Actor.t()} def delete( %Actor{ followers_url: followers_url, @@ -100,10 +103,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do end end + @spec actor(Actor.t()) :: Actor.t() | nil def actor(%Actor{} = actor), do: actor + @spec actor(Actor.t()) :: Actor.t() | nil def group_actor(%Actor{} = actor), do: actor + @spec permissions(Actor.t()) :: Permission.t() def permissions(%Actor{} = _group) do %Permission{ access: :member, @@ -113,7 +119,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do } end - @spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, map(), Member.t()} + @spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, ActivityStreams.t(), Member.t()} def join(%Actor{type: :Group} = group, %Actor{} = actor, _local, additional) do with role <- additional @@ -153,6 +159,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do end end + @spec follow(Actor.t(), Actor.t(), boolean, map) :: + {:accept, any} + | {:ok, ActivityStreams.t(), Follower.t()} + | {:error, :no_person, String.t()} def follow(%Actor{} = follower_actor, %Actor{type: type} = followed, _local, additional) when type != :Person do with {:ok, %Follower{} = follower} <- @@ -165,6 +175,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do def follow(_, _, _, _), do: {:error, :no_person, "Only group and instances can be followed"} + @spec prepare_args_for_actor(map) :: map defp prepare_args_for_actor(args) do args |> maybe_sanitize_username() @@ -191,8 +202,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do defp maybe_sanitize_summary(args), do: args # Set the participant to approved if the default role for new participants is :participant - @spec approve_if_default_role_is_member(Actor.t(), Actor.t(), map(), Member.t(), atom()) :: - {:ok, map(), Member.t()} + @spec approve_if_default_role_is_member( + Actor.t(), + Actor.t(), + ActivityStreams.t(), + Member.t(), + MemberRole.t() + ) :: + {:ok, ActivityStreams.t(), Member.t()} defp approve_if_default_role_is_member( %Actor{type: :Group} = group, %Actor{} = actor, @@ -202,7 +219,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do ) do if is_nil(group.domain) && !is_nil(actor.domain) do cond do - Mobilizon.Actors.get_default_member_role(group) === :member && + Mobilizon.Actors.get_default_member_role(group) == :member && role == :member -> {:accept, ActivityPub.accept( @@ -212,7 +229,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do %{"actor" => group.url} )} - Mobilizon.Actors.get_default_member_role(group) === :not_approved && + Mobilizon.Actors.get_default_member_role(group) == :not_approved && role == :not_approved -> Scheduler.pending_membership_notification(group) {:ok, activity_data, member} @@ -225,6 +242,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do end end + @spec approve_if_manually_approves_followers(Follower.t(), ActivityStreams.t()) :: + {:accept, any} | {:ok, ActivityStreams.t(), Follower.t()} defp approve_if_manually_approves_followers( %Follower{} = follower, follow_as_data diff --git a/lib/federation/activity_pub/types/comments.ex b/lib/federation/activity_pub/types/comments.ex index 2fedb789f..578ba6ed0 100644 --- a/lib/federation/activity_pub/types/comments.ex +++ b/lib/federation/activity_pub/types/comments.ex @@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do alias Mobilizon.Events.{Event, EventOptions} alias Mobilizon.Federation.ActivityPub.{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 @@ -20,7 +21,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do @behaviour Entity @impl Entity - @spec create(map(), map()) :: {:ok, map()} + @spec create(map(), map()) :: {:ok, Comment.t(), ActivityStream.t()} def create(args, additional) do with args <- prepare_args_for_comment(args), :ok <- make_sure_event_allows_commenting(args), @@ -41,7 +42,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do end @impl Entity - @spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any() + @spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), ActivityStream.t()} def update(%Comment{} = old_comment, args, additional) do with args <- prepare_args_for_comment_update(args), {:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args), @@ -60,7 +61,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do end @impl Entity - @spec delete(Comment.t(), Actor.t(), boolean, map()) :: {:ok, Comment.t()} + @spec delete(Comment.t(), Actor.t(), boolean, map()) :: + {:ok, ActivityStream.t(), Actor.t(), Comment.t()} def delete( %Comment{url: url, id: comment_id}, %Actor{} = actor, @@ -91,6 +93,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do end end + @spec actor(Comment.t()) :: Actor.t() | nil def actor(%Comment{actor: %Actor{} = actor}), do: actor def actor(%Comment{actor_id: actor_id}) when not is_nil(actor_id), @@ -98,6 +101,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do def actor(_), do: nil + @spec group_actor(Comment.t()) :: Actor.t() | nil def group_actor(%Comment{attributed_to: %Actor{} = group}), do: group def group_actor(%Comment{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id), @@ -105,6 +109,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do def group_actor(_), do: nil + @spec permissions(Comment.t()) :: Permission.t() def permissions(%Comment{}), do: %Permission{ access: :member, @@ -114,6 +119,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do } # Prepare and sanitize arguments for comments + @spec prepare_args_for_comment(map) :: map defp prepare_args_for_comment(args) do with in_reply_to_comment <- args |> Map.get(:in_reply_to_comment_id) |> Discussions.get_comment_with_preload(), @@ -150,6 +156,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do end end + @spec prepare_args_for_comment_update(map) :: map defp prepare_args_for_comment_update(args) do with {text, mentions, tags} <- APIUtils.make_content_html( @@ -174,6 +181,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do defp handle_event_for_comment(nil), do: nil + @spec maybe_publish_graphql_subscription(String.t() | integer() | nil) :: :ok defp maybe_publish_graphql_subscription(nil), do: :ok defp maybe_publish_graphql_subscription(discussion_id) do @@ -186,6 +194,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do end end + @spec make_sure_event_allows_commenting(%{actor_id: String.t() | integer, event: Event.t()}) :: + :ok | {:error, :event_comments_are_closed} defp make_sure_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 1fb013872..dd1bfde38 100644 --- a/lib/federation/activity_pub/types/discussions.ex +++ b/lib/federation/activity_pub/types/discussions.ex @@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Federation.ActivityPub.{Audience, Permission} alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.Service.Activity.Discussion, as: DiscussionActivity @@ -16,7 +17,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do @behaviour Entity @impl Entity - @spec create(map(), map()) :: {:ok, map()} + @spec create(map(), map()) :: {:ok, Discussion.t(), ActivityStream.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), @@ -39,7 +40,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do end @impl Entity - @spec create(map(), map()) :: {:ok, map()} + @spec create(map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()} def create(args, additional) do with args <- prepare_args(args), {:ok, %Discussion{} = discussion} <- @@ -56,7 +57,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do end @impl Entity - @spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), Activity.t()} | any() + @spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()} def update(%Discussion{} = old_discussion, args, additional) do with {:ok, %Discussion{} = new_discussion} <- Discussions.update_discussion(old_discussion, args), @@ -80,7 +81,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do end @impl Entity - @spec delete(Discussion.t(), Actor.t(), boolean, map()) :: {:ok, Discussion.t()} + @spec delete(Discussion.t(), Actor.t(), boolean, map()) :: + {:ok, ActivityStream.t(), Actor.t(), Discussion.t()} def delete( %Discussion{actor: group, url: url} = discussion, %Actor{} = actor, @@ -106,10 +108,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do end end + @spec actor(Discussion.t()) :: Actor.t() | nil def actor(%Discussion{creator_id: creator_id}), do: Actors.get_actor(creator_id) + @spec group_actor(Discussion.t()) :: Actor.t() | nil def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id) + @spec permissions(Discussion.t()) :: Permission.t() def permissions(%Discussion{}) do %Permission{access: :member, create: :member, update: :moderator, delete: :moderator} end @@ -123,6 +128,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do :ok end + @spec prepare_args(map) :: map defp prepare_args(args) do {text, _mentions, _tags} = APIUtils.make_content_html( diff --git a/lib/federation/activity_pub/types/entity.ex b/lib/federation/activity_pub/types/entity.ex index 3500294a5..30335e76a 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 +alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Posts.Post @@ -28,7 +28,19 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Entity do @moduledoc """ ActivityPub entity behaviour """ - @type t :: %{id: String.t()} + @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() @callback create(data :: any(), additionnal :: map()) :: {:ok, t(), ActivityStream.t()} diff --git a/lib/federation/activity_pub/types/events.ex b/lib/federation/activity_pub/types/events.ex index 221c1e888..3ec862348 100644 --- a/lib/federation/activity_pub/types/events.ex +++ b/lib/federation/activity_pub/types/events.ex @@ -3,8 +3,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Events, as: EventsManager - alias Mobilizon.Events.{Event, Participant} - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Events.{Event, Participant, ParticipantRole} + alias Mobilizon.Federation.{ActivityPub, ActivityStream} alias Mobilizon.Federation.ActivityPub.{Audience, Permission} alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils @@ -22,7 +22,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do @behaviour Entity @impl Entity - @spec create(map(), map()) :: {:ok, map()} + @spec create(map(), map()) :: {:ok, Event.t(), ActivityStream.t()} def create(args, additional) do with args <- prepare_args_for_event(args), {:ok, %Event{} = event} <- EventsManager.create_event(args), @@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do end @impl Entity - @spec update(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any() + @spec update(Event.t(), map(), map()) :: {:ok, Event.t(), ActivityStream.t()} def update(%Event{} = old_event, args, additional) do with args <- prepare_args_for_event(args), {:ok, %Event{} = new_event} <- EventsManager.update_event(old_event, args), @@ -59,7 +59,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do end @impl Entity - @spec delete(Event.t(), Actor.t(), boolean, map()) :: {:ok, Event.t()} + @spec delete(Event.t(), Actor.t(), boolean, map()) :: + {:ok, ActivityStream.t(), Actor.t(), Event.t()} def delete(%Event{url: url} = event, %Actor{} = actor, _local, _additionnal) do activity_data = %{ "type" => "Delete", @@ -82,6 +83,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do end end + @spec actor(Event.t()) :: Actor.t() | nil def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor def actor(%Event{organizer_actor_id: organizer_actor_id}), @@ -89,6 +91,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do def actor(_), do: nil + @spec group_actor(Event.t()) :: Actor.t() | nil def group_actor(%Event{attributed_to: %Actor{} = group}), do: group def group_actor(%Event{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id), @@ -96,6 +99,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do def group_actor(_), do: nil + @spec permissions(Event.t()) :: Permission.t() def permissions(%Event{draft: draft, attributed_to_id: _attributed_to_id}) do %Permission{ access: if(draft, do: nil, else: :member), @@ -105,9 +109,12 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do } end + @spec join(Event.t(), Actor.t(), boolean, map) :: + {:ok, ActivityStreams.t(), Participant.t()} + | {:error, :maximum_attendee_capacity_reached} def join(%Event{} = event, %Actor{} = actor, _local, additional) do with {:maximum_attendee_capacity, true} <- - {:maximum_attendee_capacity, check_attendee_capacity(event)}, + {:maximum_attendee_capacity, check_attendee_capacity?(event)}, role <- additional |> Map.get(:metadata, %{}) @@ -133,12 +140,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do role ) else - {:maximum_attendee_capacity, err} -> - {:maximum_attendee_capacity, err} + {:maximum_attendee_capacity, false} -> + {:error, :maximum_attendee_capacity_reached} end end - defp check_attendee_capacity(%Event{options: options} = event) 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 || @@ -147,6 +155,12 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do end # Set the participant to approved if the default role for new participants is :participant + @spec approve_if_default_role_is_participant( + Event.t(), + ActivityStreams.t(), + Participant.t(), + ParticipantRole.t() + ) :: {:ok, ActivityStreams.t(), Participant.t()} defp approve_if_default_role_is_participant(event, activity_data, participant, role) do case event do %Event{attributed_to: %Actor{id: group_id, url: group_url}} -> @@ -171,9 +185,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do end end + @spec do_approve(Event.t(), ActivityStreams.t(), Particpant.t(), ParticipantRole.t(), map()) :: + {:accept, any} | {:ok, ActivityStreams.t(), Participant.t()} defp do_approve(event, activity_data, participant, role, additionnal) do cond do - Mobilizon.Events.get_default_participant_role(event) === :participant && + Mobilizon.Events.get_default_participant_role(event) == :participant && role == :participant -> {:accept, ActivityPub.accept( @@ -183,7 +199,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do additionnal )} - Mobilizon.Events.get_default_participant_role(event) === :not_approved && + Mobilizon.Events.get_default_participant_role(event) == :not_approved && role == :not_approved -> Scheduler.pending_participation_notification(event) {:ok, activity_data, participant} @@ -194,6 +210,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do end # Prepare and sanitize arguments for events + @spec prepare_args_for_event(map) :: map defp prepare_args_for_event(args) do # If title is not set: we are not updating it args = diff --git a/lib/federation/activity_pub/types/members.ex b/lib/federation/activity_pub/types/members.ex index 8c55fa72a..a0e5f1e54 100644 --- a/lib/federation/activity_pub/types/members.ex +++ b/lib/federation/activity_pub/types/members.ex @@ -1,14 +1,15 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do @moduledoc false alias Mobilizon.Actors - alias Mobilizon.Actors.{Actor, Member} - alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Actors.{Actor, Member, MemberRole} + alias Mobilizon.Federation.{ActivityPub, ActivityStream} alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Service.Activity.Member, as: MemberActivity alias Mobilizon.Web.Endpoint require Logger import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2] + @spec update(Member.t(), map, map) :: {:ok, Member.t(), ActivityStream.t()} def update( %Member{ parent: %Actor{id: group_id} = group, @@ -24,7 +25,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do when moderator_role in [:moderator, :administrator, :creator] <- {:has_rights_to_update_role, Actors.get_member(moderator_id, group_id)}, {:is_only_admin, false} <- - {:is_only_admin, check_admins_left(member_id, group_id, current_role, updated_role)}, + {:is_only_admin, check_admins_left?(member_id, group_id, current_role, updated_role)}, {:ok, %Member{} = member} <- Actors.update_member(old_member, args), {:ok, _} <- @@ -56,6 +57,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do end # Used only when a group is suspended + @spec delete(Member.t(), Actor.t(), boolean(), map()) :: {:ok, Activity.t(), Member.t()} def delete( %Member{parent: %Actor{} = group, actor: %Actor{} = actor} = _member, %Actor{}, @@ -66,13 +68,21 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do ActivityPub.leave(group, actor, local, %{force_member_removal: true}) end + @spec actor(Member.t()) :: Actor.t() | nil def actor(%Member{actor_id: actor_id}), do: Actors.get_actor(actor_id) + @spec group_actor(Member.t()) :: Actor.t() | nil def group_actor(%Member{parent_id: parent_id}), do: Actors.get_actor(parent_id) - defp check_admins_left(member_id, group_id, current_role, updated_role) do + @spec check_admins_left?( + String.t() | integer, + String.t() | integer, + MemberRole.t(), + MemberRole.t() + ) :: boolean + defp check_admins_left?(member_id, group_id, current_role, updated_role) do Actors.is_only_administrator?(member_id, group_id) && current_role == :administrator && updated_role != :administrator end diff --git a/lib/federation/activity_pub/types/posts.ex b/lib/federation/activity_pub/types/posts.ex index 279a8ba5e..1bca51fee 100644 --- a/lib/federation/activity_pub/types/posts.ex +++ b/lib/federation/activity_pub/types/posts.ex @@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do alias Mobilizon.Actors.Actor alias Mobilizon.Federation.ActivityPub.{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.Posts.Post @@ -17,6 +18,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do @public_ap "https://www.w3.org/ns/activitystreams#Public" @impl Entity + @spec create(map(), map()) :: {:ok, Post.t(), ActivityStream.t()} def create(args, additional) do with args <- prepare_args(args), {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <- @@ -37,6 +39,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do end @impl Entity + @spec update(Post.t(), map(), map()) :: {:ok, Post.t(), ActivityStream.t()} def update(%Post{} = post, args, additional) do with args <- prepare_args(args), {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <- @@ -60,6 +63,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do end @impl Entity + @spec delete(Post.t(), Actor.t(), boolean, map) :: + {:ok, ActivityStream.t(), Actor.t(), Post.t()} def delete( %Post{ url: url, @@ -86,12 +91,15 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do end end + @spec actor(Post.t()) :: Actor.t() | nil def actor(%Post{author_id: author_id}), do: Actors.get_actor(author_id) + @spec group_actor(Post.t()) :: Actor.t() | nil def group_actor(%Post{attributed_to_id: attributed_to_id}), do: Actors.get_actor(attributed_to_id) + @spec permissions(Post.t()) :: Permission.t() def permissions(%Post{}) do %Permission{ access: :member, @@ -101,6 +109,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do } end + @spec prepare_args(map()) :: map defp prepare_args(args) do args |> Map.update(:tags, [], &ConverterUtils.fetch_tags/1) diff --git a/lib/federation/activity_pub/types/reports.ex b/lib/federation/activity_pub/types/reports.ex index ded8a1882..2d9577776 100644 --- a/lib/federation/activity_pub/types/reports.ex +++ b/lib/federation/activity_pub/types/reports.ex @@ -2,11 +2,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do @moduledoc false alias Mobilizon.{Actors, Discussions, Reports} alias Mobilizon.Actors.Actor + 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()} def flag(args, local \\ false, _additional \\ %{}) do with {:build_args, args} <- {:build_args, prepare_args_for_report(args)}, {:create_report, {:ok, %Report{} = report}} <- @@ -18,6 +20,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do 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)}, diff --git a/lib/federation/activity_pub/types/resources.ex b/lib/federation/activity_pub/types/resources.ex index 38f3b409e..07bfe2fa2 100644 --- a/lib/federation/activity_pub/types/resources.ex +++ b/lib/federation/activity_pub/types/resources.ex @@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do alias Mobilizon.Actors.Actor alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Federation.ActivityPub.Types.Entity + alias alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Resources.Resource alias Mobilizon.Service.Activity.Resource, as: ResourceActivity @@ -16,6 +17,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do @behaviour Entity @impl Entity + @spec create(map(), map()) :: {:ok, Resource.t(), ActivityStream.t()} def create(%{type: type} = args, additional) do args = case type do @@ -66,6 +68,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do end @impl Entity + @spec update(Resource.t(), map(), map()) :: {:ok, Resource.t(), ActivityStream.t()} def update( %Resource{parent_id: old_parent_id} = old_resource, %{parent_id: parent_id} = args, @@ -104,6 +107,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do end end + @spec update(Resource.t(), map(), map()) :: {:ok, Resource.t(), ActivityStream.t()} def move( %Resource{parent_id: old_parent_id} = old_resource, %{parent_id: _new_parent_id} = args, @@ -142,6 +146,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do end @impl Entity + @spec delete(Resource.t(), Actor.t(), boolean, map()) :: + {:ok, ActivityStream.t(), Actor.t(), Resource.t()} def delete( %Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource, %Actor{url: actor_url} = actor, @@ -166,11 +172,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do end end + @spec actor(Todo.t()) :: Actor.t() | nil def actor(%Resource{creator_id: creator_id}), do: Actors.get_actor(creator_id) + @spec group_actor(Todo.t()) :: Actor.t() | nil def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id) + @spec permissions(TodoList.t()) :: Permission.t() def permissions(%Resource{}) do %Permission{access: :member, create: :member, update: :member, delete: :member} end diff --git a/lib/federation/activity_pub/types/todo_lists.ex b/lib/federation/activity_pub/types/todo_lists.ex index fcabf1020..4ff9f1f9b 100644 --- a/lib/federation/activity_pub/types/todo_lists.ex +++ b/lib/federation/activity_pub/types/todo_lists.ex @@ -13,7 +13,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do @behaviour Entity @impl Entity - @spec create(map(), map()) :: {:ok, map()} + @spec create(map(), map()) :: + {:ok, TodoList.t(), ActivityStream.t()} + | {: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), @@ -26,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do end @impl Entity - @spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), Activity.t()} | any + @spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), ActivityStream.t()} | any 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), @@ -65,10 +67,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do end end + @spec actor(TodoList.t()) :: nil def actor(%TodoList{}), do: nil + @spec group_actor(TodoList.t()) :: Actor.t() | nil def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id) + @spec permissions(TodoList.t()) :: Permission.t() def permissions(%TodoList{}) do %Permission{access: :member, create: :member, update: :member, delete: :member} end diff --git a/lib/federation/activity_pub/types/todos.ex b/lib/federation/activity_pub/types/todos.ex index b12463cea..3a3e0948e 100644 --- a/lib/federation/activity_pub/types/todos.ex +++ b/lib/federation/activity_pub/types/todos.ex @@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do alias Mobilizon.Actors.Actor alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Todos.{Todo, TodoList} import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] @@ -12,7 +13,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do @behaviour Entity @impl Entity - @spec create(map(), map()) :: {:ok, map()} + @spec create(map(), map()) :: {:ok, Todo.t(), ActivityStream.t()} def create(args, additional) do with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <- Todos.create_todo(args), @@ -30,7 +31,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do end @impl Entity - @spec update(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any + @spec update(Todo.t(), map, map) :: {:ok, Todo.t(), ActivityStream.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), @@ -69,8 +70,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do end end + @spec actor(Todo.t()) :: Actor.t() | nil def actor(%Todo{creator_id: creator_id}), do: Actors.get_actor(creator_id) + @spec group_actor(Todo.t()) :: Actor.t() | nil def group_actor(%Todo{todo_list_id: todo_list_id}) do case Todos.get_todo_list(todo_list_id) do %TodoList{actor_id: group_id} -> @@ -81,6 +84,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do end end + @spec permissions(TodoList.t()) :: Permission.t() def permissions(%Todo{}) do %Permission{access: :member, create: :member, update: :member, delete: :member} end diff --git a/lib/federation/activity_stream/converter/actor.ex b/lib/federation/activity_stream/converter/actor.ex index 54c087a7c..59cc713f5 100644 --- a/lib/federation/activity_stream/converter/actor.ex +++ b/lib/federation/activity_stream/converter/actor.ex @@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do Converts an AP object data to our internal data structure. """ @impl Converter - @spec as_to_model_data(map()) :: {:ok, map()} + @spec as_to_model_data(map()) :: map() | {:error, :actor_not_allowed_type} def as_to_model_data(%{"type" => type} = data) when type in @allowed_types do avatar = download_picture(get_in(data, ["icon", "url"]), get_in(data, ["icon", "name"]), "avatar") @@ -64,7 +64,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do } end - def as_to_model_data(_), do: :error + def as_to_model_data(_), do: {:error, :actor_not_allowed_type} @doc """ Convert an actor struct to an ActivityStream representation. @@ -135,7 +135,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do end end - @spec download_picture(String.t() | nil, String.t(), String.t()) :: map() + @spec download_picture(String.t() | nil, String.t(), String.t()) :: map() | nil defp download_picture(nil, _name, _default_name), do: nil defp download_picture(url, name, default_name) do diff --git a/lib/federation/activity_stream/converter/comment.ex b/lib/federation/activity_stream/converter/comment.ex index d4ec756d9..f653009e1 100644 --- a/lib/federation/activity_stream/converter/comment.ex +++ b/lib/federation/activity_stream/converter/comment.ex @@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do Converts an AP object data to our internal data structure. """ @impl Converter - @spec as_to_model_data(map) :: {:ok, map} | {:error, any()} + @spec as_to_model_data(map) :: map | {:error, any()} def as_to_model_data(object) do Logger.debug("We're converting raw ActivityStream data to a comment entity") Logger.debug(inspect(object)) diff --git a/lib/federation/activity_stream/converter/converter.ex b/lib/federation/activity_stream/converter/converter.ex index 73b1ca69b..ceef41ff5 100644 --- a/lib/federation/activity_stream/converter/converter.ex +++ b/lib/federation/activity_stream/converter/converter.ex @@ -8,6 +8,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter do @type model_data :: map() - @callback as_to_model_data(as_data :: ActivityStream.t()) :: model_data() + @callback as_to_model_data(as_data :: ActivityStream.t()) :: model_data() | {:error, any()} @callback model_to_as(model :: struct()) :: ActivityStream.t() end diff --git a/lib/federation/activity_stream/converter/discussion.ex b/lib/federation/activity_stream/converter/discussion.ex index 2029923d6..9cfe48893 100644 --- a/lib/federation/activity_stream/converter/discussion.ex +++ b/lib/federation/activity_stream/converter/discussion.ex @@ -12,6 +12,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.Converter.Discussion, as: DiscussionConverter alias Mobilizon.Storage.Repo + import Mobilizon.Service.Guards, only: [is_valid_string: 1] require Logger @@ -45,20 +46,27 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do end @impl Converter - @spec as_to_model_data(map) :: {:ok, map} | {:error, any()} - def as_to_model_data(%{"type" => "Note", "name" => name} = object) when not is_nil(name) do - with creator_url <- Map.get(object, "actor"), - {:ok, %Actor{id: creator_id, suspended: false}} <- + @spec as_to_model_data(map) :: map() | {:error, any()} + 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} -> + %{actor_id: actor_id, creator_id: creator_id, title: name, url: object["id"]} + + {:error, error} -> + {:error, error} + end + end + + @spec extract_actors(map()) :: %{actor_id: String.t(), creator_id: String.t()} | {:error, any()} + 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}} <- ActivityPubActor.get_or_fetch_actor_by_url(creator_url), - actor_url <- Map.get(object, "attributedTo"), {:ok, %Actor{id: actor_id, suspended: false}} <- ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do - %{ - title: name, - actor_id: actor_id, - creator_id: creator_id, - url: object["id"] - } + %{actor_id: actor_id, creator_id: creator_id} + else + {:error, error} -> {:error, error} end end end diff --git a/lib/federation/activity_stream/converter/event.ex b/lib/federation/activity_stream/converter/event.ex index 65825a32e..b7d2c187c 100644 --- a/lib/federation/activity_stream/converter/event.ex +++ b/lib/federation/activity_stream/converter/event.ex @@ -45,7 +45,7 @@ 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) :: {:ok, map()} | {:error, any()} + @spec as_to_model_data(map) :: map() | {:error, any()} | :error def as_to_model_data(object) do with {%Actor{id: actor_id}, attributed_to} <- maybe_fetch_actor_and_attributed_to_id(object), diff --git a/lib/federation/activity_stream/converter/member.ex b/lib/federation/activity_stream/converter/member.ex index be4db50a5..3d33d1ae2 100644 --- a/lib/federation/activity_stream/converter/member.ex +++ b/lib/federation/activity_stream/converter/member.ex @@ -33,6 +33,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Member do } end + @spec as_to_model_data(map()) :: map() def as_to_model_data(%{ "type" => "Member", "actor" => actor, diff --git a/lib/federation/activity_stream/converter/post.ex b/lib/federation/activity_stream/converter/post.ex index 0cb0dfee4..40f3eab43 100644 --- a/lib/federation/activity_stream/converter/post.ex +++ b/lib/federation/activity_stream/converter/post.ex @@ -18,6 +18,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do process_pictures: 2 ] + import Mobilizon.Service.Guards, only: [is_valid_string: 1] + @behaviour Converter defimpl Convertible, for: Post do @@ -63,15 +65,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do Converts an AP object data to our internal data structure. """ @impl Converter - @spec as_to_model_data(map) :: {:ok, map} | {:error, any()} + @spec as_to_model_data(map) :: map() | {:error, any()} def as_to_model_data( %{"type" => "Article", "actor" => creator, "attributedTo" => group_uri} = object ) do with {:ok, %Actor{id: attributed_to_id} = group} <- get_actor(group_uri), - {:ok, %Actor{id: author_id}} <- get_actor(creator), - {:visibility, visibility} <- {:visibility, get_visibility(object, group)}, - [description: description, picture_id: picture_id, medias: medias] <- - process_pictures(object, attributed_to_id) do + {:ok, %Actor{id: author_id}} <- get_actor(creator) do + [description: description, picture_id: picture_id, medias: medias] = + process_pictures(object, attributed_to_id) + %{ title: object["name"], body: description, @@ -82,7 +84,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do publish_at: object["published"], picture_id: picture_id, medias: medias, - visibility: visibility, + visibility: get_visibility(object, group), draft: object["draft"] == true } else @@ -92,11 +94,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do end @spec get_actor(String.t() | map() | nil) :: {:ok, Actor.t()} | {:error, String.t()} - defp get_actor(nil), do: {:error, "nil property found for actor data"} - - defp get_actor(actor), + defp get_actor(actor) when is_valid_string(actor), do: actor |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url() + defp get_actor(_), do: {:error, "nil property found for actor data"} + + @spec to_date(DateTime.t() | NaiveDateTime.t() | nil) :: String.t() | nil defp to_date(nil), do: nil defp to_date(%DateTime{} = date), do: DateTime.to_iso8601(date) defp to_date(%NaiveDateTime{} = date), do: NaiveDateTime.to_iso8601(date) diff --git a/lib/federation/activity_stream/converter/resource.ex b/lib/federation/activity_stream/converter/resource.ex index a0d8ca6e7..becf41ded 100644 --- a/lib/federation/activity_stream/converter/resource.ex +++ b/lib/federation/activity_stream/converter/resource.ex @@ -56,18 +56,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Resource do Converts an AP object data to our internal data structure. """ @impl Converter - @spec as_to_model_data(map) :: {:ok, map} | {:error, any()} + @spec as_to_model_data(map) :: map() | {:error, any()} def as_to_model_data(%{"type" => type, "actor" => creator, "attributedTo" => group} = object) do with {:ok, %Actor{id: actor_id, resources_url: resources_url}} <- get_actor(group), - {:ok, %Actor{id: creator_id}} <- get_actor(creator), - parent_id <- get_parent_id(object["context"], resources_url) do + {:ok, %Actor{id: creator_id}} <- get_actor(creator) do data = %{ title: object["name"], summary: object["summary"], url: object["id"], actor_id: actor_id, creator_id: creator_id, - parent_id: parent_id, + parent_id: get_parent_id(object["context"], resources_url), published_at: object["published"] } diff --git a/lib/federation/activity_stream/converter/todo.ex b/lib/federation/activity_stream/converter/todo.ex index fdd6f38c8..0ac0eebb1 100644 --- a/lib/federation/activity_stream/converter/todo.ex +++ b/lib/federation/activity_stream/converter/todo.ex @@ -47,7 +47,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do Converts an AP object data to our internal data structure. """ @impl Converter - @spec as_to_model_data(map) :: {:ok, map} | {:error, any()} + @spec as_to_model_data(map) :: map() | {:error, any()} def as_to_model_data( %{"type" => "Todo", "actor" => actor_url, "todoList" => todo_list_url} = object ) do diff --git a/lib/federation/activity_stream/converter/todo_list.ex b/lib/federation/activity_stream/converter/todo_list.ex index 76ad46fc7..4491ab389 100644 --- a/lib/federation/activity_stream/converter/todo_list.ex +++ b/lib/federation/activity_stream/converter/todo_list.ex @@ -37,7 +37,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do Converts an AP object data to our internal data structure. """ @impl Converter - @spec as_to_model_data(map) :: {:ok, map} | {:error, any()} + @spec as_to_model_data(map) :: map() | {:error, :group_not_found} def as_to_model_data(%{"type" => "TodoList", "actor" => actor_url} = object) do case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do {:ok, %Actor{type: :Group, id: group_id} = _group} -> diff --git a/lib/federation/http_signatures/signature.ex b/lib/federation/http_signatures/signature.ex index e68251d3b..07ec0a82b 100644 --- a/lib/federation/http_signatures/signature.ex +++ b/lib/federation/http_signatures/signature.ex @@ -19,7 +19,7 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do @spec key_id_to_actor_url(String.t()) :: String.t() def key_id_to_actor_url(key_id) do - %{path: path} = + %URI{path: path} = uri = key_id |> URI.parse() @@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do if is_nil(path) do uri else - Map.put(uri, :path, String.trim_trailing(path, "/publickey")) + %URI{uri | path: String.trim_trailing(path, "/publickey")} end URI.to_string(uri) @@ -78,6 +78,9 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do end end + @spec fetch_public_key(Plug.Conn.t()) :: + {:ok, String.t()} + | {:error, :actor_fetch_error | :actor_not_fetchable | :pem_decode_error} def fetch_public_key(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), actor_id <- key_id_to_actor_url(kid), @@ -87,6 +90,9 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do end end + @spec refetch_public_key(Plug.Conn.t()) :: + {:ok, String.t()} + | {:error, :actor_fetch_error | :actor_not_fetchable | :pem_decode_error} def refetch_public_key(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), actor_id <- key_id_to_actor_url(kid), @@ -97,6 +103,7 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do end end + @spec sign(Actor.t(), map()) :: String.t() def sign(%Actor{domain: domain, keys: keys} = actor, headers) when is_nil(domain) do Logger.debug("Signing a payload on behalf of #{actor.url}") Logger.debug("headers") @@ -112,14 +119,17 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do raise ArgumentError, message: "Can't do a signature on remote actor #{url}" end + @spec generate_date_header :: String.t() def generate_date_header, do: generate_date_header(NaiveDateTime.utc_now()) def generate_date_header(%NaiveDateTime{} = date) do Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") end + @spec generate_request_target(String.t(), String.t()) :: String.t() def generate_request_target(method, path), do: "#{method} #{path}" + @spec build_digest(String.t()) :: String.t() def build_digest(body) do "SHA-256=#{:sha256 |> :crypto.hash(body) |> Base.encode64()}" end diff --git a/lib/federation/web_finger/web_finger.ex b/lib/federation/web_finger/web_finger.ex index 1eddb189b..872cd5892 100644 --- a/lib/federation/web_finger/web_finger.ex +++ b/lib/federation/web_finger/web_finger.ex @@ -66,10 +66,10 @@ defmodule Mobilizon.Federation.WebFinger do end end - @spec represent_actor(Actor.t()) :: struct() + @spec represent_actor(Actor.t()) :: map() + @spec represent_actor(Actor.t(), String.t()) :: map() def represent_actor(%Actor{} = actor), do: represent_actor(actor, "JSON") - @spec represent_actor(Actor.t(), String.t()) :: struct() def represent_actor(%Actor{} = actor, "JSON") do links = [ @@ -141,11 +141,15 @@ defmodule Mobilizon.Federation.WebFinger do @doc """ Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`) """ - @spec find_webfinger_endpoint(String.t()) :: String.t() + @spec find_webfinger_endpoint(String.t()) :: + {:ok, String.t()} | {:error, :link_not_found} | {:error, any()} def find_webfinger_endpoint(domain) when is_binary(domain) do with {:ok, %{body: body}} <- fetch_document("http://#{domain}/.well-known/host-meta"), link_template when is_binary(link_template) <- find_link_from_template(body) do {:ok, link_template} + else + {:error, :link_not_found} -> {:error, :link_not_found} + {:error, error} -> {:error, error} end end diff --git a/lib/graphql/api/reports.ex b/lib/graphql/api/reports.ex index e56547654..218467eb5 100644 --- a/lib/graphql/api/reports.ex +++ b/lib/graphql/api/reports.ex @@ -15,12 +15,13 @@ defmodule Mobilizon.GraphQL.API.Reports do @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()} def report(args) do - case {:make_activity, ActivityPub.flag(args, Map.get(args, :forward, false) == true)} do - {:make_activity, {:ok, %Activity{} = activity, %Report{} = report}} -> + case ActivityPub.flag(args, Map.get(args, :forward, false) == true) do + {:ok, %Activity{} = activity, %Report{} = report} -> {:ok, activity, report} - {:make_activity, err} -> + err -> {:error, err} end end @@ -28,10 +29,12 @@ defmodule Mobilizon.GraphQL.API.Reports do @doc """ Update the state of a report """ + @spec update_report_status(Actor.t(), Report.t(), ReportStatus.t()) :: + {:ok, Report.t()} | {:error, String.t()} def update_report_status(%Actor{} = actor, %Report{} = report, state) do with {:valid_state, true} <- {:valid_state, ReportStatus.valid_value?(state)}, - {:ok, report} <- ReportsAction.update_report(report, %{"status" => state}), + {:ok, %Report{} = report} <- ReportsAction.update_report(report, %{"status" => state}), {:ok, _} <- Admin.log_action(actor, "update", report) do {:ok, report} else @@ -42,7 +45,8 @@ defmodule Mobilizon.GraphQL.API.Reports do @doc """ Create a note on a report """ - @spec create_report_note(Report.t(), Actor.t(), String.t()) :: {:ok, Note.t()} + @spec create_report_note(Report.t(), Actor.t(), String.t()) :: + {:ok, Note.t()} | {:error, String.t()} def create_report_note( %Report{id: report_id}, %Actor{id: moderator_id, user_id: user_id} = moderator, @@ -67,7 +71,7 @@ defmodule Mobilizon.GraphQL.API.Reports do @doc """ Delete a report note """ - @spec delete_report_note(Note.t(), Actor.t()) :: {:ok, Note.t()} + @spec delete_report_note(Note.t(), Actor.t()) :: {:ok, Note.t()} | {:error, String.t()} def delete_report_note( %Note{moderator_id: note_moderator_id} = note, %Actor{id: moderator_id, user_id: user_id} = moderator diff --git a/lib/graphql/api/utils.ex b/lib/graphql/api/utils.ex index e15b0e0b9..1effea583 100644 --- a/lib/graphql/api/utils.ex +++ b/lib/graphql/api/utils.ex @@ -10,7 +10,7 @@ defmodule Mobilizon.GraphQL.API.Utils do @doc """ Creates HTML content from text and mentions """ - @spec make_content_html(String.t(), list(), String.t()) :: String.t() + @spec make_content_html(String.t(), list(), String.t()) :: {String.t(), list(), list()} def make_content_html(text, additional_tags, content_type) do with {text, mentions, tags} <- format_input(text, content_type, []) do {text, mentions, additional_tags ++ Enum.map(tags, fn {_, tag} -> tag end)} diff --git a/lib/graphql/resolvers/comment.ex b/lib/graphql/resolvers/comment.ex index 0e2998bb6..26435deb9 100644 --- a/lib/graphql/resolvers/comment.ex +++ b/lib/graphql/resolvers/comment.ex @@ -66,7 +66,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, %CommentModel{actor_id: comment_actor_id} = comment <- Mobilizon.Discussions.get_comment_with_preload(comment_id), - true <- actor_id === comment_actor_id, + true <- actor_id == comment_actor_id, {:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do {:ok, comment} end diff --git a/lib/graphql/resolvers/event/utils.ex b/lib/graphql/resolvers/event/utils.ex index e6c7e8c24..613dcadd4 100644 --- a/lib/graphql/resolvers/event/utils.ex +++ b/lib/graphql/resolvers/event/utils.ex @@ -6,7 +6,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Event.Utils do alias Mobilizon.Actors.Actor alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityPub.Permission + import Mobilizon.Service.Guards, only: [is_valid_string: 1] + @spec can_event_be_updated_by?(%Event{id: String.t()}, Actor.t()) :: + boolean def can_event_be_updated_by?( %Event{attributed_to: %Actor{type: :Group}} = event, %Actor{} = actor_member @@ -21,10 +24,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Event.Utils do Event.can_be_managed_by?(event, actor_member_id) end + @spec can_event_be_deleted_by?(%Event{id: String.t(), url: String.t()}, Actor.t()) :: + boolean def can_event_be_deleted_by?( - %Event{attributed_to: %Actor{type: :Group}} = event, + %Event{attributed_to: %Actor{type: :Group}, id: event_id, url: event_url} = event, %Actor{} = actor_member - ) do + ) + when is_valid_string(event_id) and is_valid_string(event_url) do Permission.can_delete_group_object?(actor_member, event) end diff --git a/lib/graphql/resolvers/member.ex b/lib/graphql/resolvers/member.ex index 44b9caed4..c41e19f4a 100644 --- a/lib/graphql/resolvers/member.ex +++ b/lib/graphql/resolvers/member.ex @@ -101,7 +101,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do with %Actor{id: actor_id} <- Users.get_actor_for_user(user), %Member{actor: %Actor{id: member_actor_id}} = member <- Actors.get_member(member_id), - {:is_same_actor, true} <- {:is_same_actor, member_actor_id === actor_id}, + {:is_same_actor, true} <- {:is_same_actor, member_actor_id == actor_id}, {:ok, _activity, %Member{} = member} <- ActivityPub.accept( :invite, @@ -119,7 +119,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do with %Actor{id: actor_id} <- Users.get_actor_for_user(user), {:invitation_exists, %Member{actor: %Actor{id: member_actor_id}} = member} <- {:invitation_exists, Actors.get_member(member_id)}, - {:is_same_actor, true} <- {:is_same_actor, member_actor_id === actor_id}, + {:is_same_actor, true} <- {:is_same_actor, member_actor_id == actor_id}, {:ok, _activity, %Member{} = member} <- ActivityPub.reject( :invite, diff --git a/lib/graphql/resolvers/todos.ex b/lib/graphql/resolvers/todos.ex index e6c2d504f..825500531 100644 --- a/lib/graphql/resolvers/todos.ex +++ b/lib/graphql/resolvers/todos.ex @@ -95,6 +95,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do ActivityPub.create(:todo_list, Map.put(args, :actor_id, group_id), true, %{}) do {:ok, todo_list} else + {:actor, nil} -> + {:error, dgettext("errors", "No profile found for user")} + {:member, _} -> {:error, dgettext("errors", "Profile is not member of group")} end @@ -187,6 +190,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do ActivityPub.create(:todo, Map.put(args, :creator_id, actor_id), true, %{}) do {:ok, todo} else + {:actor, nil} -> + {:error, dgettext("errors", "No profile found for user")} + {:todo_list, _} -> {:error, dgettext("errors", "Todo list doesn't exist")} @@ -212,6 +218,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do ActivityPub.update(todo, args, true, %{}) do {:ok, todo} else + {:actor, nil} -> + {:error, dgettext("errors", "No profile found for user")} + {:todo_list, _} -> {:error, dgettext("errors", "Todo list doesn't exist")} diff --git a/lib/graphql/resolvers/user.ex b/lib/graphql/resolvers/user.ex index 24ba9c7c8..a141b014e 100644 --- a/lib/graphql/resolvers/user.ex +++ b/lib/graphql/resolvers/user.ex @@ -65,9 +65,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do refresh_token: _refresh_token, user: %User{} = user } = user_and_tokens} <- Authenticator.authenticate(email, password), - {:ok, %User{} = user} <- update_user_login_information(user, context), - user_and_tokens <- Map.put(user_and_tokens, :user, user) do - {:ok, user_and_tokens} + {:ok, %User{} = user} <- update_user_login_information(user, context) do + {:ok, %{user_and_tokens | user: user}} else {:error, :user_not_found} -> {:error, :user_not_found} @@ -133,7 +132,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do - create the user - send a validation email to the user """ - @spec create_user(any, map, any) :: tuple + @spec create_user(any, %{email: String.t()}, any) :: tuple def create_user(_parent, %{email: email} = args, _resolution) do with :registration_ok <- check_registration_config(email), :not_deny_listed <- check_registration_denylist(email), @@ -160,20 +159,21 @@ defmodule Mobilizon.GraphQL.Resolvers.User do end end - @spec check_registration_config(map) :: atom + @spec check_registration_config(String.t()) :: atom defp check_registration_config(email) do cond do Config.instance_registrations_open?() -> :registration_ok Config.instance_registrations_allowlist?() -> - check_allow_listed_email?(email) + check_allow_listed_email(email) true -> :registration_closed end end + @spec check_registration_denylist(String.t()) :: :deny_listed | :not_deny_listed defp check_registration_denylist(email) do # Remove everything behind the + email = String.replace(email, ~r/(\+.*)(?=\@)/, "") @@ -183,8 +183,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do else: :not_deny_listed end - @spec check_allow_listed_email?(String.t()) :: :registration_ok | :not_allowlisted - defp check_allow_listed_email?(email) do + @spec check_allow_listed_email(String.t()) :: :registration_ok | :not_allowlisted + defp check_allow_listed_email(email) do if email_in_list(email, Config.instance_registrations_allowlist()), do: :registration_ok, else: :not_allowlisted @@ -199,12 +199,14 @@ defmodule Mobilizon.GraphQL.Resolvers.User do @doc """ Validate an user, get its actor and a token """ + @spec validate_user(map(), %{token: String.t()}, map()) :: {:ok, map()} def validate_user(_parent, %{token: token}, _resolution) do with {:check_confirmation_token, {:ok, %User{} = user}} <- {:check_confirmation_token, Email.User.check_confirmation_token(token)}, - {:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)}, - {:ok, %{access_token: access_token, refresh_token: refresh_token}} <- - Authenticator.generate_tokens(user) do + {:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)} do + {:ok, %{access_token: access_token, refresh_token: refresh_token}} = + Authenticator.generate_tokens(user) + {:ok, %{ access_token: access_token, @@ -267,12 +269,16 @@ defmodule Mobilizon.GraphQL.Resolvers.User do @doc """ Reset the password from an user """ + @spec reset_password(map(), %{password: String.t(), token: String.t()}, map()) :: + {:ok, map()} | {:error, String.t()} def reset_password(_parent, %{password: password, token: token}, _resolution) do - with {:ok, %User{email: email} = user} <- - Email.User.check_reset_password_token(password, token), - {:ok, %{access_token: access_token, refresh_token: refresh_token}} <- - Authenticator.authenticate(email, password) do - {:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}} + case Email.User.check_reset_password_token(password, token) do + {:ok, %User{email: email} = user} -> + {:ok, tokens} = Authenticator.authenticate(email, password) + {:ok, Map.put(tokens, :user, user)} + + {:error, error} -> + {:error, error} end end @@ -369,6 +375,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do |> Repo.update() do {:ok, user} else + {:can_change_password, false} -> + {:error, dgettext("errors", "You cannot change your password.")} + {:current_password, _} -> {:error, dgettext("errors", "The current password is invalid")} @@ -408,14 +417,18 @@ defmodule Mobilizon.GraphQL.Resolvers.User do {:ok, user} else - {:current_password, _} -> + {:current_password, {:error, _}} -> {:error, dgettext("errors", "The password provided is invalid")} {:same_email, true} -> {:error, dgettext("errors", "The new email must be different")} - {:email_valid, _} -> + {:email_valid, false} -> {:error, dgettext("errors", "The new email doesn't seem to be valid")} + + {:error, %Ecto.Changeset{} = err} -> + Logger.debug(inspect(err)) + {:error, dgettext("errors", "Failed to update user email")} end end @@ -423,12 +436,21 @@ defmodule Mobilizon.GraphQL.Resolvers.User do {:error, dgettext("errors", "You need to be logged-in to change your email")} end + @spec validate_email(map(), %{token: String.t()}, map()) :: + {:ok, User.t()} | {:error, String.t()} def validate_email(_parent, %{token: token}, _resolution) do - with {:get, %User{} = user} <- {:get, Users.get_user_by_activation_token(token)}, - {:ok, %User{} = user} <- Users.validate_email(user) do - {:ok, user} - else - {:get, nil} -> + case Users.get_user_by_activation_token(token) do + %User{} = user -> + case Users.validate_email(user) do + {:ok, %User{} = user} -> + {:ok, user} + + {:error, %Ecto.Changeset{} = err} -> + Logger.debug(inspect(err)) + {:error, dgettext("errors", "Failed to validate user email")} + end + + nil -> {:error, dgettext("errors", "Invalid activation token")} end end @@ -547,12 +569,17 @@ defmodule Mobilizon.GraphQL.Resolvers.User do def update_locale(_parent, %{locale: locale}, %{ context: %{current_user: %User{locale: current_locale} = user} }) do - with true <- current_locale != locale, - {:ok, %User{} = updated_user} <- Users.update_user(user, %{locale: locale}) do - {:ok, updated_user} + if current_locale != locale do + case Users.update_user(user, %{locale: locale}) do + {:ok, %User{} = updated_user} -> + {:ok, updated_user} + + {:error, %Ecto.Changeset{} = err} -> + Logger.debug(err) + {:error, dgettext("errors", "Error while updating locale")} + end else - false -> - {:ok, user} + {:ok, user} end end diff --git a/lib/graphql/schema/tag.ex b/lib/graphql/schema/tag.ex index f2db09449..d995c8cff 100644 --- a/lib/graphql/schema/tag.ex +++ b/lib/graphql/schema/tag.ex @@ -23,6 +23,7 @@ defmodule Mobilizon.GraphQL.Schema.TagType do object :tag_queries do @desc "Get the list of tags" field :tags, non_null(list_of(:tag)) do + arg(:filter, :string, description: "The filter to apply to the search") arg(:page, :integer, default_value: 1, description: "The page in the paginated tags list") arg(:limit, :integer, default_value: 10, description: "The limit of tags per page") resolve(&Tag.list_tags/3) diff --git a/lib/mix/tasks/mobilizon/actors/refresh.ex b/lib/mix/tasks/mobilizon/actors/refresh.ex index 893af5f2b..9c1b20d06 100644 --- a/lib/mix/tasks/mobilizon/actors/refresh.ex +++ b/lib/mix/tasks/mobilizon/actors/refresh.ex @@ -71,9 +71,6 @@ defmodule Mix.Tasks.Mobilizon.Actors.Refresh do Actor #{preferred_username} refreshed """) - {:actor, nil} -> - shell_error("Error: No such actor") - {:error, err} when is_binary(err) -> shell_error(err) diff --git a/lib/mix/tasks/mobilizon/actors/utils.ex b/lib/mix/tasks/mobilizon/actors/utils.ex index fd7e2e911..38e09e176 100644 --- a/lib/mix/tasks/mobilizon/actors/utils.ex +++ b/lib/mix/tasks/mobilizon/actors/utils.ex @@ -25,7 +25,7 @@ defmodule Mix.Tasks.Mobilizon.Actors.Utils do end # Profile from name - @spec username_and_name(String.t() | nil, String.t() | nil) :: String.t() + @spec username_and_name(String.t() | nil, String.t() | nil) :: {String.t(), String.t()} def username_and_name(nil, profile_name) do {generate_username(profile_name), profile_name} end diff --git a/lib/mix/tasks/mobilizon/common.ex b/lib/mix/tasks/mobilizon/common.ex index 8cc3ccffe..cee4f6b8a 100644 --- a/lib/mix/tasks/mobilizon/common.ex +++ b/lib/mix/tasks/mobilizon/common.ex @@ -61,7 +61,7 @@ defmodule Mix.Tasks.Mobilizon.Common do else: IO.puts(message) end - @spec shell_error(String.t()) :: :ok + @spec shell_error(String.t(), Keyword.t()) :: nil | no_return def shell_error(message, options \\ []) do if mix_shell?() do Mix.shell().error(message) diff --git a/lib/mix/tasks/mobilizon/instance.ex b/lib/mix/tasks/mobilizon/instance.ex index af4d844b1..c2529b117 100644 --- a/lib/mix/tasks/mobilizon/instance.ex +++ b/lib/mix/tasks/mobilizon/instance.ex @@ -35,6 +35,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do @preferred_cli_env "prod" @shortdoc "Generates a new config" + @spec run(list(binary())) :: no_return def run(["gen" | options]) do {options, [], []} = OptionParser.parse( diff --git a/lib/mix/tasks/mobilizon/users/modify.ex b/lib/mix/tasks/mobilizon/users/modify.ex index 42309b54f..ae02ed564 100644 --- a/lib/mix/tasks/mobilizon/users/modify.ex +++ b/lib/mix/tasks/mobilizon/users/modify.ex @@ -54,13 +54,20 @@ defmodule Mix.Tasks.Mobilizon.Users.Modify do ), {:makes_changes, true} <- {:makes_changes, attrs != %{}}, {:ok, %User{} = user} <- Users.update_user(user, attrs) do + status = + case user.confirmed_at do + %DateTime{} = confirmed_at -> + "Activated on #{DateTime.to_string(confirmed_at)} (UTC)" + + _ -> + "disabled" + end + shell_info(""" An user has been modified with the following information: - email: #{user.email} - Role: #{user.role} - - account status: #{if user.confirmed_at, - do: "activated on #{DateTime.to_string(user.confirmed_at)} (UTC)", - else: "disabled"} + - account status: #{status} """) else {:makes_changes, false} -> diff --git a/lib/mix/tasks/mobilizon/users/new.ex b/lib/mix/tasks/mobilizon/users/new.ex index b34f730d5..db013f7c1 100644 --- a/lib/mix/tasks/mobilizon/users/new.ex +++ b/lib/mix/tasks/mobilizon/users/new.ex @@ -78,6 +78,8 @@ defmodule Mix.Tasks.Mobilizon.Users.New do shell_error("mobilizon.users.new requires an email as argument") end + @spec create_user(String.t(), String.t() | nil, String.t(), Keyword.t()) :: + {:ok, User.t()} | {:error, Ecto.Changeset.t()} defp create_user(email, provider, password, options) do role = get_role(options) @@ -96,6 +98,8 @@ defmodule Mix.Tasks.Mobilizon.Users.New do end end + @spec create_database_user(String.t(), String.t(), role()) :: + {:ok, User.t()} | {:error, Ecto.Changeset.t()} defp create_database_user(email, password, role) do Users.register(%{ email: email, @@ -107,6 +111,8 @@ defmodule Mix.Tasks.Mobilizon.Users.New do }) end + @spec create_user_from_provider(String.t(), String.t(), role()) :: + {:ok, User.t()} | {:error, Ecto.Changeset.t()} defp create_user_from_provider(email, provider, role) do Users.create_external(email, provider, %{role: role}) end @@ -137,6 +143,7 @@ defmodule Mix.Tasks.Mobilizon.Users.New do end end + @spec check_password_and_provider_options(Keyword.t()) :: nil | no_return() defp check_password_and_provider_options(options) do if Keyword.get(options, :password) != nil && Keyword.get(options, :provider) != nil do shell_error(""" diff --git a/lib/mix/tasks/mobilizon/users/show.ex b/lib/mix/tasks/mobilizon/users/show.ex index 60ed9accc..c559f87f7 100644 --- a/lib/mix/tasks/mobilizon/users/show.ex +++ b/lib/mix/tasks/mobilizon/users/show.ex @@ -17,11 +17,18 @@ defmodule Mix.Tasks.Mobilizon.Users.Show do with {:ok, %User{} = user} <- Users.get_user_by_email(email), actors <- Users.get_actors_for_user(user) do + status = + case user.confirmed_at do + %DateTime{} = confirmed_at -> + "Activated on #{DateTime.to_string(confirmed_at)} (UTC)" + + _ -> + "disabled" + end + shell_info(""" Informations for the user #{user.email}: - - account status: #{if user.confirmed_at, - do: "Activated on #{DateTime.to_string(user.confirmed_at)} (UTC)", - else: "disabled"} + - account status: #{status} - Role: #{user.role} #{display_actors(actors)} """) diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 07e7e5d05..66d4f0c37 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -1466,7 +1466,7 @@ defmodule Mobilizon.Actors do @spec actors_for_location(Ecto.Query.t(), String.t(), integer()) :: Ecto.Query.t() defp actors_for_location(query, location, radius) - when is_valid_string?(location) and not is_nil(radius) do + when is_valid_string(location) and not is_nil(radius) do with {lon, lat} <- Geohax.decode(location), point <- Geo.WKT.decode!("SRID=4326;POINT(#{lon} #{lat})") do query diff --git a/lib/mobilizon/config.ex b/lib/mobilizon/config.ex index 4b7496902..bf0ce8c65 100644 --- a/lib/mobilizon/config.ex +++ b/lib/mobilizon/config.ex @@ -4,7 +4,6 @@ defmodule Mobilizon.Config do """ alias Mobilizon.Actors - alias Mobilizon.Actors.Actor alias Mobilizon.Service.GitStatus @spec instance_config :: keyword @@ -317,14 +316,14 @@ defmodule Mobilizon.Config do @spec create_cache(atom()) :: integer() defp create_cache(:anonymous_actor_id) do - with {:ok, %Actor{id: actor_id}} <- Actors.get_or_create_internal_actor("anonymous") do + with {:ok, %{id: actor_id}} <- Actors.get_or_create_internal_actor("anonymous") do actor_id end end @spec create_cache(atom()) :: integer() defp create_cache(:relay_actor_id) do - with {:ok, %Actor{id: actor_id}} <- Actors.get_or_create_internal_actor("relay") do + with {:ok, %{id: actor_id}} <- Actors.get_or_create_internal_actor("relay") do actor_id end end diff --git a/lib/mobilizon/discussions/discussions.ex b/lib/mobilizon/discussions/discussions.ex index 5c10d0e92..77106c470 100644 --- a/lib/mobilizon/discussions/discussions.ex +++ b/lib/mobilizon/discussions/discussions.ex @@ -249,8 +249,7 @@ defmodule Mobilizon.Discussions do {:ok, comment} end else - comment - |> Repo.delete() + Repo.delete(comment) end end diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index b4a46d7b1..52b0f5ce7 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -206,6 +206,7 @@ defmodule Mobilizon.Events.Event do defp put_tags(%Changeset{} = changeset, _), do: changeset + @spec process_tag(map() | Tag.t()) :: Tag.t() | Ecto.Changeset.t() # We need a changeset instead of a raw struct because of slug which is generated in changeset defp process_tag(%{id: id} = _tag) do Events.get_tag(id) @@ -248,13 +249,8 @@ defmodule Mobilizon.Events.Event do # In case the provided picture is an existing one @spec put_picture(Changeset.t(), map) :: Changeset.t() defp put_picture(%Changeset{} = changeset, %{picture: %{media_id: id} = _picture}) do - case Medias.get_media!(id) do - %Media{} = picture -> - put_assoc(changeset, :picture, picture) - - _ -> - changeset - end + %Media{} = picture = Medias.get_media!(id) + put_assoc(changeset, :picture, picture) end # In case it's a new picture diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index af3bb0f25..4fc85511e 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -631,9 +631,10 @@ defmodule Mobilizon.Events do @doc """ Returns the list of tags. """ - @spec list_tags(integer | nil, integer | nil) :: [Tag.t()] - def list_tags(page \\ nil, limit \\ nil) do + @spec list_tags(String.t() | nil, integer | nil, integer | nil) :: [Tag.t()] + def list_tags(filter \\ nil, page \\ nil, limit \\ nil) do Tag + |> tag_filter(filter) |> Page.paginate(page, limit) |> Repo.all() end @@ -1396,7 +1397,7 @@ defmodule Mobilizon.Events do end @spec events_for_tags(Ecto.Query.t(), map()) :: Ecto.Query.t() - defp events_for_tags(query, %{tags: tags}) when is_valid_string?(tags) do + defp events_for_tags(query, %{tags: tags}) when is_valid_string(tags) do query |> join(:inner, [q], te in "events_tags", on: q.id == te.event_id) |> join(:inner, [q, ..., te], t in Tag, on: te.tag_id == t.id) @@ -1410,7 +1411,7 @@ defmodule Mobilizon.Events do do: query defp events_for_location(query, %{location: location, radius: radius}) - when is_valid_string?(location) and not is_nil(radius) do + when is_valid_string(location) and not is_nil(radius) do with {lon, lat} <- Geohax.decode(location), point <- Geo.WKT.decode!("SRID=4326;POINT(#{lon} #{lat})") do query @@ -1471,6 +1472,16 @@ defmodule Mobilizon.Events do from(t in Tag, where: t.title == ^title, limit: 1) end + @spec tag_filter(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t() + defp tag_filter(query, nil), do: query + defp tag_filter(query, ""), do: query + + defp tag_filter(query, filter) when is_binary(filter) do + query + |> where([q], ilike(q.slug, ^"%#{filter}%")) + |> or_where([q], ilike(q.title, ^"%#{filter}%")) + end + @spec tags_for_event_query(integer) :: Ecto.Query.t() defp tags_for_event_query(event_id) do from( diff --git a/lib/mobilizon/events/participant.ex b/lib/mobilizon/events/participant.ex index 1670e786e..b490a379d 100644 --- a/lib/mobilizon/events/participant.ex +++ b/lib/mobilizon/events/participant.ex @@ -10,7 +10,7 @@ defmodule Mobilizon.Events.Participant do alias Mobilizon.Actors.Actor alias Mobilizon.Events alias Mobilizon.Events.{Event, ParticipantRole} - alias Mobilizon.Web.Email.Checker + alias Mobilizon.Events.Participant.Metadata alias Mobilizon.Web.Endpoint @@ -24,7 +24,6 @@ defmodule Mobilizon.Events.Participant do @required_attrs [:url, :role, :event_id, :actor_id] @attrs @required_attrs - @metadata_attrs [:email, :confirmation_token, :cancellation_token, :message, :locale] @timestamps_opts [type: :utc_datetime] @@ -33,13 +32,7 @@ defmodule Mobilizon.Events.Participant do field(:role, ParticipantRole, default: :participant) field(:url, :string) - embeds_one :metadata, Metadata, on_replace: :delete do - field(:email, :string) - field(:confirmation_token, :string) - field(:cancellation_token, :string) - field(:message, :string) - field(:locale, :string) - end + embeds_one(:metadata, Metadata, on_replace: :delete) belongs_to(:event, Event, primary_key: true) belongs_to(:actor, Actor, primary_key: true) @@ -68,18 +61,12 @@ defmodule Mobilizon.Events.Participant do def changeset(%__MODULE__{} = participant, attrs) do participant |> cast(attrs, @attrs) - |> cast_embed(:metadata, with: &metadata_changeset/2) + |> cast_embed(:metadata) |> ensure_url() |> validate_required(@required_attrs) |> unique_constraint(:actor_id, name: :participants_event_id_actor_id_index) end - defp metadata_changeset(schema, params) do - schema - |> cast(params, @metadata_attrs) - |> Checker.validate_changeset() - end - # If there's a blank URL that's because we're doing the first insert @spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do diff --git a/lib/mobilizon/events/participant_metadata.ex b/lib/mobilizon/events/participant_metadata.ex new file mode 100644 index 000000000..bca00a0cb --- /dev/null +++ b/lib/mobilizon/events/participant_metadata.ex @@ -0,0 +1,36 @@ +defmodule Mobilizon.Events.Participant.Metadata do + @moduledoc """ + Participation stats on event + """ + + use Ecto.Schema + import Ecto.Changeset + alias Mobilizon.Web.Email.Checker + + @type t :: %__MODULE__{ + email: String.t(), + confirmation_token: String.t(), + cancellation_token: String.t(), + message: String.t(), + locale: String.t() + } + + @attrs [:email, :confirmation_token, :cancellation_token, :message, :locale] + + @derive Jason.Encoder + embedded_schema do + field(:email, :string) + field(:confirmation_token, :string) + field(:cancellation_token, :string) + field(:message, :string) + field(:locale, :string) + end + + @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(schema, params) do + schema + |> cast(params, @attrs) + |> Checker.validate_changeset() + end +end diff --git a/lib/mobilizon/medias/media.ex b/lib/mobilizon/medias/media.ex index 0133b5bfc..5feb5e944 100644 --- a/lib/mobilizon/medias/media.ex +++ b/lib/mobilizon/medias/media.ex @@ -51,7 +51,7 @@ defmodule Mobilizon.Medias.Media do end @doc false - @spec changeset(struct(), map) :: Ecto.Changeset.t() + @spec metadata_changeset(Metadata.t(), map) :: Ecto.Changeset.t() def metadata_changeset(metadata, attrs) do metadata |> cast(attrs, @metadata_attrs) diff --git a/lib/service/activity/renderer/renderer.ex b/lib/service/activity/renderer/renderer.ex index 87e1d56d1..14eb6693f 100644 --- a/lib/service/activity/renderer/renderer.ex +++ b/lib/service/activity/renderer/renderer.ex @@ -19,9 +19,17 @@ defmodule Mobilizon.Service.Activity.Renderer do require Logger import Mobilizon.Web.Gettext, only: [dgettext: 3] - @type render :: %{body: String.t(), url: String.t()} + @type render :: %{ + body: String.t(), + url: String.t(), + timestamp: String.t(), + locale: String.t(), + title: String.t() + } - @callback render(entity :: Activity.t(), Keyword.t()) :: render() + @type common_render :: %{body: String.t(), url: String.t()} + + @callback render(entity :: Activity.t(), Keyword.t()) :: common_render() @spec render(Activity.t()) :: render() def render(%Activity{} = activity, options \\ []) do @@ -43,6 +51,7 @@ defmodule Mobilizon.Service.Activity.Renderer do res end + @spec do_render(Activity.t(), Keyword.t()) :: common_render() defp do_render(%Activity{type: type} = activity, options) do case type do :discussion -> Discussion.render(activity, options) diff --git a/lib/service/activity/utils.ex b/lib/service/activity/utils.ex index 7a0f15754..6441ea6c6 100644 --- a/lib/service/activity/utils.ex +++ b/lib/service/activity/utils.ex @@ -12,7 +12,7 @@ defmodule Mobilizon.Service.Activity.Utils do |> add_activity_object() end - @spec add_activity_object(Activity.t()) :: Activity.t() + @spec add_activity_object(Activity.t()) :: map() def add_activity_object(%Activity{} = activity) do Map.put(activity, :object, ActivityService.object(activity)) end diff --git a/lib/service/geospatial/nominatim.ex b/lib/service/geospatial/nominatim.ex index 9b8754d58..c32e7ebcd 100644 --- a/lib/service/geospatial/nominatim.ex +++ b/lib/service/geospatial/nominatim.ex @@ -143,15 +143,13 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do if is_nil(value), do: url, else: do_add_parameter(url, key, value) end - @spec do_add_parameter(String.t(), atom(), any()) :: String.t() + @spec do_add_parameter(String.t(), :zoom | :country_code | :api_key, any()) :: String.t() defp do_add_parameter(url, :zoom, zoom), do: "#{url}&zoom=#{zoom}" - @spec do_add_parameter(String.t(), atom(), any()) :: String.t() defp do_add_parameter(url, :country_code, country_code), do: "#{url}&countrycodes=#{country_code}" - @spec do_add_parameter(String.t(), atom(), any()) :: String.t() defp do_add_parameter(url, :api_key, api_key), do: "#{url}&key=#{api_key}" diff --git a/lib/service/geospatial/provider.ex b/lib/service/geospatial/provider.ex index 4d9fadd11..dd2f4d890 100644 --- a/lib/service/geospatial/provider.ex +++ b/lib/service/geospatial/provider.ex @@ -67,7 +67,7 @@ defmodule Mobilizon.Service.Geospatial.Provider do @doc """ Returns a `Geo.Point` for given coordinates """ - @spec coordinates([number], number) :: Geo.Point.t() + @spec coordinates([number], number) :: Geo.Point.t() | nil def coordinates(coords, srid \\ 4326) def coordinates([x, y], srid) when is_number(x) and is_number(y) do @@ -78,7 +78,6 @@ defmodule Mobilizon.Service.Geospatial.Provider do %Geo.Point{coordinates: {String.to_float(x), String.to_float(y)}, srid: srid} end - @spec coordinates(any) :: nil def coordinates(_, _), do: nil @spec endpoint(atom()) :: String.t() diff --git a/lib/service/guards.ex b/lib/service/guards.ex index e48e65076..0db60d099 100644 --- a/lib/service/guards.ex +++ b/lib/service/guards.ex @@ -3,7 +3,31 @@ defmodule Mobilizon.Service.Guards do Various guards """ - defguard is_valid_string?(value) when is_binary(value) and value != "" + @doc """ + Returns `true` if `term` is a valid string and not empty. - defguard is_valid_list?(value) when is_list(value) and length(value) > 0 + ## Examples + + iex> is_valid_string("one") + true + iex> is_valid_string("") + false + iex> is_valid_string(2) + false + """ + defguard is_valid_string(term) when is_binary(term) and term != "" + + @doc """ + Returns `true` if `term` is a valid list and not empty. + + ## Examples + + iex> is_valid_list(["one"]) + true + iex> is_valid_list([]) + false + iex> is_valid_list("foo") + false + """ + defguard is_valid_list(term) when is_list(term) and length(term) > 0 end diff --git a/lib/service/language_detection/language_detection.ex b/lib/service/language_detection/language_detection.ex index 96d50d498..87b8af0cb 100644 --- a/lib/service/language_detection/language_detection.ex +++ b/lib/service/language_detection/language_detection.ex @@ -64,7 +64,7 @@ defmodule Mobilizon.Service.LanguageDetection do def normalize(language) do case Cldr.AcceptLanguage.parse(language, Mobilizon.Cldr) do - {:ok, [{_, tag}]} -> + {:ok, [{_, %Cldr.LanguageTag{} = tag}]} -> tag.language _ -> diff --git a/lib/web/auth/context.ex b/lib/web/auth/context.ex index 68a1c4ffe..1d472156c 100644 --- a/lib/web/auth/context.ex +++ b/lib/web/auth/context.ex @@ -9,17 +9,20 @@ defmodule Mobilizon.Web.Auth.Context do alias Mobilizon.Service.ErrorReporting.Sentry, as: SentryAdapter alias Mobilizon.Users.User + @spec init(Plug.opts()) :: Plug.opts() def init(opts) do opts end + @spec call(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t() def call(%{assigns: %{ip: _}} = conn, _opts), do: conn def call(conn, _opts) do set_user_information_in_context(conn) end - def set_user_information_in_context(conn) do + @spec set_user_information_in_context(Plug.Conn.t()) :: Plug.Conn.t() + defp set_user_information_in_context(conn) do context = %{ip: conn.remote_ip |> :inet.ntoa() |> to_string()} {conn, context} = diff --git a/lib/web/cache/cache.ex b/lib/web/cache/cache.ex index 5afcd4690..08e3068f0 100644 --- a/lib/web/cache/cache.ex +++ b/lib/web/cache/cache.ex @@ -4,16 +4,19 @@ defmodule Mobilizon.Web.Cache do """ alias Mobilizon.Actors.Actor - alias Mobilizon.Web.Cache.ActivityPub + import Mobilizon.Service.Guards, only: [is_valid_string: 1] @caches [:activity_pub, :feed, :ics] + @type local_actor :: %Actor{domain: nil} + @doc """ - Clears all caches for an actor. + Clears all caches for a local actor. """ - @spec clear_cache(Actor.t()) :: {:ok, true} - def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do + @spec clear_cache(%Actor{domain: nil, preferred_username: String.t()}) :: :ok + def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) + when is_valid_string(preferred_username) do Enum.each(@caches, &Cachex.del(&1, "actor_" <> preferred_username)) end diff --git a/lib/web/channels/graphql_socket.ex b/lib/web/channels/graphql_socket.ex index b694416a1..0bf0e7244 100644 --- a/lib/web/channels/graphql_socket.ex +++ b/lib/web/channels/graphql_socket.ex @@ -6,6 +6,7 @@ defmodule Mobilizon.Web.GraphQLSocket do alias Mobilizon.Users.User + @spec connect(map, Phoenix.Socket.t()) :: {:ok, Phoenix.Socket.t()} | :error def connect(%{"token" => token}, socket) do with {:ok, authed_socket} <- Guardian.Phoenix.Socket.authenticate(socket, Mobilizon.Web.Auth.Guardian, token), @@ -26,5 +27,6 @@ defmodule Mobilizon.Web.GraphQLSocket do def connect(_args, _socket), do: :error + @spec id(any) :: nil def id(_socket), do: nil end diff --git a/lib/web/email/activity.ex b/lib/web/email/activity.ex index 17268424c..15570e5c3 100644 --- a/lib/web/email/activity.ex +++ b/lib/web/email/activity.ex @@ -12,7 +12,7 @@ defmodule Mobilizon.Web.Email.Activity do alias Mobilizon.Config alias Mobilizon.Web.Email - @spec direct_activity(String.t(), list(), String.t()) :: + @spec direct_activity(String.t(), list(), Keyword.t()) :: Bamboo.Email.t() def direct_activity( email, diff --git a/lib/web/email/mailer.ex b/lib/web/email/mailer.ex index 6f4693746..68f9d905a 100644 --- a/lib/web/email/mailer.ex +++ b/lib/web/email/mailer.ex @@ -5,6 +5,7 @@ defmodule Mobilizon.Web.Email.Mailer do use Bamboo.Mailer, otp_app: :mobilizon alias Mobilizon.Service.ErrorReporting.Sentry + @spec send_email_later(Bamboo.Email.t()) :: Bamboo.Email.t() def send_email_later(email) do Mobilizon.Web.Email.Mailer.deliver_later!(email) rescue @@ -17,6 +18,7 @@ defmodule Mobilizon.Web.Email.Mailer do reraise error, __STACKTRACE__ end + @spec send_email(Bamboo.Email.t()) :: Bamboo.Email.t() | {Bamboo.Email.t(), any()} def send_email(email) do Mobilizon.Web.Email.Mailer.deliver_now!(email) rescue diff --git a/lib/web/email/participation.ex b/lib/web/email/participation.ex index edf319906..7fea008fb 100644 --- a/lib/web/email/participation.ex +++ b/lib/web/email/participation.ex @@ -50,7 +50,6 @@ defmodule Mobilizon.Web.Email.Participation do Bamboo.Email.t() def participation_updated(user, participant, locale \\ "en") - @spec participation_updated(User.t(), Participant.t(), String.t()) :: Bamboo.Email.t() def participation_updated( %User{email: email}, %Participant{} = participant, @@ -58,7 +57,6 @@ defmodule Mobilizon.Web.Email.Participation do ), do: participation_updated(email, participant, locale) - @spec participation_updated(String.t(), Participant.t(), String.t()) :: Bamboo.Email.t() def participation_updated( email, %Participant{event: event, role: :rejected}, @@ -79,7 +77,6 @@ defmodule Mobilizon.Web.Email.Participation do |> render(:event_participation_rejected) end - @spec participation_updated(String.t(), Participant.t(), String.t()) :: Bamboo.Email.t() def participation_updated( email, %Participant{event: %Event{join_options: :free} = event, role: :participant}, @@ -100,7 +97,6 @@ defmodule Mobilizon.Web.Email.Participation do |> render(:event_participation_confirmed) end - @spec participation_updated(String.t(), Participant.t(), String.t()) :: Bamboo.Email.t() def participation_updated( email, %Participant{event: event, role: :participant}, diff --git a/lib/web/email/user.ex b/lib/web/email/user.ex index 54cf253ee..638f0dcca 100644 --- a/lib/web/email/user.ex +++ b/lib/web/email/user.ex @@ -57,6 +57,7 @@ defmodule Mobilizon.Web.Email.User do |> render(:password_reset) end + @spec check_confirmation_token(String.t()) :: {:ok, User.t()} | {:error, :invalid_token} def check_confirmation_token(token) when is_binary(token) do with %User{} = user <- Users.get_user_by_activation_token(token), {:ok, %User{} = user} <- @@ -86,7 +87,7 @@ defmodule Mobilizon.Web.Email.User do end end - @spec send_confirmation_email(User.t(), String.t()) :: {:ok, Bamboo.Email.t()} | {:error, any()} + @spec send_confirmation_email(User.t(), String.t()) :: Bamboo.Email.t() def send_confirmation_email(%User{} = user, locale \\ "en") do user |> Email.User.confirmation_email(locale) @@ -96,7 +97,8 @@ defmodule Mobilizon.Web.Email.User do @doc """ Check that the provided token is correct and update provided password """ - @spec check_reset_password_token(String.t(), String.t()) :: tuple + @spec check_reset_password_token(String.t(), String.t()) :: + {:ok, User.t()} | {:error, String.t()} def check_reset_password_token(password, token) do with %User{} = user <- Users.get_user_by_reset_password_token(token), {:ok, %User{} = user} <- diff --git a/lib/web/mobilizon_web.ex b/lib/web/mobilizon_web.ex index 76f5b5fe1..059b9eb92 100644 --- a/lib/web/mobilizon_web.ex +++ b/lib/web/mobilizon_web.ex @@ -21,8 +21,8 @@ defmodule Mobilizon.Web do quote do use Phoenix.Controller, namespace: Mobilizon.Web import Plug.Conn - import Mobilizon.Web.Router.Helpers import Mobilizon.Web.Gettext + alias Mobilizon.Web.Router.Helpers, as: Routes end end @@ -33,14 +33,13 @@ defmodule Mobilizon.Web do pattern: "**/*", namespace: Mobilizon.Web - # Import convenience functions from controllers - import Phoenix.Controller, only: [get_flash: 2, view_module: 1] - # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML - import Mobilizon.Web.Router.Helpers + import Phoenix.View + import Mobilizon.Web.ErrorHelpers import Mobilizon.Web.Gettext + alias Mobilizon.Web.Router.Helpers, as: Routes end end diff --git a/lib/web/plugs/http_security_plug.ex b/lib/web/plugs/http_security_plug.ex index 00f0eede8..fd2ddba65 100644 --- a/lib/web/plugs/http_security_plug.ex +++ b/lib/web/plugs/http_security_plug.ex @@ -19,12 +19,13 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do if Config.get([:http_security, :enabled]) do conn |> merge_resp_headers(headers(options)) - |> maybe_send_sts_header(Config.get([:http_security, :sts])) + |> maybe_send_sts_header(Config.get([:http_security, :sts], false)) else conn end end + @spec headers(Keyword.t()) :: list({String.t(), String.t()}) defp headers(options) do referrer_policy = Keyword.get(options, :referrer_policy, Config.get([:http_security, :referrer_policy])) @@ -55,6 +56,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do @style_src "style-src 'self' " @font_src "font-src 'self' " + @spec csp_string(Keyword.t()) :: String.t() defp csp_string(options) do scheme = Keyword.get(options, :scheme, Config.get([Pleroma.Web.Endpoint, :url])[:scheme]) static_url = Mobilizon.Web.Endpoint.static_url() @@ -115,10 +117,11 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do |> to_string() end + @spec add_csp_param(list(), list(String.t()) | String.t() | nil) :: list() defp add_csp_param(csp_iodata, nil), do: csp_iodata - defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] + @spec maybe_send_sts_header(Plug.Conn.t(), boolean()) :: Plug.Conn.t() defp maybe_send_sts_header(conn, true) do max_age_sts = Config.get([:http_security, :sts_max_age]) @@ -127,8 +130,9 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do ]) end - defp maybe_send_sts_header(conn, _), do: conn + defp maybe_send_sts_header(conn, false), do: conn + @spec get_csp_config(atom(), Keyword.t()) :: String.t() defp get_csp_config(type, options) do options |> Keyword.get(type, Config.get([:http_security, :csp_policy, type])) diff --git a/lib/web/plugs/http_signatures.ex b/lib/web/plugs/http_signatures.ex index 80333a782..f57b4d4c9 100644 --- a/lib/web/plugs/http_signatures.ex +++ b/lib/web/plugs/http_signatures.ex @@ -16,6 +16,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSignatures do options end + @spec call(Plug.Conn.t(), any) :: Plug.Conn.t() def call(%{assigns: %{valid_signature: true}} = conn, _opts) do conn end diff --git a/lib/web/plugs/uploaded_media.ex b/lib/web/plugs/uploaded_media.ex index 14538249f..150386f29 100644 --- a/lib/web/plugs/uploaded_media.ex +++ b/lib/web/plugs/uploaded_media.ex @@ -45,11 +45,13 @@ defmodule Mobilizon.Web.Plugs.UploadedMedia do config = Config.get([Upload]) - with uploader <- Keyword.fetch!(config, :uploader), - proxy_remote = Keyword.get(config, :proxy_remote, false), - {:ok, get_method} <- uploader.get_file(file) do - get_media(conn, get_method, proxy_remote, opts) - else + uploader = Keyword.fetch!(config, :uploader) + proxy_remote = Keyword.get(config, :proxy_remote, false) + + case uploader.get_file(file) do + {:ok, get_method} -> + get_media(conn, get_method, proxy_remote, opts) + _ -> conn |> send_resp(500, "Failed") @@ -59,6 +61,12 @@ defmodule Mobilizon.Web.Plugs.UploadedMedia do def call(conn, _opts), do: conn + @spec get_media( + Plug.Conn.t(), + {:static_dir, String.t()} | {:url, String.t()} | any(), + boolean, + any() + ) :: Plug.Conn.t() defp get_media(conn, {:static_dir, directory}, _, opts) do static_opts = opts diff --git a/lib/web/proxy/reverse_proxy.ex b/lib/web/proxy/reverse_proxy.ex index f09d751a0..766dd5e06 100644 --- a/lib/web/proxy/reverse_proxy.ex +++ b/lib/web/proxy/reverse_proxy.ex @@ -381,6 +381,10 @@ defmodule Mobilizon.Web.ReverseProxy do defp body_size_constraint(_, _), do: :ok + @spec check_read_duration(any(), integer()) :: + {:ok, {integer(), integer()}} + | {:ok, :no_duration_limit, :no_duration_limit} + | {:error, :read_duration_exceeded} defp check_read_duration(duration, max) when is_integer(duration) and is_integer(max) and max > 0 do if duration > max do diff --git a/lib/web/templates/email/activity/_comment_activity_item.html.eex b/lib/web/templates/email/activity/_comment_activity_item.html.eex index 4a9abd54b..7b648bb46 100644 --- a/lib/web/templates/email/activity/_comment_activity_item.html.eex +++ b/lib/web/templates/email/activity/_comment_activity_item.html.eex @@ -5,7 +5,7 @@ %{ profile: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", event: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", event: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", event: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", event: " -<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :participation_event_comment -> %><%= dgettext("activity", "%{profile} has posted an announcement under event %{event}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :participation_event_comment -> %><%= dgettext("activity", "%{profile} has posted an announcement under event %{event}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), event: @activity.subject_params["event_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_new_comment -> %><%= if @activity.subject_params["comment_reply_to"] do %><%=dgettext("activity", "%{profile} has posted a new reply under your event %{event}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_new_comment -> %><%= if @activity.subject_params["comment_reply_to"] do %><%=dgettext("activity", "%{profile} has posted a new reply under your event %{event}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), event: @activity.subject_params["event_title"] } ) %> -<%= "#{page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode()}#comment-#{@activity.subject_params["comment_reply_to_uuid"]}-#{@activity.subject_params["comment_uuid"]}" %><% else %><%= dgettext("activity", "%{profile} has posted a new comment under your event %{event}.", +<%= "#{Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode()}#comment-#{@activity.subject_params["comment_reply_to_uuid"]}-#{@activity.subject_params["comment_uuid"]}" %><% else %><%= dgettext("activity", "%{profile} has posted a new comment under your event %{event}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), event: @activity.subject_params["event_title"] } ) %> -<%= "#{page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode()}#comment-#{@activity.subject_params["comment_uuid"]}"%><% end %><% end %> \ No newline at end of file +<%= "#{Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode()}#comment-#{@activity.subject_params["comment_uuid"]}"%><% end %><% end %> \ No newline at end of file diff --git a/lib/web/templates/email/activity/_discussion_activity_item.html.eex b/lib/web/templates/email/activity/_discussion_activity_item.html.eex index b678ae5ab..3a74f1168 100644 --- a/lib/web/templates/email/activity/_discussion_activity_item.html.eex +++ b/lib/web/templates/email/activity/_discussion_activity_item.html.eex @@ -5,7 +5,7 @@ %{ profile: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", discussion: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", discussion: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", discussion: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", discussion: " -<%= page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_replied -> %><%= dgettext("activity", "%{profile} replied to the discussion %{discussion}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_replied -> %><%= dgettext("activity", "%{profile} replied to the discussion %{discussion}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), discussion: @activity.subject_params["discussion_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_renamed -> %><%= dgettext("activity", "%{profile} renamed the discussion %{discussion}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_renamed -> %><%= dgettext("activity", "%{profile} renamed the discussion %{discussion}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), discussion: @activity.subject_params["discussion_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_archived -> %><%= dgettext("activity", "%{profile} archived the discussion %{discussion}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_archived -> %><%= dgettext("activity", "%{profile} archived the discussion %{discussion}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), discussion: @activity.subject_params["discussion_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_deleted -> %><%= dgettext("activity", "%{profile} deleted the discussion %{discussion}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_deleted -> %><%= dgettext("activity", "%{profile} deleted the discussion %{discussion}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), discussion: @activity.subject_params["discussion_title"] diff --git a/lib/web/templates/email/activity/_event_activity_item.html.eex b/lib/web/templates/email/activity/_event_activity_item.html.eex index 074e9c4a6..5f32ab87b 100644 --- a/lib/web/templates/email/activity/_event_activity_item.html.eex +++ b/lib/web/templates/email/activity/_event_activity_item.html.eex @@ -5,7 +5,7 @@ %{ profile: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", event: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", event: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", event: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", event: " -<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_updated -> %><%= dgettext("activity", "The event %{event} was updated by %{profile}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_updated -> %><%= dgettext("activity", "The event %{event} was updated by %{profile}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), event: @activity.subject_params["event_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_deleted -> %><%= dgettext("activity", "The event %{event} was deleted by %{profile}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_deleted -> %><%= dgettext("activity", "The event %{event} was deleted by %{profile}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), event: @activity.subject_params["event_title"] @@ -22,10 +22,10 @@ event: @activity.subject_params["event_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} posted a comment on the event %{event}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} posted a comment on the event %{event}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), event: @activity.subject_params["event_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% end %><% end %> \ No newline at end of file +<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% end %><% end %> \ No newline at end of file diff --git a/lib/web/templates/email/activity/_group_activity_item.html.eex b/lib/web/templates/email/activity/_group_activity_item.html.eex index a17c7956f..bf4be4e8e 100644 --- a/lib/web/templates/email/activity/_group_activity_item.html.eex +++ b/lib/web/templates/email/activity/_group_activity_item.html.eex @@ -5,7 +5,7 @@ %{ profile: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", group: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", group: " -<%= page_url(Mobilizon.Web.Endpoint, :actor, @activity.subject_params["group_federated_username"]) |> URI.decode() %><% :group_updated -> %><%= dgettext("activity", "%{profile} updated the group %{group}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, @activity.subject_params["group_federated_username"]) |> URI.decode() %><% :group_updated -> %><%= dgettext("activity", "%{profile} updated the group %{group}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), group: @activity.subject_params["group_name"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :actor, @activity.subject_params["group_federated_username"]) |> URI.decode() %><% end %> \ No newline at end of file +<%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, @activity.subject_params["group_federated_username"]) |> URI.decode() %><% end %> \ No newline at end of file diff --git a/lib/web/templates/email/activity/_post_activity_item.html.eex b/lib/web/templates/email/activity/_post_activity_item.html.eex index 856056be9..81733b4ab 100644 --- a/lib/web/templates/email/activity/_post_activity_item.html.eex +++ b/lib/web/templates/email/activity/_post_activity_item.html.eex @@ -5,7 +5,7 @@ %{ profile: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", post: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", post: " -<%= page_url(Mobilizon.Web.Endpoint, :post, @activity.subject_params["post_slug"]) |> URI.decode() %><% :post_updated -> %><%= dgettext("activity", "The post %{post} was updated by %{profile}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :post, @activity.subject_params["post_slug"]) |> URI.decode() %><% :post_updated -> %><%= dgettext("activity", "The post %{post} was updated by %{profile}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), post: @activity.subject_params["post_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :post, @activity.subject_params["post_slug"]) |> URI.decode() %><% :post_deleted -> %><%= dgettext("activity", "The post %{post} was deleted by %{profile}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :post, @activity.subject_params["post_slug"]) |> URI.decode() %><% :post_deleted -> %><%= dgettext("activity", "The post %{post} was deleted by %{profile}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), post: @activity.subject_params["post_title"] diff --git a/lib/web/templates/email/activity/_resource_activity_item.html.eex b/lib/web/templates/email/activity/_resource_activity_item.html.eex index ad5b42d5e..6a894c83a 100644 --- a/lib/web/templates/email/activity/_resource_activity_item.html.eex +++ b/lib/web/templates/email/activity/_resource_activity_item.html.eex @@ -6,7 +6,7 @@ %{ profile: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", resource: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", resource: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", resource: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", resource: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", resource: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", resource: " -<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} created the resource %{resource}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} created the resource %{resource}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), resource: @activity.subject_params["resource_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_renamed -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} renamed the folder from %{old_resource_title} to %{resource}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_renamed -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} renamed the folder from %{old_resource_title} to %{resource}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), resource: @activity.subject_params["resource_title"], old_resource_title: @activity.subject_params["old_resource_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} renamed the resource from %{old_resource_title} to %{resource}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} renamed the resource from %{old_resource_title} to %{resource}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), resource: @activity.subject_params["resource_title"], old_resource_title: @activity.subject_params["old_resource_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_moved -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} moved the folder %{resource}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_moved -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} moved the folder %{resource}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), resource: @activity.subject_params["resource_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} moved the resource %{resource}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} moved the resource %{resource}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), resource: @activity.subject_params["resource_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_deleted -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} deleted the folder %{resource}.", +<%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_deleted -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} deleted the folder %{resource}.", %{ profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), resource: @activity.subject_params["resource_title"] diff --git a/lib/web/templates/email/anonymous_participation_confirmation.html.eex b/lib/web/templates/email/anonymous_participation_confirmation.html.eex index cbb6ce477..8cbb1a8b7 100644 --- a/lib/web/templates/email/anonymous_participation_confirmation.html.eex +++ b/lib/web/templates/email/anonymous_participation_confirmation.html.eex @@ -47,7 +47,7 @@ - diff --git a/lib/web/templates/email/anonymous_participation_confirmation.text.eex b/lib/web/templates/email/anonymous_participation_confirmation.text.eex index b5e46e1da..ae4943c15 100644 --- a/lib/web/templates/email/anonymous_participation_confirmation.text.eex +++ b/lib/web/templates/email/anonymous_participation_confirmation.text.eex @@ -2,5 +2,5 @@ == <%= gettext "Hi there! You just registered to join this event: « %{title} ». Please confirm the e-mail address you provided:", title: @participant.event.title %> <%= gettext "If you didn't trigger this email, you may safely ignore it." %> -<%= page_url(Mobilizon.Web.Endpoint, :participation_email_confirmation, @participant.metadata.confirmation_token) %> +<%= Routes.page_url(Mobilizon.Web.Endpoint, :participation_email_confirmation, @participant.metadata.confirmation_token) %> <%= ngettext "Would you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button.", "Would you wish to cancel your attendance to one or several events, visit the event pages through the links above and click the « Attending » button.", 1 %> diff --git a/lib/web/templates/email/before_event_notification.html.eex b/lib/web/templates/email/before_event_notification.html.eex index 213b038c2..ab4f54c83 100644 --- a/lib/web/templates/email/before_event_notification.html.eex +++ b/lib/web/templates/email/before_event_notification.html.eex @@ -47,7 +47,7 @@
+ <%= gettext "Confirm my e-mail address" %>
- diff --git a/lib/web/templates/email/before_event_notification.text.eex b/lib/web/templates/email/before_event_notification.text.eex index f1f561c32..cb72a7cce 100644 --- a/lib/web/templates/email/before_event_notification.text.eex +++ b/lib/web/templates/email/before_event_notification.text.eex @@ -2,5 +2,5 @@ == <%= gettext "Get ready for %{title}", title: @participant.event.title %> <%= gettext "Go to event page" %> -<%= gettext "View the event on: %{link}", link: page_url(Mobilizon.Web.Endpoint, :event, @participant.event.uuid) %> +<%= gettext "View the event on: %{link}", link: Routes.page_url(Mobilizon.Web.Endpoint, :event, @participant.event.uuid) %> <%= gettext "If you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button." %> diff --git a/lib/web/templates/email/email_anonymous_activity.html.eex b/lib/web/templates/email/email_anonymous_activity.html.eex index 66b95852a..52f624712 100644 --- a/lib/web/templates/email/email_anonymous_activity.html.eex +++ b/lib/web/templates/email/email_anonymous_activity.html.eex @@ -42,7 +42,7 @@ %{ profile: "#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}", event: "
+ <%= gettext "Go to event page" %>
- diff --git a/lib/web/templates/email/email_anonymous_activity.text.eex b/lib/web/templates/email/email_anonymous_activity.text.eex index 0df709a1c..c19b43834 100644 --- a/lib/web/templates/email/email_anonymous_activity.text.eex +++ b/lib/web/templates/email/email_anonymous_activity.text.eex @@ -8,4 +8,4 @@ event: @activity.subject_params["event_title"] } ) %> -<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %> \ No newline at end of file +<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %> \ No newline at end of file diff --git a/lib/web/templates/email/email_changed_new.html.eex b/lib/web/templates/email/email_changed_new.html.eex index f3eb0267b..75fd87b81 100644 --- a/lib/web/templates/email/email_changed_new.html.eex +++ b/lib/web/templates/email/email_changed_new.html.eex @@ -47,7 +47,7 @@
" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> + " target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <%= gettext "Visit event page" %>
diff --git a/lib/web/templates/email/email_changed_new.text.eex b/lib/web/templates/email/email_changed_new.text.eex index 65d5c4fd6..5ae1b016e 100644 --- a/lib/web/templates/email/email_changed_new.text.eex +++ b/lib/web/templates/email/email_changed_new.text.eex @@ -1,5 +1,5 @@ <%= gettext "Confirm new email" %> == <%= gettext "Hi there! It seems like you wanted to change the email address linked to your account on %{instance}. If you still wish to do so, please click the button below to confirm the change. You will then be able to log in to %{instance} with this new email address.", %{instance: @instance_name} %> -<%= page_url(Mobilizon.Web.Endpoint, :user_email_validation, @token) %> +<%= Routes.page_url(Mobilizon.Web.Endpoint, :user_email_validation, @token) %> <%= gettext "If you didn't trigger the change yourself, please ignore this message." %> diff --git a/lib/web/templates/email/email_direct_activity.html.eex b/lib/web/templates/email/email_direct_activity.html.eex index be3d997b5..23115ea33 100644 --- a/lib/web/templates/email/email_direct_activity.html.eex +++ b/lib/web/templates/email/email_direct_activity.html.eex @@ -64,7 +64,7 @@ <%= if hd(group_activities).group.avatar do %> @@ -73,7 +73,7 @@
- + <%= gettext "Verify your email address" %>
- +
@@ -81,7 +81,7 @@ <%= if hd(group_activities).group.name do %> @@ -131,7 +131,7 @@
- + <%= hd(group_activities).group.name || "@#{Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)}" %>
- + @<%= Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group) %>
diff --git a/lib/web/templates/email/email_direct_activity.text.eex b/lib/web/templates/email/email_direct_activity.text.eex index 44572d5a2..e795ec0ab 100644 --- a/lib/web/templates/email/email_direct_activity.text.eex +++ b/lib/web/templates/email/email_direct_activity.text.eex @@ -21,7 +21,7 @@ <% end %> <%= if length(group_activities) > 5 do %> <%= dngettext "activity", "View one more activity", "View %{count} more activities", length(group_activities) - 5, %{count: length(group_activities) - 5} %> -<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>/timeline +<%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>/timeline <% end %> <% end %> <%= dgettext("activity", "Don't want to receive activity notifications? You may change frequency or disable them in your settings.") %> diff --git a/lib/web/templates/email/event_participation_approved.html.eex b/lib/web/templates/email/event_participation_approved.html.eex index 7ec2ed034..1a1f2f80a 100644 --- a/lib/web/templates/email/event_participation_approved.html.eex +++ b/lib/web/templates/email/event_participation_approved.html.eex @@ -54,7 +54,7 @@
- + <%= dngettext "activity", "View one more activity", "View %{count} more activities", length(group_activities) - 5, %{count: length(group_activities) - 5} %> - diff --git a/lib/web/templates/email/event_participation_approved.text.eex b/lib/web/templates/email/event_participation_approved.text.eex index 906cfbfa2..5b71ca3f1 100644 --- a/lib/web/templates/email/event_participation_approved.text.eex +++ b/lib/web/templates/email/event_participation_approved.text.eex @@ -6,6 +6,6 @@ <%= gettext "Good news: one of the event organizers just approved your request. Update your calendar, because you're on the guest list now!" %> -<%= page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %> +<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %> <%= gettext "Would you wish to update or cancel your attendance, simply access the event page through the link above and click on the Attending button." %> diff --git a/lib/web/templates/email/event_participation_confirmed.html.eex b/lib/web/templates/email/event_participation_confirmed.html.eex index 3dbbab91c..d38056a4d 100644 --- a/lib/web/templates/email/event_participation_confirmed.html.eex +++ b/lib/web/templates/email/event_participation_confirmed.html.eex @@ -54,7 +54,7 @@
+ <%= gettext "Visit event page" %>
- diff --git a/lib/web/templates/email/event_participation_confirmed.text.eex b/lib/web/templates/email/event_participation_confirmed.text.eex index 869df0b4d..56f9edad8 100644 --- a/lib/web/templates/email/event_participation_confirmed.text.eex +++ b/lib/web/templates/email/event_participation_confirmed.text.eex @@ -2,5 +2,5 @@ == <%= gettext "You recently requested to attend %{title}.", title: @event.title %> <%= gettext "You have confirmed your participation. Update your calendar, because you're on the guest list now!" %> -<%= page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %> +<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %> <%= gettext "Would you wish to update or cancel your attendance, simply access the event page through the link above and click on the Attending button." %> diff --git a/lib/web/templates/email/event_updated.html.eex b/lib/web/templates/email/event_updated.html.eex index f6539b2e8..61b1318fd 100644 --- a/lib/web/templates/email/event_updated.html.eex +++ b/lib/web/templates/email/event_updated.html.eex @@ -117,7 +117,7 @@
+ <%= gettext "Visit event page" %>
- diff --git a/lib/web/templates/email/event_updated.text.eex b/lib/web/templates/email/event_updated.text.eex index d65225f13..676cbae61 100644 --- a/lib/web/templates/email/event_updated.text.eex +++ b/lib/web/templates/email/event_updated.text.eex @@ -20,5 +20,5 @@ <%= if MapSet.member?(@changes, :ends_on) && !is_nil(@event.ends_on) do %> <%= gettext "End %{ends_on}", ends_on: @event.ends_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %> <% end %> -<%= gettext "Visit the updated event page: %{link}", link: page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %> +<%= gettext "Visit the updated event page: %{link}", link: Routes.page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %> <%= ngettext "Would you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button.", "Would you wish to cancel your attendance to one or several events, visit the event pages through the links above and click the « Attending » button.", 1 %> diff --git a/lib/web/templates/email/group_invite.html.eex b/lib/web/templates/email/group_invite.html.eex index 6c134ba02..129478107 100644 --- a/lib/web/templates/email/group_invite.html.eex +++ b/lib/web/templates/email/group_invite.html.eex @@ -55,7 +55,7 @@
+ <%= gettext "Visit the updated event page" %>
diff --git a/lib/web/templates/email/group_invite.text.eex b/lib/web/templates/email/group_invite.text.eex index 70d1c983f..72b03aeb5 100644 --- a/lib/web/templates/email/group_invite.text.eex +++ b/lib/web/templates/email/group_invite.text.eex @@ -3,4 +3,4 @@ <%= gettext "%{inviter} just invited you to join their group %{group}", group: @group.name, inviter: @inviter.name %> <%= @group.url %> <%= gettext "To accept this invitation, head over to your groups." %> -<%= page_url(Mobilizon.Web.Endpoint, :my_groups) %> +<%= Routes.page_url(Mobilizon.Web.Endpoint, :my_groups) %> diff --git a/lib/web/templates/email/notification_each_week.html.eex b/lib/web/templates/email/notification_each_week.html.eex index c27c23018..270785fa9 100644 --- a/lib/web/templates/email/notification_each_week.html.eex +++ b/lib/web/templates/email/notification_each_week.html.eex @@ -48,7 +48,7 @@ <%= participation.event.begins_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %> - + <%= participation.event.title %> @@ -58,7 +58,7 @@ <%= @participation.event.begins_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %> - + <%= @participation.event.title %> <% end %> diff --git a/lib/web/templates/email/notification_each_week.text.eex b/lib/web/templates/email/notification_each_week.text.eex index e0c2c3cbe..f0f2fc303 100644 --- a/lib/web/templates/email/notification_each_week.text.eex +++ b/lib/web/templates/email/notification_each_week.text.eex @@ -3,9 +3,9 @@ <%= ngettext "You have one event this week:", "You have %{total} events this week:", @total, total: @total %> <%= if @total > 1 do %> <%= for participation <- @participations do %> - - <%= participation.event.begins_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %> - <%= participation.event.title %> <%= page_url(Mobilizon.Web.Endpoint, :event, participation.event.uuid) %> + - <%= participation.event.begins_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %> - <%= participation.event.title %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, participation.event.uuid) %> <% end %> <% else %> - <%= @participation.event.begins_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %> - <%= @participation.event.title %> <%= page_url(Mobilizon.Web.Endpoint, :event, @participation.event.uuid) %> + <%= @participation.event.begins_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %> - <%= @participation.event.title %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @participation.event.uuid) %> <% end %> <%= ngettext "Would you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button.", "Would you wish to cancel your attendance to one or several events, visit the event pages through the links above and click the « Attending » button.", @total %> diff --git a/lib/web/templates/email/on_day_notification.html.eex b/lib/web/templates/email/on_day_notification.html.eex index dd31dce02..7e6b9441d 100644 --- a/lib/web/templates/email/on_day_notification.html.eex +++ b/lib/web/templates/email/on_day_notification.html.eex @@ -48,7 +48,7 @@ <%= participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_time_string(@locale) %> - + <%= participation.event.title %> @@ -58,7 +58,7 @@ <%= @participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_time_string(@locale) %> - + <%= @participation.event.title %> <% end %> diff --git a/lib/web/templates/email/on_day_notification.text.eex b/lib/web/templates/email/on_day_notification.text.eex index 6d1b0f3e0..b902f2646 100644 --- a/lib/web/templates/email/on_day_notification.text.eex +++ b/lib/web/templates/email/on_day_notification.text.eex @@ -5,10 +5,10 @@ <%= if @total > 1 do %> <%= for participation <- @participations do %> - - <%= participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_time_string(@locale) %> - <%= participation.event.title %> <%= page_url(Mobilizon.Web.Endpoint, :event, participation.event.uuid) %> + - <%= participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_time_string(@locale) %> - <%= participation.event.title %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, participation.event.uuid) %> <% end %> <% else %> - <%= DateTime.shift_zone!(@participation.event.begins_on, @timezone) |> datetime_to_time_string(@locale) %> - <%= @participation.event.title %> <%= page_url(Mobilizon.Web.Endpoint, :event, @participation.event.uuid) %> + <%= DateTime.shift_zone!(@participation.event.begins_on, @timezone) |> datetime_to_time_string(@locale) %> - <%= @participation.event.title %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @participation.event.uuid) %> <% end %> <%= ngettext "Would you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button.", "Would you wish to cancel your attendance to one or several events, visit the event pages through the links above and click the « Attending » button.", @total %> diff --git a/lib/web/templates/email/pending_participation_notification.html.eex b/lib/web/templates/email/pending_participation_notification.html.eex index 56c4c5ea7..79aeb85ab 100644 --- a/lib/web/templates/email/pending_participation_notification.html.eex +++ b/lib/web/templates/email/pending_participation_notification.html.eex @@ -47,7 +47,7 @@
- + <%= gettext "See my groups" %>
diff --git a/lib/web/templates/email/pending_participation_notification.text.eex b/lib/web/templates/email/pending_participation_notification.text.eex index a1de9a579..6a0f31c77 100644 --- a/lib/web/templates/email/pending_participation_notification.text.eex +++ b/lib/web/templates/email/pending_participation_notification.text.eex @@ -3,6 +3,6 @@ <%= ngettext "You have one pending attendance request to process:", "You have %{number_participation_requests} attendance requests to process:", @total, number_participation_requests: @total %> -<%= gettext "Manage pending requests" %> <%= page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) <> "/participations" %> +<%= gettext "Manage pending requests" %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) <> "/participations" %> <%= gettext "You are receiving this email because you chose to get notifications for pending attendance requests to your events. You can disable or change your notification settings in your user account settings under « Notifications »." %>s diff --git a/lib/web/templates/email/report.html.eex b/lib/web/templates/email/report.html.eex index 1d1775ef5..b25939438 100644 --- a/lib/web/templates/email/report.html.eex +++ b/lib/web/templates/email/report.html.eex @@ -122,7 +122,7 @@
- " target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> + " target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <%= gettext "Manage pending requests" %> - diff --git a/lib/web/templates/email/report.text.eex b/lib/web/templates/email/report.text.eex index b5521c4db..7cd119b57 100644 --- a/lib/web/templates/email/report.text.eex +++ b/lib/web/templates/email/report.text.eex @@ -21,4 +21,4 @@ <%= gettext "Reason" %> <%= @report.content %> <% end %> -<%= gettext "View report:" %> <%= page_url(Mobilizon.Web.Endpoint, :moderation_report, @report.id) %> +<%= gettext "View report:" %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :moderation_report, @report.id) %> diff --git a/lib/web/upload/filter/analyze_metadata.ex b/lib/web/upload/filter/analyze_metadata.ex index ea12fb487..72fb44203 100644 --- a/lib/web/upload/filter/analyze_metadata.ex +++ b/lib/web/upload/filter/analyze_metadata.ex @@ -13,7 +13,7 @@ defmodule Mobilizon.Web.Upload.Filter.AnalyzeMetadata do @behaviour Mobilizon.Web.Upload.Filter @spec filter(Upload.t()) :: - {:ok, :filtered, Upload.t()} | {:ok, :noop} | {:error, String.t()} + {:ok, :filtered, Upload.t()} | {:ok, :noop} def filter(%Upload{tempfile: file, content_type: "image" <> _} = upload) do image = file diff --git a/lib/web/upload/filter/anonymize_filename.ex b/lib/web/upload/filter/anonymize_filename.ex index 093479448..108635995 100644 --- a/lib/web/upload/filter/anonymize_filename.ex +++ b/lib/web/upload/filter/anonymize_filename.ex @@ -14,19 +14,27 @@ defmodule Mobilizon.Web.Upload.Filter.AnonymizeFilename do alias Mobilizon.Config alias Mobilizon.Web.Upload + alias Mobilizon.Web.Upload.Filter + import Mobilizon.Service.Guards, only: [is_valid_string: 1] + @impl Filter + @spec filter(any) :: {:ok, :filtered, Upload.t()} | {:ok, :noop} def filter(%Upload{name: name} = upload) do extension = List.last(String.split(name, ".")) - name = predefined_name(extension) || random(extension) + name = predefined_name(extension) + name = if is_nil(name), do: random(extension), else: name {:ok, :filtered, %Upload{upload | name: name}} end + @impl Filter def filter(_), do: {:ok, :noop} @spec predefined_name(String.t()) :: String.t() | nil defp predefined_name(extension) do - with name when not is_nil(name) <- Config.get([__MODULE__, :text]), - do: String.replace(name, "{extension}", extension) + case Config.get([__MODULE__, :text]) do + name when is_valid_string(name) -> String.replace(name, "{extension}", extension) + _ -> nil + end end defp random(extension) do diff --git a/lib/web/upload/filter/blurhash.ex b/lib/web/upload/filter/blurhash.ex index b2454dea6..a05c790a9 100644 --- a/lib/web/upload/filter/blurhash.ex +++ b/lib/web/upload/filter/blurhash.ex @@ -8,7 +8,7 @@ defmodule Mobilizon.Web.Upload.Filter.BlurHash do @behaviour Mobilizon.Web.Upload.Filter @spec filter(Upload.t()) :: - {:ok, :filtered, Upload.t()} | {:ok, :noop} | {:error, String.t()} + {:ok, :filtered, Upload.t()} | {:ok, :noop} def filter(%Upload{tempfile: file, content_type: "image" <> _} = upload) do {:ok, :filtered, %Upload{upload | blurhash: generate_blurhash(file)}} rescue diff --git a/lib/web/upload/filter/dedupe.ex b/lib/web/upload/filter/dedupe.ex index 1e7731a5d..d6ca08860 100644 --- a/lib/web/upload/filter/dedupe.ex +++ b/lib/web/upload/filter/dedupe.ex @@ -10,6 +10,7 @@ defmodule Mobilizon.Web.Upload.Filter.Dedupe do @behaviour Mobilizon.Web.Upload.Filter alias Mobilizon.Web.Upload + @spec filter(Upload.t()) :: {:ok, :filtered, Upload.t()} | {:ok, :noop} def filter(%Upload{name: name, tempfile: tempfile} = upload) do extension = name |> String.split(".") |> List.last() shasum = :crypto.hash(:sha256, File.read!(tempfile)) |> Base.encode16(case: :lower) diff --git a/lib/web/upload/filter/exiftool.ex b/lib/web/upload/filter/exiftool.ex index fd7391f27..4f5a1773c 100644 --- a/lib/web/upload/filter/exiftool.ex +++ b/lib/web/upload/filter/exiftool.ex @@ -11,7 +11,7 @@ defmodule Mobilizon.Web.Upload.Filter.Exiftool do @behaviour Mobilizon.Web.Upload.Filter - @spec filter(Upload.t()) :: {:ok, any()} | {:error, String.t()} + @spec filter(Upload.t()) :: {:ok, :filtered | :noop} | {:error, String.t()} # webp is not compatible with exiftool at this time def filter(%Upload{content_type: "image/webp"}), do: {:ok, :noop} diff --git a/lib/web/upload/filter/filter.ex b/lib/web/upload/filter/filter.ex index be85909a8..a2f63a9df 100644 --- a/lib/web/upload/filter/filter.ex +++ b/lib/web/upload/filter/filter.ex @@ -20,11 +20,10 @@ defmodule Mobilizon.Web.Upload.Filter do {:ok, :filtered} | {:ok, :noop} | {:ok, :filtered, Mobilizon.Web.Upload.t()} - | {:error, any()} + | {:error, String.t() | atom} @spec filter([module()], Mobilizon.Web.Upload.t()) :: - {:ok, Mobilizon.Web.Upload.t()} | {:error, any()} - + {:ok, Mobilizon.Web.Upload.t()} | {:error, String.t() | atom} def filter([], upload) do {:ok, upload} end diff --git a/lib/web/upload/filter/mogrify.ex b/lib/web/upload/filter/mogrify.ex index f10c49728..587fb5bdd 100644 --- a/lib/web/upload/filter/mogrify.ex +++ b/lib/web/upload/filter/mogrify.ex @@ -15,7 +15,7 @@ defmodule Mobilizon.Web.Upload.Filter.Mogrify do @type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()} @type conversions :: conversion() | [conversion()] - @spec filter(Mobilizon.Web.Upload.t()) :: {:ok, :atom} | {:error, String.t()} + @spec filter(Mobilizon.Web.Upload.t()) :: {:ok, :filtered | :noop} | {:error, String.t()} def filter(%Mobilizon.Web.Upload{tempfile: file, content_type: "image" <> _}) do do_filter(file, Config.get!([__MODULE__, :args])) {:ok, :filtered} diff --git a/lib/web/upload/filter/optimize.ex b/lib/web/upload/filter/optimize.ex index c19c9fa04..fc1f73f9e 100644 --- a/lib/web/upload/filter/optimize.ex +++ b/lib/web/upload/filter/optimize.ex @@ -6,6 +6,8 @@ defmodule Mobilizon.Web.Upload.Filter.Optimize do @behaviour Mobilizon.Web.Upload.Filter alias Mobilizon.Config + alias Mobilizon.Web.Upload + require Logger @default_optimizers [ JpegOptim, @@ -16,24 +18,24 @@ defmodule Mobilizon.Web.Upload.Filter.Optimize do Cwebp ] - def filter(%Mobilizon.Web.Upload{tempfile: file, content_type: "image" <> _}) do + @spec filter(Upload.t()) :: {:ok, :filtered | :noop} | {:error, :file_not_found} + def filter(%Upload{tempfile: file, content_type: "image" <> _}) do optimizers = Config.get([__MODULE__, :optimizers], @default_optimizers) case ExOptimizer.optimize(file, deps: optimizers) do {:ok, _res} -> {:ok, :filtered} - {:error, err} -> - require Logger + {:error, :file_not_found} -> + Logger.warn("Unable to optimize file #{file}. File was not found") + {:error, :file_not_found} + {:error, err} -> Logger.warn( "Unable to optimize file #{file}. The return from the process was #{inspect(err)}" ) {:ok, :noop} - - err -> - {:error, err} end end diff --git a/lib/web/upload/filter/resize.ex b/lib/web/upload/filter/resize.ex index 12c34ea7a..be6e9e6c7 100644 --- a/lib/web/upload/filter/resize.ex +++ b/lib/web/upload/filter/resize.ex @@ -11,6 +11,7 @@ defmodule Mobilizon.Web.Upload.Filter.Resize do @maximum_width 1_920 @maximum_height 1_080 + @spec filter(Upload.t()) :: {:ok, :filtered, Upload.t()} | {:ok, :noop} def filter( %Upload{ tempfile: file, @@ -31,6 +32,7 @@ defmodule Mobilizon.Web.Upload.Filter.Resize do def filter(_), do: {:ok, :noop} + @spec limit_sizes({non_neg_integer, non_neg_integer}) :: {non_neg_integer, non_neg_integer} def limit_sizes({width, height}) when width > @maximum_width do new_height = round(@maximum_width * height / width) limit_sizes({@maximum_width, new_height}) @@ -43,5 +45,6 @@ defmodule Mobilizon.Web.Upload.Filter.Resize do def limit_sizes({width, height}), do: {width, height} + @spec string({non_neg_integer, non_neg_integer}) :: String.t() defp string({width, height}), do: "#{width}x#{height}" end diff --git a/lib/web/upload/mime.ex b/lib/web/upload/mime.ex index 8a570c12f..eb871d2f8 100644 --- a/lib/web/upload/mime.ex +++ b/lib/web/upload/mime.ex @@ -10,8 +10,9 @@ defmodule Mobilizon.Web.Upload.MIME do @default "application/octet-stream" @read_bytes 35 - @spec file_mime_type(String.t()) :: + @spec file_mime_type(String.t(), String.t()) :: {:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error + @spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error def file_mime_type(path, filename) do with {:ok, content_type} <- file_mime_type(path), filename when is_binary(filename) <- fix_extension(filename, content_type) do @@ -19,7 +20,6 @@ defmodule Mobilizon.Web.Upload.MIME do end end - @spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error def file_mime_type(filename) do File.open(filename, [:read], fn f -> check_mime_type(IO.binread(f, @read_bytes)) diff --git a/lib/web/upload/upload.ex b/lib/web/upload/upload.ex index 16b2f389b..85e14af9c 100644 --- a/lib/web/upload/upload.ex +++ b/lib/web/upload/upload.ex @@ -53,6 +53,7 @@ defmodule Mobilizon.Web.Upload do | {:size_limit, nil | non_neg_integer()} | {:uploader, module()} | {:filters, [module()]} + | {:allow_list_mime_types, boolean()} @type t :: %__MODULE__{ id: String.t(), @@ -65,33 +66,36 @@ defmodule Mobilizon.Web.Upload do height: integer(), blurhash: String.t() } - defstruct [:id, :name, :tempfile, :content_type, :path, :size, :width, :height, :blurhash] + defstruct [:id, :name, :url, :tempfile, :content_type, :path, :size, :width, :height, :blurhash] - @spec store(source, options :: [option()]) :: {:ok, map()} | {:error, any()} + @typep internal_options :: %{ + activity_type: String.t() | nil, + size_limit: integer(), + uploader: module(), + filters: [module()], + description: String.t(), + allow_list_mime_types: list(String.t()), + base_url: String.t() + } + + @spec store(source, options :: [option()]) :: + {:ok, map()} | {:error, String.t()} | {:error, atom()} def store(upload, opts \\ []) do opts = get_opts(opts) - with {:ok, upload} <- prepare_upload(upload, opts), - %__MODULE__{} = upload <- %__MODULE__{ - upload - | path: upload.path || "#{upload.id}/#{upload.name}" - }, - {:ok, upload} <- Filter.filter(opts.filters, upload), - {:ok, url_spec} <- Uploader.put_file(opts.uploader, upload) do - {:ok, - upload - |> Map.put(:name, Map.get(opts, :description) || upload.name) - |> Map.put(:url, url_from_spec(upload, opts.base_url, url_spec))} - else - {:error, error} -> - Logger.error( - "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}" - ) + case prepare_upload(upload, opts) do + {:ok, upload} -> + upload + |> set_default_upload_path() + |> perform_filter_and_put_file(opts) - {:error, error} + {:error, error} -> + error end end + @spec remove(String.t(), Keyword.t()) :: + {:ok, String.t()} | {:error, atom} | {:error, String.t()} def remove(url, opts \\ []) do with opts <- get_opts(opts), %URI{path: "/media/" <> path, host: host} <- URI.parse(url), @@ -106,10 +110,52 @@ defmodule Mobilizon.Web.Upload do end end - def char_unescaped?(char) do + @spec char_unescaped?(byte()) :: boolean() + defp char_unescaped?(char) do URI.char_unreserved?(char) or char == ?/ end + @spec set_default_upload_path(t) :: t + defp set_default_upload_path(%__MODULE__{} = upload) do + %__MODULE__{ + upload + | path: upload.path || "#{upload.id}/#{upload.name}" + } + end + + @spec perform_filter_and_put_file(t, map) :: + {:ok, t} | {:error, String.t()} | {:error, atom()} + defp perform_filter_and_put_file(%__MODULE__{} = upload, opts) do + case Filter.filter(opts.filters, upload) do + {:ok, upload} -> + perform_put_file(upload, opts) + + {:error, error} -> + {:error, error} + end + end + + @spec perform_put_file(t, map) :: {:ok, t} | {:error, atom()} + defp perform_put_file(%__MODULE__{} = upload, opts) do + case Uploader.put_file(opts.uploader, upload) do + {:ok, url_spec} -> + {:ok, + %__MODULE__{ + upload + | name: Map.get(opts, :description) || upload.name, + url: url_from_spec(upload, opts.base_url, url_spec) + }} + + {:error, error} -> + Logger.error( + "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}" + ) + + {:error, error} + end + end + + @spec get_opts(Keyword.t()) :: internal_options() defp get_opts(opts) do {size_limit, activity_type} = case Keyword.get(opts, :type) do @@ -144,6 +190,7 @@ defmodule Mobilizon.Web.Upload do } end + @spec prepare_upload(t(), internal_options()) :: {:ok, t()} defp prepare_upload(%Plug.Upload{} = file, opts) do with {:ok, size} <- check_file_size(file.path, opts.size_limit), {:ok, content_type, name} <- MIME.file_mime_type(file.path, file.filename), @@ -159,6 +206,7 @@ defmodule Mobilizon.Web.Upload do end end + @spec prepare_upload(%{body: String.t(), name: String.t()}, internal_options()) :: {:ok, t()} defp prepare_upload(%{body: body, name: name} = _file, opts) do with :ok <- check_binary_size(body, opts.size_limit), tmp_path <- tempfile_for_image(body), @@ -175,8 +223,10 @@ defmodule Mobilizon.Web.Upload do end end + @spec check_file_size(String.t(), non_neg_integer()) :: + {:ok, non_neg_integer()} | {:error, :file_too_large} | {:error, :file.posix()} defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do - with {:ok, %{size: size}} <- File.stat(path), + with {:ok, %File.Stat{size: size}} <- File.stat(path), true <- size <= size_limit do {:ok, size} else @@ -185,8 +235,7 @@ defmodule Mobilizon.Web.Upload do end end - defp check_file_size(_, _), do: :ok - + @spec check_binary_size(String.t(), non_neg_integer()) :: :ok | {:error, :file_too_large} defp check_binary_size(binary, size_limit) when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do {:error, :file_too_large} @@ -196,6 +245,7 @@ defmodule Mobilizon.Web.Upload do # Creates a tempfile using the Plug.Upload Genserver which cleans them up # automatically. + @spec tempfile_for_image(iodata) :: String.t() defp tempfile_for_image(data) do {:ok, tmp_path} = Plug.Upload.random_file("temp_files") {:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary]) @@ -204,6 +254,7 @@ defmodule Mobilizon.Web.Upload do tmp_path end + @spec url_from_spec(t, String.t(), {:file | :url, String.t()}) :: String.t() defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do path = URI.encode(path, &char_unescaped?/1) <> diff --git a/lib/web/upload/uploader/local.ex b/lib/web/upload/uploader/local.ex index 97ff58fa6..978cf59a2 100644 --- a/lib/web/upload/uploader/local.ex +++ b/lib/web/upload/uploader/local.ex @@ -19,10 +19,24 @@ defmodule Mobilizon.Web.Upload.Uploader.Local do end @impl true + @spec put_file(Upload.t()) :: + :ok | {:ok, {:file, String.t()}} | {:error, :tempfile_no_longer_exists} def put_file(%Upload{path: path, tempfile: tempfile}) do {path, file} = local_path(path) result_file = Path.join(path, file) + if File.exists?(result_file) do + # If the resulting file already exists, it's because of the Dedupe filter + :ok + else + if File.exists?(tempfile) do + File.cp!(tempfile, result_file) + {:ok, {:file, result_file}} + else + {:error, :tempfile_no_longer_exists} + end + end + with {:result_exists, false} <- {:result_exists, File.exists?(result_file)}, {:temp_file_exists, true} <- {:temp_file_exists, File.exists?(tempfile)} do File.cp!(tempfile, result_file) @@ -37,28 +51,54 @@ defmodule Mobilizon.Web.Upload.Uploader.Local do end @impl true + @spec remove_file(String.t()) :: + {:ok, {:file, String.t()}} + | {:error, :folder_not_empty} + | {:error, :enofile} + | {:error, File.posix()} def remove_file(path) do - with {path, file} <- local_path(path), - full_path <- Path.join(path, file), - true <- File.exists?(full_path), - :ok <- File.rm(full_path), - :ok <- remove_folder(path) do - {:ok, path} + {path, file} = local_path(path) + full_path = Path.join(path, file) + + if File.exists?(full_path) do + do_remove_file(path, full_path) else - false -> {:error, "File #{path} doesn't exist"} + {:error, :enofile} end end + @spec do_remove_file(String.t(), String.t()) :: + {:ok, {:file, String.t()}} + | {:error, :folder_not_empty} + | {:error, File.posix()} + defp do_remove_file(path, full_path) do + case File.rm(full_path) do + :ok -> + case remove_folder(path) do + :ok -> + {:ok, {:file, path}} + + {:error, err} -> + {:error, err} + end + + {:error, err} -> + {:error, err} + end + end + + @spec remove_folder(String.t()) :: :ok | {:error, :folder_not_empty} | {:error, File.posix()} defp remove_folder(path) do with {:subfolder, true} <- {:subfolder, path != upload_path()}, {:empty_folder, {:ok, [] = _files}} <- {:empty_folder, File.ls(path)} do File.rmdir(path) else {:subfolder, _} -> :ok - {:empty_folder, _} -> {:error, "Error: Folder is not empty"} + {:empty_folder, _} -> {:error, :folder_not_empty} end end + @spec local_path(String.t()) :: {String.t(), String.t()} defp local_path(path) do case Enum.reverse(String.split(path, "/", trim: true)) do [file] -> @@ -71,6 +111,7 @@ defmodule Mobilizon.Web.Upload.Uploader.Local do end end + @spec upload_path :: String.t() def upload_path do Config.get!([__MODULE__, :uploads]) end diff --git a/lib/web/upload/uploader/uploader.ex b/lib/web/upload/uploader/uploader.ex index 57c9de994..f5372a36c 100644 --- a/lib/web/upload/uploader/uploader.ex +++ b/lib/web/upload/uploader/uploader.ex @@ -33,9 +33,9 @@ defmodule Mobilizon.Web.Upload.Uploader do """ @type file_spec :: {:file | :url, String.t()} @callback put_file(Mobilizon.Web.Upload.t()) :: - :ok | {:ok, file_spec()} | {:error, String.t()} | :wait_callback + :ok | {:ok, file_spec()} | {:error, atom()} | :wait_callback - @callback remove_file(file_spec()) :: :ok | {:ok, file_spec()} | {:error, String.t()} + @callback remove_file(file_spec()) :: :ok | {:ok, file_spec()} | {:error, atom()} @callback http_callback(Plug.Conn.t(), map()) :: {:ok, Plug.Conn.t()} @@ -43,7 +43,7 @@ defmodule Mobilizon.Web.Upload.Uploader do | {:error, Plug.Conn.t(), String.t()} @optional_callbacks http_callback: 2 - @spec put_file(module(), Mobilizon.Web.Upload.t()) :: {:ok, file_spec()} | {:error, String.t()} + @spec put_file(module(), Mobilizon.Web.Upload.t()) :: {:ok, file_spec()} | {:error, atom()} def put_file(uploader, upload) do case uploader.put_file(upload) do :ok -> {:ok, {:file, upload.path}} @@ -53,6 +53,7 @@ defmodule Mobilizon.Web.Upload.Uploader do end end + @spec remove_file(module(), String.t()) :: {:ok, String.t()} | {:error, atom()} def remove_file(uploader, path) do uploader.remove_file(path) end diff --git a/lib/web/views/activity_pub/actor_view.ex b/lib/web/views/activity_pub/actor_view.ex index 0e61e75a6..eba9dd8a3 100644 --- a/lib/web/views/activity_pub/actor_view.ex +++ b/lib/web/views/activity_pub/actor_view.ex @@ -70,7 +70,18 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do } end - @spec fetch_collection(atom(), Actor.t(), integer()) :: Page.t() + @type collection :: + :following + | :followers + | :members + | :resources + | :discussions + | :posts + | :events + | :todos + | :outbox + + @spec fetch_collection(collection(), Actor.t(), integer()) :: Page.t() defp fetch_collection(:following, actor, page) do Actors.build_followings_for_actor(actor, page) end @@ -103,7 +114,6 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do Todos.get_todo_lists_for_group(actor, page) end - @spec fetch_collection(atom(), Actor.t(), integer()) :: %{total: integer(), elements: Enum.t()} defp fetch_collection(:outbox, actor, page) do ActivityPub.fetch_public_activities_for_actor(actor, page) end @@ -179,8 +189,7 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do def item(%Event{} = event), do: Convertible.model_to_as(event) def item(%TodoList{} = todo_list), do: Convertible.model_to_as(todo_list) - defp actor_applicant_group_member?(%Actor{}, nil), do: false - + @spec actor_applicant_group_member?(Actor.t(), Actor.t()) :: boolean() defp actor_applicant_group_member?(%Actor{id: group_id}, %Actor{id: actor_applicant_id}), do: Actors.get_member(actor_applicant_id, group_id, [ diff --git a/lib/web/views/utils.ex b/lib/web/views/utils.ex index ecc1018c4..93336f44f 100644 --- a/lib/web/views/utils.ex +++ b/lib/web/views/utils.ex @@ -8,7 +8,7 @@ defmodule Mobilizon.Web.Views.Utils do import Plug.Conn, only: [put_status: 2, halt: 1] # sobelow_skip ["Traversal.FileModule"] - @spec inject_tags(Enum.t(), String.t()) :: {:ok, {:safe, String.t()}} + @spec inject_tags(Enum.t(), String.t()) :: {:ok, {:safe, String.t()}} | {:error, atom()} def inject_tags(tags, locale \\ "en") do with path <- Path.join(Application.app_dir(:mobilizon, "priv/static"), "index.html"), {:exists, true} <- {:exists, File.exists?(path)}, @@ -17,6 +17,7 @@ defmodule Mobilizon.Web.Views.Utils do {:ok, {:safe, safe}} else {:exists, false} -> {:error, :index_not_found} + {:error, error} when is_atom(error) -> {:error, error} end end diff --git a/mix.exs b/mix.exs index 01805dd1e..6ee9c9793 100644 --- a/mix.exs +++ b/mix.exs @@ -15,6 +15,7 @@ defmodule Mobilizon.Mixfile do aliases: aliases(), deps: deps(), test_coverage: [tool: ExCoveralls], + dialyzer: [plt_add_apps: [:mix]], preferred_cli_env: [ coveralls: :test, "coveralls.detail": :test, diff --git a/test/tasks/users_test.exs b/test/tasks/users_test.exs index 89575e030..bff6acce2 100644 --- a/test/tasks/users_test.exs +++ b/test/tasks/users_test.exs @@ -277,7 +277,7 @@ defmodule Mix.Tasks.Mobilizon.UsersTest do assert {:ok, %User{confirmed_at: confirmed_at}} = Users.get_user_by_email(@email) assert output_received == - "An user has been modified with the following information:\n - email: #{user.email}\n - Role: #{user.role}\n - account status: activated on #{confirmed_at} (UTC)\n" + "An user has been modified with the following information:\n - email: #{user.email}\n - Role: #{user.role}\n - account status: Activated on #{confirmed_at} (UTC)\n" refute is_nil(confirmed_at) @@ -308,7 +308,7 @@ defmodule Mix.Tasks.Mobilizon.UsersTest do Users.get_user_by_email(@modified_email) assert output_received == - "An user has been modified with the following information:\n - email: #{@modified_email}\n - Role: #{user.role}\n - account status: activated on #{confirmed_at} (UTC)\n" + "An user has been modified with the following information:\n - email: #{@modified_email}\n - Role: #{user.role}\n - account status: Activated on #{confirmed_at} (UTC)\n" end end end diff --git a/test/web/upload/upload_test.exs b/test/web/upload/upload_test.exs index b9aeadbeb..871a6e10e 100644 --- a/test/web/upload/upload_test.exs +++ b/test/web/upload/upload_test.exs @@ -127,7 +127,8 @@ defmodule Mobilizon.UploadTest do filename: "test.txt" } - assert {:error, :mime_type_not_allowed} == Upload.store(file) + res = Upload.store(file) + assert match?({:error, :mime_type_not_allowed}, res) end test "copies the file to the configured folder with anonymizing filename" do @@ -189,7 +190,7 @@ defmodule Mobilizon.UploadTest do refute File.exists?(file) - assert {:error, "File not_existing/definitely.jpg doesn't exist"} = + assert {:error, :enofile} = Upload.remove("https://mobilizon.test/media/not_existing/definitely.jpg") end end From 6bb0b6d08ac478d228af122ef304aec654ebdf43 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 10 Sep 2021 11:28:38 +0200 Subject: [PATCH 02/17] Improve Gettext compilation Signed-off-by: Thomas Citharel --- config/config.exs | 2 ++ config/dev.exs | 2 ++ config/test.exs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/config/config.exs b/config/config.exs index 9f125f79e..1830afcd9 100644 --- a/config/config.exs +++ b/config/config.exs @@ -179,6 +179,8 @@ config :phoenix, :filter_parameters, ["password", "token"] config :absinthe, schema: Mobilizon.GraphQL.Schema config :absinthe, Absinthe.Logger, filter_variables: ["token", "password", "secret"] +config :mobilizon, Mobilizon.Web.Gettext, one_module_per_locale: true + config :ex_cldr, default_locale: "en", default_backend: Mobilizon.Cldr diff --git a/config/dev.exs b/config/dev.exs index 660e6dd58..291bdc463 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -58,6 +58,8 @@ config :logger, :console, format: "[$level] $message\n", level: :debug config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim +config :mobilizon, Mobilizon.Web.Gettext, allowed_locales: ["fr", "en"] + # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. config :phoenix, :stacktrace_depth, 20 diff --git a/config/test.exs b/config/test.exs index 4f024d17c..788b683f6 100644 --- a/config/test.exs +++ b/config/test.exs @@ -77,6 +77,8 @@ config :mobilizon, Mobilizon.Web.Auth.Guardian, secret_key: "some secret" config :mobilizon, :activitypub, sign_object_fetches: false +config :mobilizon, Mobilizon.Web.Gettext, allowed_locales: ["fr", "en"] + config :junit_formatter, report_dir: "." if System.get_env("DOCKER", "false") == "false" && File.exists?("./config/test.secret.exs") do From e9e12500dc652e65d57faa41ad91f113176702e9 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 10 Sep 2021 11:29:28 +0200 Subject: [PATCH 03/17] Fix tags autocomplete Signed-off-by: Thomas Citharel --- js/src/components/Event/TagInput.vue | 38 ++++++++++++++------ js/src/graphql/tags.ts | 20 +++++++---- js/src/views/Event/Edit.vue | 7 +--- js/src/views/Posts/Edit.vue | 4 +-- lib/graphql/resolvers/tag.ex | 5 +-- test/graphql/resolvers/tag_test.exs | 52 +++++++++++++++++---------- test/mobilizon/events/events_test.exs | 6 ++++ 7 files changed, 84 insertions(+), 48 deletions(-) diff --git a/js/src/components/Event/TagInput.vue b/js/src/components/Event/TagInput.vue index 6537e50b8..e866819ba 100644 --- a/js/src/components/Event/TagInput.vue +++ b/js/src/components/Event/TagInput.vue @@ -29,19 +29,28 @@
+ <%= gettext "View report" %>