Merge branch 'fixes' into 'main'

Fix various instance view stuff and legacy cleaning

Closes #1393

See merge request framasoft/mobilizon!1520
This commit is contained in:
Thomas Citharel 2024-01-04 13:00:30 +00:00
commit b317fe6163
61 changed files with 288 additions and 256 deletions

View file

@ -33,7 +33,7 @@
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
#
strict: false,
strict: true,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
@ -160,6 +160,7 @@
#
{Credo.Check.Warning.LazyLogging, false},
{Credo.Check.Refactor.MapInto, false},
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, false}
]
}
]

View file

@ -25,7 +25,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Invite do
) do
Logger.debug("Handling #{actor_url} invite to #{group_url} sent to #{target_actor_url}")
if is_able_to_invite?(actor, group) do
if able_to_invite?(actor, group) do
with {:ok, %Member{url: member_url} = member} <-
Actors.create_member(%{
parent_id: group_id,
@ -64,8 +64,8 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Invite do
end
end
@spec is_able_to_invite?(Actor.t(), Actor.t()) :: boolean
defp is_able_to_invite?(%Actor{domain: actor_domain, id: actor_id}, %Actor{
@spec able_to_invite?(Actor.t(), Actor.t()) :: boolean
defp able_to_invite?(%Actor{domain: actor_domain, id: actor_id}, %Actor{
domain: group_domain,
id: group_id
}) do
@ -76,7 +76,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Invite do
# If local group, we'll send the invite
case Actors.get_member(actor_id, group_id) do
{:ok, %Member{} = admin_member} ->
Member.is_administrator(admin_member)
Member.administrator?(admin_member)
_ ->
false

View file

@ -34,7 +34,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Leave do
local,
additional
) do
if Participant.is_not_only_organizer(event_id, actor_id) do
if Participant.not_only_organizer?(event_id, actor_id) do
{:error, :is_only_organizer}
else
case Mobilizon.Events.get_participant(
@ -83,7 +83,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Leave do
case Actors.get_member(actor_id, group_id) do
{:ok, %Member{id: member_id} = member} ->
if Map.get(additional, :force_member_removal, false) || group_domain != actor_domain ||
!Actors.is_only_administrator?(member_id, group_id) do
!Actors.only_administrator?(member_id, group_id) do
with {:ok, %Member{} = member} <- Actors.delete_member(member) do
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_quit")

View file

@ -44,13 +44,13 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
) do
case object |> Ownable.permissions() |> get_in([:create]) do
:member ->
Actors.is_member?(actor_id, group_id)
Actors.member?(actor_id, group_id)
:moderator ->
Actors.is_moderator?(actor_id, group_id)
Actors.moderator?(actor_id, group_id)
:administrator ->
Actors.is_administrator?(actor_id, group_id)
Actors.administrator?(actor_id, group_id)
_ ->
false
@ -122,21 +122,21 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
"Checking if activity actor #{actor_url} is a moderator from group from #{object.url}"
)
Actors.is_moderator?(actor_id, group_id)
Actors.moderator?(actor_id, group_id)
:administrator ->
Logger.debug(
"Checking if activity actor #{actor_url} is an administrator from group from #{object.url}"
)
Actors.is_administrator?(actor_id, group_id)
Actors.administrator?(actor_id, group_id)
_ ->
Logger.debug(
"Checking if activity actor #{actor_url} is a member from group from #{object.url}"
)
Actors.is_member?(actor_id, group_id)
Actors.member?(actor_id, group_id)
end
_ ->

View file

@ -21,10 +21,10 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
Logger.debug("Publishing an activity")
Logger.debug(inspect(activity, pretty: true))
public = Visibility.is_public?(activity)
public = Visibility.public?(activity)
Logger.debug("is public ? #{public}")
if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do
if public && create_activity?(activity) && Config.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
@ -125,9 +125,9 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
end)
end
@spec is_create_activity?(Activity.t()) :: boolean
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
defp is_create_activity?(_), do: false
@spec create_activity?(Activity.t()) :: boolean
defp create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
defp create_activity?(_), do: false
@spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())}
defp convert_members_in_recipients(recipients) do

View file

@ -285,7 +285,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data when is_map(object_data) <-
object |> Converter.Resource.as_to_model_data(),
{:member, true} <-
{:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)},
{:member, Actors.member?(object_data.creator_id, object_data.actor_id)},
{:ok, %Activity{} = activity, %Resource{} = resource} <-
Actions.Create.create(:resource, object_data, false) do
{:ok, activity, resource}
@ -1005,14 +1005,14 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
# Comment initiates a whole discussion only if it has full title
@spec is_data_for_comment_or_discussion?(map()) :: boolean()
defp is_data_for_comment_or_discussion?(object_data) do
is_data_a_discussion_initialization?(object_data) and
@spec data_for_comment_or_discussion?(map()) :: boolean()
defp data_for_comment_or_discussion?(object_data) do
data_a_discussion_initialization?(object_data) and
is_nil(object_data.discussion_id)
end
# Comment initiates a whole discussion only if it has full title
defp is_data_a_discussion_initialization?(object_data) do
defp data_a_discussion_initialization?(object_data) do
not Map.has_key?(object_data, :title) or
is_nil(object_data.title) or object_data.title == ""
end
@ -1034,7 +1034,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
@spec transform_object_data_for_discussion(map()) :: map()
defp transform_object_data_for_discussion(object_data) do
# Basic comment
if is_data_a_discussion_initialization?(object_data) do
if data_a_discussion_initialization?(object_data) do
object_data
else
# Conversation
@ -1138,8 +1138,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
defp is_group_object_gone(object_id) do
Logger.debug("is_group_object_gone #{object_id}")
defp group_object_gone_check(object_id) do
Logger.debug("Checking if group object #{object_id} is gone")
case ActivityPub.fetch_object_from_url(object_id, force: true) do
# comments are just emptied
@ -1163,14 +1163,10 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
# Before 1.0.4 the object of a "Remove" activity was an actor's URL
# instead of the member's URL.
# TODO: Remove in 1.2
@spec get_remove_object(map() | String.t()) :: {:ok, integer()}
defp get_remove_object(object) do
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
{:ok, %Member{actor: %Actor{id: person_id}}} -> {:ok, person_id}
{:ok, %Actor{id: person_id}} -> {:ok, person_id}
_ -> {:error, :remove_object_not_found}
end
end
@ -1196,7 +1192,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
@spec create_comment_or_discussion(map()) ::
{:ok, Activity.t(), struct()} | {:error, atom() | Ecto.Changeset.t()}
defp create_comment_or_discussion(object_data) do
if is_data_for_comment_or_discussion?(object_data) do
if data_for_comment_or_discussion?(object_data) do
Logger.debug("Chosing to create a regular comment")
Actions.Create.create(:comment, object_data, false)
else
@ -1248,7 +1244,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
defp handle_group_being_gone(actor, actor_url, object_id) do
case is_group_object_gone(object_id) do
case group_object_gone_check(object_id) do
# The group object is no longer there, we can remove the element
{:ok, entity} ->
if Utils.origin_check_from_id?(actor_url, object_id) ||

View file

@ -93,7 +93,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
atom()
) :: 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 &&
Actors.only_administrator?(member_id, group_id) && current_role == :administrator &&
updated_role != :administrator
end
end

View file

@ -14,17 +14,17 @@ defmodule Mobilizon.Federation.ActivityPub.Visibility do
@public "https://www.w3.org/ns/activitystreams#Public"
@spec is_public?(Activity.t() | map()) :: boolean()
def is_public?(%{data: %{"type" => "Tombstone"}}), do: false
def is_public?(%{data: data}), do: is_public?(data)
def is_public?(%Activity{data: data}), do: is_public?(data)
@spec public?(Activity.t() | map()) :: boolean()
def public?(%{data: %{"type" => "Tombstone"}}), do: false
def public?(%{data: data}), do: public?(data)
def public?(%Activity{data: data}), do: public?(data)
def is_public?(data) when is_map(data) do
def public?(data) when is_map(data) do
@public in make_list(Map.get(data, "to", []))
end
def is_public?(%Comment{deleted_at: deleted_at}), do: !is_nil(deleted_at)
def is_public?(err), do: raise(ArgumentError, message: "Invalid argument #{inspect(err)}")
def public?(%Comment{deleted_at: deleted_at}), do: !is_nil(deleted_at)
def public?(err), do: raise(ArgumentError, message: "Invalid argument #{inspect(err)}")
defp make_list(data) when is_list(data), do: data
defp make_list(data), do: [data]

View file

@ -76,14 +76,13 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
def as_to_model_data(_), do: {:error, :actor_not_allowed_type}
defp add_endpoints_to_model(actor, data) do
# TODO: Remove fallbacks in 3.0
endpoints = %{
members_url: get_in(data, ["endpoints", "members"]) || data["members"],
resources_url: get_in(data, ["endpoints", "resources"]) || data["resources"],
todos_url: get_in(data, ["endpoints", "todos"]) || data["todos"],
events_url: get_in(data, ["endpoints", "events"]) || data["events"],
posts_url: get_in(data, ["endpoints", "posts"]) || data["posts"],
discussions_url: get_in(data, ["endpoints", "discussions"]) || data["discussions"],
members_url: get_in(data, ["endpoints", "members"]),
resources_url: get_in(data, ["endpoints", "resources"]),
todos_url: get_in(data, ["endpoints", "todos"]),
events_url: get_in(data, ["endpoints", "events"]),
posts_url: get_in(data, ["endpoints", "posts"]),
discussions_url: get_in(data, ["endpoints", "discussions"]),
shared_inbox_url: data["endpoints"]["sharedInbox"]
}

View file

@ -62,7 +62,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
tags: fetch_tags(tag_object),
mentions: fetch_mentions(tag_object),
local: is_nil(actor_domain),
visibility: if(Visibility.is_public?(object), do: :public, else: :private),
visibility: if(Visibility.public?(object), do: :public, else: :private),
published_at: object["published"],
is_announcement: Map.get(object, "isAnnouncement", false)
}

View file

@ -77,7 +77,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
category: Categories.get_category(object["category"]),
visibility: visibility,
join_options: Map.get(object, "joinMode", "free"),
local: is_local?(object["id"]),
local: local?(object["id"]),
external_participation_url: object["externalParticipationUrl"],
options: options,
metadata: metadata,
@ -305,8 +305,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
)
end
@spec is_local?(String.t()) :: boolean()
defp is_local?(url) do
@spec local?(String.t()) :: boolean()
defp local?(url) do
%URI{host: url_domain} = URI.parse(url)
%URI{host: local_domain} = URI.parse(Endpoint.url())
url_domain == local_domain

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Federation.NodeInfo do
alias Mobilizon.Service.HTTP.WebfingerClient
require Logger
import Mobilizon.Service.HTTP.Utils, only: [is_content_type?: 2]
import Mobilizon.Service.HTTP.Utils, only: [content_type_matches?: 2]
@application_uri "https://www.w3.org/ns/activitystreams#Application"
@nodeinfo_rel_2_0 "http://nodeinfo.diaspora.software/ns/schema/2.0"
@ -110,7 +110,7 @@ defmodule Mobilizon.Federation.NodeInfo do
{:ok, String.t()} | {:error, :bad_content_type | :body_not_json}
defp validate_json_response(body, headers) do
cond do
!is_content_type?(headers, "application/json") ->
!content_type_matches?(headers, "application/json") ->
{:error, :bad_content_type}
!is_map(body) ->

View file

@ -25,8 +25,8 @@ defmodule Mobilizon.GraphQL.API.Search do
cond do
# Some URLs could be domain.tld/@username, so keep this condition above
# the `is_handle` function
is_url(term) ->
# the `handle?` function
url?(term) ->
# skip, if it's not an actor
case process_from_url(term) do
%Page{total: _total, elements: [%Actor{} = _actor]} = page ->
@ -36,11 +36,11 @@ defmodule Mobilizon.GraphQL.API.Search do
{:ok, %{total: 0, elements: []}}
end
is_handle(term) ->
handle?(term) ->
{:ok, process_from_username(term)}
true ->
if is_global_search(args) do
if global_search?(args) do
service = GlobalSearch.service()
{:ok, service.search_groups(Keyword.new(args, fn {k, v} -> {k, v} end))}
@ -75,7 +75,7 @@ defmodule Mobilizon.GraphQL.API.Search do
def search_events(%{term: term} = args, page \\ 1, limit \\ 10) do
term = String.trim(term)
if is_url(term) do
if url?(term) do
# skip, if it's not an event
case process_from_url(term) do
%Page{total: _total, elements: [%Event{} = event]} = page ->
@ -89,7 +89,7 @@ defmodule Mobilizon.GraphQL.API.Search do
{:ok, %{total: 0, elements: []}}
end
else
if is_global_search(args) do
if global_search?(args) do
service = GlobalSearch.service()
{:ok, service.search_events(Keyword.new(args, fn {k, v} -> {k, v} end))}
@ -140,17 +140,17 @@ defmodule Mobilizon.GraphQL.API.Search do
end
end
@spec is_url(String.t()) :: boolean
defp is_url(search), do: String.starts_with?(search, ["http://", "https://"])
@spec url?(String.t()) :: boolean
defp url?(search), do: String.starts_with?(search, ["http://", "https://"])
@spec is_handle(String.t()) :: boolean
defp is_handle(search), do: String.match?(search, ~r/@/)
@spec handle?(String.t()) :: boolean
defp handle?(search), do: String.match?(search, ~r/@/)
defp is_global_search(%{search_target: :global}) do
defp global_search?(%{search_target: :global}) do
global_search_enabled?()
end
defp is_global_search(_), do: global_search_enabled?() && global_search_default?()
defp global_search?(_), do: global_search_enabled?() && global_search_default?()
defp global_search_enabled? do
Application.get_env(:mobilizon, :search) |> get_in([:global]) |> get_in([:is_enabled])

View file

@ -68,6 +68,6 @@ defmodule Mobilizon.GraphQL.API.Utils do
@spec check_actor_owns_media?(integer() | String.t(), integer() | String.t()) :: boolean()
defp check_actor_owns_media?(actor_id, media_actor_id) do
actor_id == media_actor_id || Mobilizon.Actors.is_member?(media_actor_id, actor_id)
actor_id == media_actor_id || Mobilizon.Actors.member?(media_actor_id, actor_id)
end
end

View file

@ -5,8 +5,8 @@ defmodule Mobilizon.GraphQL.Error do
require Logger
alias __MODULE__
alias Mobilizon.Web.Gettext, as: GettextBackend
import Mobilizon.Web.Gettext, only: [dgettext: 2]
import Mobilizon.Storage.Ecto, only: [convert_ecto_errors: 1]
@type t :: %{code: atom(), message: String.t(), status_code: pos_integer(), field: atom()}
@ -64,7 +64,7 @@ defmodule Mobilizon.GraphQL.Error do
defp handle(%Ecto.Changeset{} = changeset) do
changeset
|> Ecto.Changeset.traverse_errors(&translate_error/1)
|> convert_ecto_errors()
|> Enum.map(fn {k, v} ->
%Error{
code: :validation,
@ -126,27 +126,4 @@ defmodule Mobilizon.GraphQL.Error do
Logger.warning("Unhandled error code: #{inspect(code)}")
{422, to_string(code)}
end
# Translates an error message using gettext.
defp translate_error({msg, opts}) do
# Because error messages were defined within Ecto, we must
# call the Gettext module passing our Gettext backend. We
# also use the "errors" domain as translations are placed
# in the errors.po file.
# Ecto will pass the :count keyword if the error message is
# meant to be pluralized.
# On your own code and templates, depending on whether you
# need the message to be pluralized or not, this could be
# written simply as:
#
# dngettext "errors", "1 file", "%{count} files", count
# dgettext "errors", "is invalid"
#
if count = opts[:count] do
Gettext.dngettext(GettextBackend, "errors", msg, msg, count, opts)
else
Gettext.dgettext(GettextBackend, "errors", msg, opts)
end
end
end

View file

@ -18,7 +18,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
def group_activity(%Actor{type: :Group, id: group_id}, %{page: page, limit: limit} = args, %{
context: %{current_user: %User{role: role}, current_actor: %Actor{id: actor_id}}
}) do
if Actors.is_member?(actor_id, group_id) or is_moderator(role) do
if Actors.member?(actor_id, group_id) or is_moderator(role) do
%Page{total: total, elements: elements} =
Activities.list_group_activities_for_member(
group_id,

View file

@ -26,7 +26,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do
}
)
when not is_nil(attributed_to_id) do
if Actors.is_member?(actor_id, attributed_to_id) do
if Actors.member?(actor_id, attributed_to_id) do
{:ok,
event_id
|> Conversations.find_conversations_for_event(attributed_to_id, page, limit)
@ -103,7 +103,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do
{:error, :not_found}
%ConversationParticipant{actor_id: actor_id} = conversation_participant ->
if actor_id == performing_actor_id or Actors.is_member?(performing_actor_id, actor_id) do
if actor_id == performing_actor_id or Actors.member?(performing_actor_id, actor_id) do
{:ok, conversation_participant_to_view(conversation_participant)}
else
{:error, :not_found}
@ -121,7 +121,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do
}
) do
if conversation_actor_id == performing_actor_id or
Actors.is_member?(performing_actor_id, conversation_actor_id) do
Actors.member?(performing_actor_id, conversation_actor_id) do
{:ok,
Mobilizon.Discussions.get_comments_in_reply_to_comment_id(origin_comment_id, page, limit)}
else
@ -184,7 +184,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do
{:valid_actor, true} <-
{:valid_actor,
actor_id == current_actor_id or
Actors.is_member?(current_actor_id, actor_id)},
Actors.member?(current_actor_id, actor_id)},
{:ok, %ConversationParticipant{} = conversation_participant} <-
Conversations.update_conversation_participant(conversation_participant, %{
unread: !read
@ -269,7 +269,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do
to_string(current_actor_id) in participant_ids or
Enum.any?(participant_ids, fn participant_id ->
Actors.is_member?(current_actor_id, participant_id) and
Actors.member?(current_actor_id, participant_id) and
attributed_to_id == participant_id
end)
end

View file

@ -23,7 +23,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
}
}
) do
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
with {:member, true} <- {:member, Actors.member?(actor_id, group_id)},
{:ok, %Actor{type: :Group} = group} <- Actors.get_group_by_actor_id(group_id) do
{:ok, Discussions.find_discussions_for_actor(group, page, limit)}
else
@ -45,7 +45,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
}) do
case Discussions.get_discussion(id) do
%Discussion{actor_id: actor_id} = discussion ->
if Actors.is_member?(creator_id, actor_id) do
if Actors.member?(creator_id, actor_id) do
{:ok, discussion}
else
{:error, :unauthorized}
@ -63,7 +63,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
}) do
with %Discussion{actor_id: actor_id} = discussion <-
Discussions.get_discussion_by_slug(slug),
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do
{:member, true} <- {:member, Actors.member?(creator_id, actor_id)} do
{:ok, discussion}
else
nil -> {:error, dgettext("errors", "Discussion not found")}
@ -105,7 +105,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
}
}
) do
if Actors.is_member?(creator_id, group_id) do
if Actors.member?(creator_id, group_id) do
case Comments.create_discussion(%{
title: title,
text: text,
@ -150,7 +150,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
}
} = _discussion} <-
{:no_discussion, Discussions.get_discussion(discussion_id)},
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
{:member, true} <- {:member, Actors.member?(creator_id, actor_id)},
{:ok, _activity, %Discussion{} = discussion} <-
Comments.create_discussion(%{
text: text,
@ -183,7 +183,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
) do
with {:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
{:no_discussion, Discussions.get_discussion(discussion_id)},
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
{:member, true} <- {:member, Actors.member?(creator_id, actor_id)},
{:ok, _activity, %Discussion{} = discussion} <-
Actions.Update.update(
discussion,
@ -213,7 +213,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
}) do
with {:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
{:no_discussion, Discussions.get_discussion(discussion_id)},
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
{:member, true} <- {:member, Actors.member?(creator_id, actor_id)},
{:ok, _activity, %Discussion{} = discussion} <-
Actions.Delete.delete(discussion, actor) do
{:ok, discussion}

View file

@ -36,7 +36,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
when not is_nil(attributed_to_id) do
with %Actor{id: group_id} <- Actors.get_actor(attributed_to_id),
{:member, true} <-
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)},
{:member, Actors.member?(actor_id, group_id) or is_moderator(user_role)},
%Actor{} = actor <- Actors.get_actor(organizer_actor_id) do
{:ok, actor}
else
@ -176,7 +176,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
_args,
%{context: %{current_user: %User{id: user_id} = _user}} = _resolution
) do
if Events.is_user_moderator_for_event?(user_id, event_id) do
if Events.user_moderator_for_event?(user_id, event_id) do
{:ok,
Map.put(
stats,
@ -256,7 +256,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:can_create_event, true} <- can_create_event(args),
{:event_external, true} <- edit_event_external_checker(args),
{:organizer_group_member, true} <-
{:organizer_group_member, is_organizer_group_member?(args)},
{:organizer_group_member, organizer_group_member?(args)},
args_with_organizer <-
args |> Map.put(:organizer_actor, organizer_actor) |> extract_timezone(user.id),
{:askismet, :ham} <-
@ -447,17 +447,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
end
end
@spec is_organizer_group_member?(map()) :: boolean()
defp is_organizer_group_member?(%{
@spec organizer_group_member?(map()) :: boolean()
defp organizer_group_member?(%{
attributed_to_id: attributed_to_id,
organizer_actor_id: organizer_actor_id
})
when not is_nil(attributed_to_id) do
Actors.is_member?(organizer_actor_id, attributed_to_id) &&
Actors.member?(organizer_actor_id, attributed_to_id) &&
Permission.can_create_group_object?(organizer_actor_id, attributed_to_id, %Event{})
end
defp is_organizer_group_member?(_), do: true
defp organizer_group_member?(_), do: true
@spec verify_profile_change(map(), Event.t(), User.t(), Actor.t()) :: {:ok, map()}
defp verify_profile_change(

View file

@ -23,7 +23,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do
) do
followers = group_followers(group, args)
if Actors.is_moderator?(actor_id, group_id) or is_moderator(user_role) do
if Actors.moderator?(actor_id, group_id) or is_moderator(user_role) do
{:ok, followers}
else
{:ok, %Page{followers | elements: []}}
@ -48,7 +48,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do
with %Follower{target_actor: %Actor{type: :Group, id: group_id}} = follower <-
Actors.get_follower(follower_id),
{:member, true} <-
{:member, Actors.is_moderator?(actor_id, group_id)},
{:member, Actors.moderator?(actor_id, group_id)},
{:ok, _activity, %Follower{} = follower} <-
(if approved do
Actions.Accept.accept(:follow, follower)

View file

@ -36,7 +36,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
) do
case ActivityPubActor.find_or_make_group_from_nickname(name) do
{:ok, %Actor{id: group_id, suspended: false} = group} ->
if Actors.is_member?(actor_id, group_id) do
if Actors.member?(actor_id, group_id) do
{:ok, group}
else
find_group(parent, args, nil)
@ -72,7 +72,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
}
}) do
with %Actor{suspended: false, id: group_id} = group <- Actors.get_actor_with_preload(id),
true <- Actors.is_member?(actor_id, group_id) do
true <- Actors.member?(actor_id, group_id) do
{:ok, group}
else
_ ->
@ -215,7 +215,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
}
}
) do
if Actors.is_administrator?(updater_actor.id, group_id) do
if Actors.administrator?(updater_actor.id, group_id) do
args = Map.put(args, :updater_actor, updater_actor)
case save_attached_pictures(args) do
@ -265,7 +265,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
) do
with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
{:is_admin, true} <- {:is_admin, Member.is_administrator(member)},
{:is_admin, true} <- {:is_admin, Member.administrator?(member)},
{:ok, _activity, group} <- Actions.Delete.delete(group, actor, true) do
{:ok, %{id: group.id}}
else
@ -448,7 +448,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
}
}
) do
if Actors.is_member?(actor_id, group_id) do
if Actors.member?(actor_id, group_id) do
{:ok,
Events.list_organized_events_for_group(
group,

View file

@ -26,7 +26,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
context: %{current_user: %User{role: user_role}, current_actor: %Actor{id: actor_id}}
} = _resolution
) do
if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do
if Actors.member?(actor_id, group_id) or is_moderator(user_role) do
roles =
case roles do
"" ->

View file

@ -384,7 +384,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
with {:member, true} <-
{:member,
to_string(current_actor_id) == to_string(actor_id) or
Actors.is_member?(current_actor_id, actor_id)},
Actors.member?(current_actor_id, actor_id)},
{:ok, _activity, %Conversation{} = conversation} <- Comments.create_conversation(args) do
{:ok, conversation_to_view(conversation, Actors.get_actor(actor_id))}
else

View file

@ -32,7 +32,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
}
} = _resolution
) do
if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do
if Actors.member?(actor_id, group_id) or is_moderator(user_role) do
%Page{} = page = Posts.get_posts_for_group(group, page, limit)
{:ok, page}
else
@ -111,7 +111,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
}
} = _resolution
) do
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
with {:member, true} <- {:member, Actors.member?(actor_id, group_id)},
%Actor{} = group <- Actors.get_actor(group_id),
args <-
Map.update(args, :picture, nil, fn picture ->
@ -160,7 +160,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
process_picture(picture, group)
end),
args <- extract_pictures_from_post_body(args, actor_id),
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.member?(actor_id, group_id)},
{:ok, _, %Post{} = post} <-
Actions.Update.update(post, args, true, %{"actor" => actor_url}) do
{:ok, post}
@ -194,7 +194,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(post_id)},
{:post, %Post{attributed_to: %Actor{id: group_id}} = post} <-
{:post, Posts.get_post_with_preloads(post_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.member?(actor_id, group_id)},
{:ok, _, %Post{} = post} <-
Actions.Delete.delete(post, actor) do
{:ok, post}

View file

@ -32,7 +32,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
}
} = _resolution
) do
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
with {:member, true} <- {:member, Actors.member?(actor_id, group_id)},
%Page{} = page <- Resources.get_resources_for_group(group, page, limit) do
{:ok, page}
else
@ -60,7 +60,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
}
} = _resolution
) do
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
with {:member, true} <- {:member, Actors.member?(actor_id, group_id)},
%Page{} = page <- Resources.get_resources_for_folder(parent, page, limit) do
{:ok, page}
end
@ -83,7 +83,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
Logger.debug("Getting resource for group with username #{username}")
with {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(username, :Group)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.member?(actor_id, group_id)},
{:resource, %Resource{} = resource} <-
{:resource, Resources.get_resource_by_group_and_path_with_preloads(group_id, path)} do
{:ok, resource}
@ -109,7 +109,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
}
} = _resolution
) do
if Actors.is_member?(actor_id, group_id) do
if Actors.member?(actor_id, group_id) do
parent = get_eventual_parent(args)
if check_resource_owned_by_group(parent, group_id) do
@ -155,7 +155,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
) do
case Resources.get_resource_with_preloads(resource_id) do
%Resource{actor_id: group_id} = resource ->
if Actors.is_member?(actor_id, group_id) do
if Actors.member?(actor_id, group_id) do
case Actions.Update.update(resource, args, true, %{"actor" => actor_url}) do
{:ok, _, %Resource{} = resource} ->
{:ok, resource}
@ -192,7 +192,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
) do
with {:resource, %Resource{parent_id: _parent_id, actor_id: group_id} = resource} <-
{:resource, Resources.get_resource_with_preloads(resource_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.member?(actor_id, group_id)},
{:ok, _, %Resource{} = resource} <-
Actions.Delete.delete(resource, actor) do
{:ok, resource}

View file

@ -26,7 +26,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
context: %{current_actor: %Actor{id: actor_id}}
} = _resolution
) do
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
with {:member, true} <- {:member, Actors.member?(actor_id, group_id)},
%Page{} = page <- Todos.get_todo_lists_for_group(group, page, limit) do
{:ok, page}
else
@ -50,7 +50,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
context: %{current_actor: %Actor{id: actor_id}}
} = _resolution
) do
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
with {:member, true} <- {:member, Actors.member?(actor_id, group_id)},
%Page{} = page <- Todos.get_todos_for_todo_list(todo_list, page, limit) do
{:ok, page}
else
@ -70,7 +70,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
) do
with {:todo, %TodoList{actor_id: group_id} = todo} <-
{:todo, Todos.get_todo_list(todo_list_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
{:member, true} <- {:member, Actors.member?(actor_id, group_id)} do
{:ok, todo}
else
{:todo, nil} ->
@ -93,7 +93,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
context: %{current_actor: %Actor{id: actor_id}}
} = _resolution
) do
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
with {:member, true} <- {:member, Actors.member?(actor_id, group_id)},
{:ok, _, %TodoList{} = todo_list} <-
Actions.Create.create(
:todo_list,
@ -121,7 +121,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
# with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
# {:todo_list, %TodoList{actor_id: group_id} = todo_list} <-
# {:todo_list, Todos.get_todo_list(todo_list_id)},
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
# {:member, true} <- {:member, Actors.member?(actor_id, group_id)},
# {:ok, _, %TodoList{} = todo} <-
# Actions.Update.update_todo_list(todo_list, actor, true, %{}) do
# {:ok, todo}
@ -144,7 +144,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
# with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
# {:todo_list, %TodoList{actor_id: group_id} = todo_list} <-
# {:todo_list, Todos.get_todo_list(todo_list_id)},
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
# {:member, true} <- {:member, Actors.member?(actor_id, group_id)},
# {:ok, _, %TodoList{} = todo} <-
# Actions.Delete.delete_todo_list(todo_list, actor, true, %{}) do
# {:ok, todo}
@ -169,7 +169,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
{:todo, Todos.get_todo(todo_id)},
{:todo_list, %TodoList{actor_id: group_id}} <-
{:todo_list, Todos.get_todo_list(todo_list_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
{:member, true} <- {:member, Actors.member?(actor_id, group_id)} do
{:ok, todo}
else
{:todo, nil} ->
@ -194,7 +194,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
) do
with {:todo_list, %TodoList{actor_id: group_id} = _todo_list} <-
{:todo_list, Todos.get_todo_list(todo_list_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.member?(actor_id, group_id)},
{:ok, _, %Todo{} = todo} <-
Actions.Create.create(
:todo,
@ -228,7 +228,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
{:todo, Todos.get_todo(todo_id)},
{:todo_list, %TodoList{actor_id: group_id}} <-
{:todo_list, Todos.get_todo_list(todo_list_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.member?(actor_id, group_id)},
{:ok, _, %Todo{} = todo} <-
Actions.Update.update(todo, args, true, %{}) do
{:ok, todo}
@ -259,7 +259,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
# {:todo, Todos.get_todo(todo_id)},
# {:todo_list, %TodoList{actor_id: group_id}} <-
# {:todo_list, Todos.get_todo_list(todo_list_id)},
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
# {:member, true} <- {:member, Actors.member?(actor_id, group_id)},
# {:ok, _, %Todo{} = todo} <-
# Actions.Delete.delete_todo(todo, actor, true, %{}) do
# {:ok, todo}

View file

@ -209,8 +209,8 @@ defmodule Mobilizon.Actors.Actor do
@doc """
Checks whether actor visibility is public.
"""
@spec is_public_visibility?(t) :: boolean
def is_public_visibility?(%__MODULE__{visibility: visibility}) do
@spec public_visibility?(t) :: boolean
def public_visibility?(%__MODULE__{visibility: visibility}) do
visibility in [:public, :unlisted]
end

View file

@ -710,8 +710,8 @@ defmodule Mobilizon.Actors do
@doc """
Returns whether the `actor_id` is a confirmed member for the group `parent_id`
"""
@spec is_member?(integer | String.t(), integer | String.t()) :: boolean()
def is_member?(actor_id, parent_id) do
@spec member?(integer | String.t(), integer | String.t()) :: boolean()
def member?(actor_id, parent_id) do
match?(
{:ok, %Member{}},
get_member(actor_id, parent_id, @member_roles)
@ -721,8 +721,8 @@ defmodule Mobilizon.Actors do
@doc """
Returns whether the `actor_id` is a moderator for the group `parent_id`
"""
@spec is_moderator?(integer | String.t(), integer | String.t()) :: boolean()
def is_moderator?(actor_id, parent_id) do
@spec moderator?(integer | String.t(), integer | String.t()) :: boolean()
def moderator?(actor_id, parent_id) do
match?(
{:ok, %Member{}},
get_member(actor_id, parent_id, @moderator_roles)
@ -732,8 +732,8 @@ defmodule Mobilizon.Actors do
@doc """
Returns whether the `actor_id` is an administrator for the group `parent_id`
"""
@spec is_administrator?(integer | String.t(), integer | String.t()) :: boolean()
def is_administrator?(actor_id, parent_id) do
@spec administrator?(integer | String.t(), integer | String.t()) :: boolean()
def administrator?(actor_id, parent_id) do
match?(
{:ok, %Member{}},
get_member(actor_id, parent_id, @administrator_roles)
@ -922,8 +922,8 @@ defmodule Mobilizon.Actors do
@doc """
Returns whether the member is the last administrator for a group
"""
@spec is_only_administrator?(integer | String.t(), integer | String.t()) :: boolean()
def is_only_administrator?(member_id, group_id) do
@spec only_administrator?(integer | String.t(), integer | String.t()) :: boolean()
def only_administrator?(member_id, group_id) do
Member
|> where(
[m],

View file

@ -55,9 +55,10 @@ defmodule Mobilizon.Actors.Member do
@doc """
Checks whether the member is an administrator (admin or creator) of the group.
"""
def is_administrator(%__MODULE__{role: :administrator}), do: true
def is_administrator(%__MODULE__{role: :creator}), do: true
def is_administrator(%__MODULE__{}), do: false
@spec administrator?(t()) :: boolean()
def administrator?(%__MODULE__{role: :administrator}), do: true
def administrator?(%__MODULE__{role: :creator}), do: true
def administrator?(%__MODULE__{}), do: false
@doc false
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()

View file

@ -77,7 +77,7 @@ defmodule Mobilizon.Discussions do
|> join(:left, [c], r in Comment, on: r.origin_comment_id == c.id)
|> where([c, _], is_nil(c.in_reply_to_comment_id))
|> where([c], c.visibility in ^@public_visibility)
# TODO: This was added because we don't want to count deleted comments in total_replies.
# This was added because we don't want to count deleted comments in total_replies.
# However, it also excludes all top-level comments with deleted replies from being selected
# |> where([_, r], is_nil(r.deleted_at))
|> group_by([c], c.id)

View file

@ -515,8 +515,8 @@ defmodule Mobilizon.Events do
|> Page.build_page(page, limit)
end
@spec is_user_moderator_for_event?(integer | String.t(), integer | String.t()) :: boolean
def is_user_moderator_for_event?(user_id, event_id) do
@spec user_moderator_for_event?(integer | String.t(), integer | String.t()) :: boolean
def user_moderator_for_event?(user_id, event_id) do
Participant
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id)
|> where([p, _a], p.event_id == ^event_id)
@ -1492,14 +1492,14 @@ defmodule Mobilizon.Events do
end
@spec filter_online(Ecto.Query.t(), map()) :: Ecto.Query.t()
defp filter_online(query, %{type: :online}), do: is_online_fragment(query, true)
defp filter_online(query, %{type: :online}), do: online_fragment_check(query, true)
defp filter_online(query, %{type: :in_person}), do: is_online_fragment(query, false)
defp filter_online(query, %{type: :in_person}), do: online_fragment_check(query, false)
defp filter_online(query, _), do: query
@spec is_online_fragment(Ecto.Query.t(), boolean()) :: Ecto.Query.t()
defp is_online_fragment(query, value) do
@spec online_fragment_check(Ecto.Query.t(), boolean()) :: Ecto.Query.t()
defp online_fragment_check(query, value) do
where(query, [q], fragment("(?->>'is_online')::bool = ?", q.options, ^value))
end

View file

@ -49,8 +49,8 @@ defmodule Mobilizon.Events.Participant do
We start by fetching the list of organizers and if there's only one of them
and that it's the actor requesting leaving the event we return true.
"""
@spec is_not_only_organizer(integer | String.t(), integer | String.t()) :: boolean
def is_not_only_organizer(event_id, actor_id) do
@spec not_only_organizer?(integer | String.t(), integer | String.t()) :: boolean
def not_only_organizer?(event_id, actor_id) do
case Events.list_organizers_participants_for_event(event_id) do
[%__MODULE__{actor: %Actor{id: participant_actor_id}}] ->
participant_actor_id == actor_id

View file

@ -34,6 +34,10 @@ defmodule Mobilizon.Instances.InstanceActor do
instance_actor
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
|> validate_length(:domain, max: 254)
|> validate_length(:instance_name, max: 254)
|> validate_length(:software, max: 254)
|> validate_length(:software_version, max: 254)
|> unique_constraint(:domain)
end
end

View file

@ -4,6 +4,7 @@ defmodule Mobilizon.Instances do
"""
alias Ecto.Adapters.SQL
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Instances.{Instance, InstanceActor}
alias Mobilizon.Storage.{Page, Repo}
import Ecto.Query
@ -22,11 +23,15 @@ defmodule Mobilizon.Instances do
order_by_options = Keyword.new([{direction, order_by}])
%Actor{id: relay_id} = Relay.get_actor()
query =
Instance
|> join(:left, [i], ia in InstanceActor, on: i.domain == ia.domain)
|> join(:left, [_i, ia], a in Actor, on: ia.actor_id == a.id)
# following
|> join(:left, [_i, _ia, a], f1 in Follower, on: f1.target_actor_id == a.id)
# followed
|> join(:left, [_i, _ia, a], f2 in Follower, on: f2.actor_id == a.id)
|> select([i, ia, a, f1, f2], %{
instance: i,
@ -45,14 +50,27 @@ defmodule Mobilizon.Instances do
if is_nil(filter_domain) or filter_domain == "" do
query
else
where(query, [i], like(i.domain, ^"%#{filter_domain}%"))
where(
query,
[i, ia],
like(i.domain, ^"%#{filter_domain}%") or like(ia.instance_name, ^"%#{filter_domain}%")
)
end
query =
case follow_status do
:following -> where(query, [i, s], s.following == true)
:followed -> where(query, [i, s], s.follower == true)
:all -> query
:following ->
where(query, [_i, _ia, _a, f1], f1.actor_id == ^relay_id and f1.approved == true)
:followed ->
where(
query,
[_i, _ia, _a, _f1, f2],
f2.target_actor_id == ^relay_id and f2.approved == true
)
:all ->
query
end
%Page{elements: elements} = paged_instances = Page.build_page(query, page, limit, :domain)

View file

@ -214,7 +214,7 @@ defmodule Mobilizon.Medias do
query
|> Repo.all(timeout: :infinity)
|> Enum.filter(fn %Media{file: %File{url: url}} ->
!url_is_also_a_profile_file?(url) && is_all_media_orphan?(url, expiration_date)
!url_is_also_a_profile_file?(url) && all_media_orphan?(url, expiration_date)
end)
|> Enum.chunk_by(fn %Media{file: %File{url: url}} ->
url
@ -223,14 +223,14 @@ defmodule Mobilizon.Medias do
end)
end
defp is_all_media_orphan?(url, expiration_date) do
defp all_media_orphan?(url, expiration_date) do
url
|> get_all_media_by_url()
|> Enum.all?(&is_media_orphan?(&1, expiration_date))
|> Enum.all?(&media_orphan?(&1, expiration_date))
end
@spec is_media_orphan?(Media.t(), DateTime.t()) :: boolean()
defp is_media_orphan?(%Media{id: media_id}, expiration_date) do
@spec media_orphan?(Media.t(), DateTime.t()) :: boolean()
defp media_orphan?(%Media{id: media_id}, expiration_date) do
media_query =
from(m in Media,
as: :media,

View file

@ -7,6 +7,7 @@ defmodule Mobilizon.Storage.Ecto do
import Ecto.Changeset, only: [fetch_change: 2, put_change: 3, get_field: 2]
alias Ecto.{Changeset, Query}
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Gettext, as: GettextBackend
alias Mobilizon.Web.Router.Helpers, as: Routes
@doc """
@ -56,4 +57,30 @@ defmodule Mobilizon.Storage.Ecto do
changeset
end
end
def convert_ecto_errors(%Ecto.Changeset{} = changeset),
do: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
# Translates an error message using gettext.
defp translate_error({msg, opts}) do
# Because error messages were defined within Ecto, we must
# call the Gettext module passing our Gettext backend. We
# also use the "errors" domain as translations are placed
# in the errors.po file.
# Ecto will pass the :count keyword if the error message is
# meant to be pluralized.
# On your own code and templates, depending on whether you
# need the message to be pluralized or not, this could be
# written simply as:
#
# dngettext "errors", "1 file", "%{count} files", count
# dgettext "errors", "is invalid"
#
if count = opts[:count] do
Gettext.dngettext(GettextBackend, "errors", msg, msg, count, opts)
else
Gettext.dgettext(GettextBackend, "errors", msg, opts)
end
end
end

View file

@ -55,14 +55,14 @@ defmodule Mobilizon.Service.DateTime do
)
end
@spec is_first_day_of_week(Date.t(), String.t()) :: boolean()
defp is_first_day_of_week(%Date{} = date, locale) do
@spec first_day_of_week?(Date.t(), String.t()) :: boolean()
defp first_day_of_week?(%Date{} = date, locale) do
Date.day_of_week(date) == Cldr.Calendar.first_day_for_locale(locale)
end
@spec calculate_first_day_of_week(Date.t(), String.t()) :: Date.t()
def calculate_first_day_of_week(%Date{} = date, locale \\ "en") do
if is_first_day_of_week(date, locale),
if first_day_of_week?(date, locale),
do: date,
else: calculate_first_day_of_week(Date.add(date, -1), locale)
end
@ -204,11 +204,11 @@ defmodule Mobilizon.Service.DateTime do
compare_to_day = Keyword.get(options, :compare_to_day, Date.utc_today())
locale = Keyword.get(options, :locale, "en")
is_first_day_of_week(compare_to_day, locale) && is_between_hours?(options)
first_day_of_week?(compare_to_day, locale) && is_between_hours?(options)
end
@spec is_delay_ok_since_last_notification_sent?(DateTime.t(), pos_integer()) :: boolean()
def is_delay_ok_since_last_notification_sent?(
@spec delay_ok_since_last_notification_sent?(DateTime.t(), pos_integer()) :: boolean()
def delay_ok_since_last_notification_sent?(
%DateTime{} = last_notification_sent,
delay \\ 3_600
) do
@ -216,8 +216,8 @@ defmodule Mobilizon.Service.DateTime do
:lt
end
@spec is_same_day?(DateTime.t(), DateTime.t()) :: boolean()
def is_same_day?(%DateTime{} = one, %DateTime{} = two) do
@spec same_day?(DateTime.t(), DateTime.t()) :: boolean()
def same_day?(%DateTime{} = one, %DateTime{} = two) do
DateTime.to_date(one) == DateTime.to_date(two)
end

View file

@ -16,7 +16,7 @@ defmodule Mobilizon.Service.Export.Common do
def fetch_actor_event_feed(name, limit) do
case Actors.get_actor_by_name(name) do
%Actor{} = actor ->
if Actor.is_public_visibility?(actor) do
if Actor.public_visibility?(actor) do
%Page{elements: events} = Events.list_public_upcoming_events_for_actor(actor, 1, limit)
%Page{elements: posts} = Posts.get_public_posts_for_group(actor, 1, limit)
{:ok, actor, events, posts}

View file

@ -265,7 +265,7 @@ defmodule Mobilizon.Service.Export.Feed do
end
defp clear_actor_feed(%Actor{preferred_username: preferred_username} = actor) do
if Actor.is_public_visibility?(actor) do
if Actor.public_visibility?(actor) do
Cachex.del(:feed, "actor_#{preferred_username}")
end
end

View file

@ -216,7 +216,7 @@ defmodule Mobilizon.Service.Export.ICalendar do
end
defp clear_actor_feed(%Actor{preferred_username: preferred_username} = actor) do
if Actor.is_public_visibility?(actor) do
if Actor.public_visibility?(actor) do
Cachex.del(:ics, "actor_#{preferred_username}")
end
end

View file

@ -13,8 +13,8 @@ defmodule Mobilizon.Service.HTTP.Utils do
end
end
@spec is_content_type?(Enum.t(), String.t() | list(String.t())) :: boolean
def is_content_type?(headers, content_type) do
@spec content_type_matches?(Enum.t(), String.t() | list(String.t())) :: boolean
def content_type_matches?(headers, content_type) do
headers
|> get_header("Content-Type")
|> content_type_header_matches(content_type)

View file

@ -12,8 +12,8 @@ defmodule Mobilizon.Service.Notifier.Email do
import Mobilizon.Service.DateTime,
only: [
is_delay_ok_since_last_notification_sent?: 1,
is_delay_ok_since_last_notification_sent?: 2
delay_ok_since_last_notification_sent?: 1,
delay_ok_since_last_notification_sent?: 2
]
require Logger
@ -129,7 +129,7 @@ defmodule Mobilizon.Service.Notifier.Email do
# Delay ok since last notification
defp match_group_notifications_setting(:one_hour, _, %DateTime{} = last_notification_sent, _) do
is_delay_ok_since_last_notification_sent?(last_notification_sent)
delay_ok_since_last_notification_sent?(last_notification_sent)
end
# Delay ok since last notification
@ -139,7 +139,7 @@ defmodule Mobilizon.Service.Notifier.Email do
%DateTime{} = last_notification_sent,
options
) do
is_delay_ok_since_last_notification_sent?(last_notification_sent, 3_600 * 23) and
delay_ok_since_last_notification_sent?(last_notification_sent, 3_600 * 23) and
Keyword.get(options, :recap, false) != false
end
@ -149,7 +149,7 @@ defmodule Mobilizon.Service.Notifier.Email do
%DateTime{} = last_notification_sent,
options
) do
is_delay_ok_since_last_notification_sent?(last_notification_sent, 3_600 * 24 * 6) and
delay_ok_since_last_notification_sent?(last_notification_sent, 3_600 * 24 * 6) and
Keyword.get(options, :recap, false) != false
end

View file

@ -11,9 +11,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
max_body: 2_000_000,
timeout: 10_000,
recv_timeout: 20_000,
follow_redirect: true,
# TODO: Remove me once Hackney/HTTPoison fixes their issue with TLS1.3 and OTP 23
ssl: [{:versions, [:"tlsv1.2"]}]
follow_redirect: true
]
alias Mobilizon.Config
@ -75,7 +73,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
opts: @options
)},
{:is_html, _response_headers, true} <-
{:is_html, response_headers, is_html?(response_headers)} do
{:is_html, response_headers, html?(response_headers)} do
body
|> convert_utf8(response_headers)
|> maybe_parse()
@ -108,21 +106,21 @@ defmodule Mobilizon.Service.RichMedia.Parser do
defp get_data_for_media(response_headers, url) do
data = %{title: get_filename_from_headers(response_headers) || get_filename_from_url(url)}
if is_image?(response_headers) do
if image?(response_headers) do
Map.put(data, :image_remote_url, url)
else
data
end
end
@spec is_html?(Enum.t()) :: boolean
defp is_html?(headers) do
is_content_type?(headers, ["text/html", "application/xhtml"])
@spec html?(Enum.t()) :: boolean
defp html?(headers) do
content_type_matches?(headers, ["text/html", "application/xhtml"])
end
@spec is_image?(Enum.t()) :: boolean
defp is_image?(headers) do
is_content_type?(headers, ["image/"])
@spec image?(Enum.t()) :: boolean
defp image?(headers) do
content_type_matches?(headers, ["image/"])
end
@spec get_filename_from_headers(Enum.t()) :: String.t() | nil

View file

@ -81,7 +81,7 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
options
) do
mentionned_actor_ids
|> Enum.filter(&Actors.is_member?(&1, Keyword.fetch!(options, :group_id)))
|> Enum.filter(&Actors.member?(&1, Keyword.fetch!(options, :group_id)))
|> users_from_actor_ids(Keyword.fetch!(options, :author_id))
end

View file

@ -13,6 +13,7 @@ defmodule Mobilizon.Service.Workers.RefreshInstances do
alias Mobilizon.Instances.{Instance, InstanceActor}
alias Oban.Job
require Logger
import Mobilizon.Storage.Ecto, only: [convert_ecto_errors: 1]
@impl Oban.Worker
@spec perform(Oban.Job.t()) :: :ok
@ -56,6 +57,10 @@ defmodule Mobilizon.Service.Workers.RefreshInstances do
Instances.create_instance_actor(args) do
Logger.info("Saved instance actor details for domain #{host}")
else
{:error, %Ecto.Changeset{} = changeset} ->
Logger.error("Unable to save instance \"#{domain}\" metadata")
Logger.debug(convert_ecto_errors(changeset))
err ->
Logger.error(inspect(err))
end

View file

@ -16,7 +16,7 @@ defmodule Mobilizon.Service.Workers.SendActivityRecapWorker do
only: [
is_between_hours?: 1,
is_between_hours_on_first_day?: 1,
is_delay_ok_since_last_notification_sent?: 1
delay_ok_since_last_notification_sent?: 1
]
@impl Oban.Worker
@ -108,7 +108,7 @@ defmodule Mobilizon.Service.Workers.SendActivityRecapWorker do
"Testing if it's less than an hour since the last time we sent an activity recap"
)
is_delay_ok_since_last_notification_sent?(last_notification_sent)
delay_ok_since_last_notification_sent?(last_notification_sent)
end
# If we're between notification hours

View file

@ -173,26 +173,26 @@ defmodule Mobilizon.Web.PageController do
end
end
@spec is_visible?(map) :: boolean()
defp is_visible?(%{visibility: v}), do: v in [:public, :unlisted]
defp is_visible?(%Tombstone{}), do: true
defp is_visible?(_), do: true
@spec visible?(map) :: boolean()
defp visible?(%{visibility: v}), do: v in [:public, :unlisted]
defp visible?(%Tombstone{}), do: true
defp visible?(_), do: true
@spec ok_status?(cache_status) :: boolean()
defp ok_status?(status), do: status in [:ok, :commit]
@typep cache_status :: :ok | :commit | :ignore
@spec ok_status_and_is_visible?(Plug.Conn.t(), cache_status, map()) :: boolean()
defp ok_status_and_is_visible?(_conn, status, o),
do: ok_status?(status) and is_visible?(o)
@spec ok_status_and_visible?(Plug.Conn.t(), cache_status, map()) :: boolean()
defp ok_status_and_visible?(_conn, status, o),
do: ok_status?(status) and visible?(o)
defp checks?(conn, status, o) do
cond do
ok_status_and_is_visible?(conn, status, o) ->
if is_local?(o) == :remote && get_format(conn) == "activity-json", do: :remote, else: true
ok_status_and_visible?(conn, status, o) ->
if local?(o) == :remote && get_format(conn) == "activity-json", do: :remote, else: true
is_person?(o) && get_format(conn) == "activity-json" ->
person?(o) && get_format(conn) == "activity-json" ->
true
true ->
@ -200,9 +200,9 @@ defmodule Mobilizon.Web.PageController do
end
end
@spec is_local?(map()) :: boolean | :remote
defp is_local?(%{local: local}), do: if(local, do: true, else: :remote)
defp is_local?(_), do: false
@spec local?(map()) :: boolean | :remote
defp local?(%{local: local}), do: if(local, do: true, else: :remote)
defp local?(_), do: false
@spec maybe_add_noindex_header(Plug.Conn.t(), map()) :: Plug.Conn.t()
defp maybe_add_noindex_header(conn, %{visibility: visibility})
@ -212,9 +212,9 @@ defmodule Mobilizon.Web.PageController do
defp maybe_add_noindex_header(conn, _), do: conn
@spec is_person?(Actor.t()) :: boolean()
defp is_person?(%Actor{type: :Person}), do: true
defp is_person?(_), do: false
@spec person?(Actor.t()) :: boolean()
defp person?(%Actor{type: :Person}), do: true
defp person?(_), do: false
defp maybe_add_content_type_header(conn) do
case get_format(conn) do

View file

@ -87,8 +87,6 @@ defmodule Mobilizon.Web.Email.Group do
end
end
# TODO : def send_confirmation_to_inviter()
@member_roles [:administrator, :moderator, :member]
@spec send_group_suspension_notification(Member.t()) :: :ok
def send_group_suspension_notification(%Member{actor: %Actor{user_id: nil}}), do: :ok

View file

@ -124,6 +124,4 @@ defmodule Mobilizon.Web.Email.Member do
:ok
end
end
# TODO : def send_confirmation_to_inviter()
end

View file

@ -71,13 +71,7 @@ defmodule Mobilizon.Web.MediaProxy do
@compile {:no_warn_undefined, {:crypto, :mac, 4}}
@compile {:no_warn_undefined, {:crypto, :hmac, 3}}
defp sha_hmac(key, url) do
# :crypto.hmac was removed in OTP24, but :crypto.mac was added in OTP 22.1
# TODO: Remove me when we don't support OTP 21/22 anymore
if function_exported?(:crypto, :mac, 4) do
:crypto.mac(:hmac, :sha, key, url)
else
:crypto.hmac(:sha, key, url)
end
end
@spec filename(String.t()) :: String.t() | nil

View file

@ -6,7 +6,7 @@
timezone: @timezone,
locale: @locale
) %>
<% is_same_day?(@start_date, @end_date) -> %>
<% same_day?(@start_date, @end_date) -> %>
<strong>
<%= gettext("On %{date} from %{start_time} to %{end_time}",
date: datetime_to_date_string(@start_date, @locale),

View file

@ -1 +1 @@
<%= cond do %><% @end_date == nil -> %><%= render("date/event_tz_date.text", date: @start_date, event: @event, timezone: @timezone, locale: @locale) %><% is_same_day?(@start_date, @end_date) -> %><%= gettext "On %{date} from %{start_time} to %{end_time}", date: datetime_to_date_string(@start_date, @locale), start_time: datetime_to_time_string(@start_date, @locale), end_time: datetime_to_time_string(@end_date, @locale) %><%= if @event.options.timezone != @timezone do %> <%= gettext "🌐 %{timezone} %{offset}", timezone: @event.options.timezone, offset: Cldr.DateTime.Formatter.zone_gmt(@start_date) %><% end %><% true -> %><%= gettext "From the %{start} to the %{end}", start: datetime_to_string(@start_date, @locale, :short), end: datetime_to_string(@end_date, @locale, :short) %><%= if @event.options.timezone != @timezone do %> <%= gettext "🌐 %{timezone} %{offset}", timezone: @event.options.timezone, offset: Cldr.DateTime.Formatter.zone_gmt(@start_date) %><% end %><% end %>
<%= cond do %><% @end_date == nil -> %><%= render("date/event_tz_date.text", date: @start_date, event: @event, timezone: @timezone, locale: @locale) %><% same_day?(@start_date, @end_date) -> %><%= gettext "On %{date} from %{start_time} to %{end_time}", date: datetime_to_date_string(@start_date, @locale), start_time: datetime_to_time_string(@start_date, @locale), end_time: datetime_to_time_string(@end_date, @locale) %><%= if @event.options.timezone != @timezone do %> <%= gettext "🌐 %{timezone} %{offset}", timezone: @event.options.timezone, offset: Cldr.DateTime.Formatter.zone_gmt(@start_date) %><% end %><% true -> %><%= gettext "From the %{start} to the %{end}", start: datetime_to_string(@start_date, @locale, :short), end: datetime_to_string(@end_date, @locale, :short) %><%= if @event.options.timezone != @timezone do %> <%= gettext "🌐 %{timezone} %{offset}", timezone: @event.options.timezone, offset: Cldr.DateTime.Formatter.zone_gmt(@start_date) %><% end %><% end %>

View file

@ -15,7 +15,7 @@
document.documentElement.classList.remove('dark')
}
</script>
<%= if is_root(assigns) do %>
<%= if root?(assigns) do %>
<link rel="preload" href="/img/shape-1.svg" as="image" />
<link rel="preload" href="/img/shape-2.svg" as="image" />
<link rel="preload" href="/img/shape-3.svg" as="image" />

View file

@ -25,7 +25,7 @@ defmodule Mobilizon.Web.EmailView do
defdelegate datetime_tz_convert(datetime, timezone), to: DateTimeRenderer
defdelegate datetime_relative(datetime, locale \\ "en"), to: DateTimeRenderer
defdelegate render_address(address), to: Address
defdelegate is_same_day?(one, two), to: DateTimeRenderer
defdelegate same_day?(one, two), to: DateTimeRenderer
defdelegate display_name(actor), to: Actor
defdelegate preferred_username_and_domain(actor), to: Actor

View file

@ -87,8 +87,8 @@ defmodule Mobilizon.Web.PageView do
assigns |> Map.get(:locale, "en") |> get_language_direction()
end
@spec is_root(map()) :: boolean()
def is_root(assigns) do
@spec root?(map()) :: boolean()
def root?(assigns) do
assigns |> Map.get(:conn, %{request_path: "/"}) |> Map.get(:request_path, "/") == "/"
end
end

View file

@ -22,6 +22,10 @@
"**/*.{js,ts,vue}": [
"eslint --fix",
"prettier --write"
],
"**/*.{ex,exs,eex,heex}": [
"mix format",
"mix credo"
]
},
"dependencies": {

View file

@ -0,0 +1,15 @@
defmodule Mobilizon.Storage.Repo.Migrations.ChangeActorInstanceDescriptionTypeToText do
use Ecto.Migration
def up do
alter table(:instance_actors) do
modify(:instance_description, :text)
end
end
def down do
alter table(:instance_actors) do
modify(:instance_description, :string)
end
end
end

View file

@ -1641,5 +1641,6 @@
"Announcements for {eventTitle}": "Announcements for {eventTitle}",
"Visit {instance_domain}": "Visit {instance_domain}",
"Software details: {software_details}": "Software details: {software_details}",
"Only instances with an application actor can be followed": "Only instances with an application actor can be followed"
"Only instances with an application actor can be followed": "Only instances with an application actor can be followed",
"Domain or instance name": "Domain or instance name"
}

View file

@ -1635,5 +1635,6 @@
"Announcements for {eventTitle}": "Annonces pour {eventTitle}",
"Visit {instance_domain}": "Visiter {instance_domain}",
"Software details: {software_details}": "Détails du logiciel : {software_details}",
"Only instances with an application actor can be followed": "Seules les instances avec un acteur application peuvent être suivies"
"Only instances with an application actor can be followed": "Seules les instances avec un acteur application peuvent être suivies",
"Domain or instance name": "Domaine ou nom de l'instance"
}

View file

@ -54,15 +54,14 @@
>
</o-field>
<o-field
:label="t('Domain')"
:label="t('Domain or instance name')"
label-for="domain-filter"
class="flex-auto"
>
<o-input
id="domain-filter"
:placeholder="t('mobilizon-instance.tld')"
:value="filterDomain"
@input="debouncedUpdateDomainFilter"
v-model="filterDomain"
/>
</o-field>
</div>
@ -223,7 +222,6 @@ import { Paginate } from "@/types/paginate";
import RouteName from "../../router/name";
import { IInstance } from "@/types/instance.model";
import EmptyContent from "@/components/Utils/EmptyContent.vue";
import debounce from "lodash/debounce";
import {
InstanceFilterFollowStatus,
InstanceFollowStatus,
@ -254,12 +252,16 @@ const followStatus = useRouteQuery(
const { result: instancesResult } = useQuery<{
instances: Paginate<IInstance>;
}>(INSTANCES, () => ({
}>(
INSTANCES,
() => ({
page: instancePage.value,
limit: INSTANCES_PAGE_LIMIT,
filterDomain: filterDomain.value,
filterFollowStatus: followStatus.value,
}));
}),
{ debounce: 500 }
);
const instances = computed(() => instancesResult.value?.instances);
@ -276,13 +278,6 @@ const newRelayAddress = ref("");
// relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
const updateDomainFilter = (event: InputEvent) => {
const newValue = (event.target as HTMLInputElement).value;
filterDomain.value = newValue;
};
const debouncedUpdateDomainFilter = debounce(updateDomainFilter, 500);
const hasFilter = computed((): boolean => {
return (
followStatus.value !== InstanceFilterFollowStatus.ALL ||

View file

@ -179,7 +179,7 @@ defmodule Mix.Tasks.Mobilizon.Actors.NewTest do
assert %Actor{name: @group_name, preferred_username: @group_username, id: group_id} =
Actors.get_group_by_title(@group_username)
assert Actors.is_administrator?(admin_id, group_id)
assert Actors.administrator?(admin_id, group_id)
end
end
end