Refactoring of Actors context
This commit is contained in:
@@ -1,44 +1,17 @@
|
||||
import EctoEnum
|
||||
|
||||
defenum(Mobilizon.Actors.ActorTypeEnum, :actor_type, [
|
||||
:Person,
|
||||
:Application,
|
||||
:Group,
|
||||
:Organization,
|
||||
:Service
|
||||
])
|
||||
|
||||
defenum(Mobilizon.Actors.ActorOpennessEnum, :actor_openness, [
|
||||
:invite_only,
|
||||
:moderated,
|
||||
:open
|
||||
])
|
||||
|
||||
defenum(Mobilizon.Actors.ActorVisibilityEnum, :actor_visibility_type, [
|
||||
:public,
|
||||
:unlisted,
|
||||
# Probably unused
|
||||
:restricted,
|
||||
:private
|
||||
])
|
||||
|
||||
defmodule Mobilizon.Actors.Actor do
|
||||
@moduledoc """
|
||||
Represents an actor (local and remote actors)
|
||||
Represents an actor (local and remote).
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.{Actors, Config, Crypto}
|
||||
alias Mobilizon.Actors.{Actor, ActorOpenness, ActorType, ActorVisibility, Follower, Member}
|
||||
alias Mobilizon.Events.{Event, FeedToken}
|
||||
alias Mobilizon.Media.File
|
||||
alias Mobilizon.Reports.{Report, Note}
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias MobilizonWeb.Router.Helpers, as: Routes
|
||||
@@ -46,7 +19,97 @@ defmodule Mobilizon.Actors.Actor do
|
||||
|
||||
require Logger
|
||||
|
||||
# @type t :: %Actor{description: String.t, id: integer(), inserted_at: DateTime.t, updated_at: DateTime.t, display_name: String.t, domain: String.t, keys: String.t, suspended: boolean(), url: String.t, username: String.t, organized_events: list(), groups: list(), group_request: list(), user: User.t, field: ActorTypeEnum.t}
|
||||
@type t :: %__MODULE__{
|
||||
url: String.t(),
|
||||
outbox_url: String.t(),
|
||||
inbox_url: String.t(),
|
||||
following_url: String.t(),
|
||||
followers_url: String.t(),
|
||||
shared_inbox_url: String.t(),
|
||||
type: ActorType.t(),
|
||||
name: String.t(),
|
||||
domain: String.t(),
|
||||
summary: String.t(),
|
||||
preferred_username: String.t(),
|
||||
keys: String.t(),
|
||||
manually_approves_followers: boolean,
|
||||
openness: ActorOpenness.t(),
|
||||
visibility: ActorVisibility.t(),
|
||||
suspended: boolean,
|
||||
avatar: File.t(),
|
||||
banner: File.t(),
|
||||
user: User.t(),
|
||||
followers: [Follower.t()],
|
||||
followings: [Follower.t()],
|
||||
organized_events: [Event.t()],
|
||||
feed_tokens: [FeedToken.t()],
|
||||
created_reports: [Report.t()],
|
||||
subject_reports: [Report.t()],
|
||||
report_notes: [Note.t()],
|
||||
memberships: [Actor.t()]
|
||||
}
|
||||
|
||||
@required_attrs [:preferred_username, :keys, :suspended, :url]
|
||||
@optional_attrs [
|
||||
:outbox_url,
|
||||
:inbox_url,
|
||||
:shared_inbox_url,
|
||||
:following_url,
|
||||
:followers_url,
|
||||
:type,
|
||||
:name,
|
||||
:domain,
|
||||
:summary,
|
||||
:manually_approves_followers,
|
||||
:user_id
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@update_required_attrs @required_attrs
|
||||
@update_optional_attrs [:name, :summary, :manually_approves_followers, :user_id]
|
||||
@update_attrs @update_required_attrs ++ @update_optional_attrs
|
||||
|
||||
@registration_required_attrs [:preferred_username, :keys, :suspended, :url, :type]
|
||||
@registration_optional_attrs [:domain, :name, :summary, :user_id]
|
||||
@registration_attrs @registration_required_attrs ++ @registration_optional_attrs
|
||||
|
||||
@remote_actor_creation_required_attrs [
|
||||
:url,
|
||||
:inbox_url,
|
||||
:type,
|
||||
:domain,
|
||||
:preferred_username,
|
||||
:keys
|
||||
]
|
||||
@remote_actor_creation_optional_attrs [
|
||||
:outbox_url,
|
||||
:shared_inbox_url,
|
||||
:following_url,
|
||||
:followers_url,
|
||||
:name,
|
||||
:summary,
|
||||
:manually_approves_followers
|
||||
]
|
||||
@remote_actor_creation_attrs @remote_actor_creation_required_attrs ++
|
||||
@remote_actor_creation_optional_attrs
|
||||
|
||||
@relay_creation_attrs [
|
||||
:type,
|
||||
:name,
|
||||
:summary,
|
||||
:url,
|
||||
:keys,
|
||||
:preferred_username,
|
||||
:domain,
|
||||
:inbox_url,
|
||||
:followers_url,
|
||||
:following_url,
|
||||
:shared_inbox_url
|
||||
]
|
||||
|
||||
@group_creation_required_attrs [:url, :outbox_url, :inbox_url, :type, :preferred_username]
|
||||
@group_creation_optional_attrs [:shared_inbox_url, :name, :domain, :summary]
|
||||
@group_creation_attrs @group_creation_required_attrs ++ @group_creation_optional_attrs
|
||||
|
||||
schema "actors" do
|
||||
field(:url, :string)
|
||||
@@ -55,187 +118,156 @@ defmodule Mobilizon.Actors.Actor do
|
||||
field(:following_url, :string)
|
||||
field(:followers_url, :string)
|
||||
field(:shared_inbox_url, :string)
|
||||
field(:type, Mobilizon.Actors.ActorTypeEnum, default: :Person)
|
||||
field(:type, ActorType, default: :Person)
|
||||
field(:name, :string)
|
||||
field(:domain, :string, default: nil)
|
||||
field(:summary, :string)
|
||||
field(:preferred_username, :string)
|
||||
field(:keys, :string)
|
||||
field(:manually_approves_followers, :boolean, default: false)
|
||||
field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
|
||||
field(:visibility, Mobilizon.Actors.ActorVisibilityEnum, default: :private)
|
||||
field(:openness, ActorOpenness, default: :moderated)
|
||||
field(:visibility, ActorVisibility, default: :private)
|
||||
field(:suspended, :boolean, default: false)
|
||||
# field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
|
||||
|
||||
embeds_one(:avatar, File, on_replace: :update)
|
||||
embeds_one(:banner, File, on_replace: :update)
|
||||
belongs_to(:user, User)
|
||||
has_many(:followers, Follower, foreign_key: :target_actor_id)
|
||||
has_many(:followings, Follower, foreign_key: :actor_id)
|
||||
has_many(:organized_events, Event, foreign_key: :organizer_actor_id)
|
||||
many_to_many(:memberships, Actor, join_through: Member)
|
||||
belongs_to(:user, User)
|
||||
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
|
||||
embeds_one(:avatar, File, on_replace: :update)
|
||||
embeds_one(:banner, File, on_replace: :update)
|
||||
has_many(:created_reports, Report, foreign_key: :reporter_id)
|
||||
has_many(:subject_reports, Report, foreign_key: :reported_id)
|
||||
has_many(:report_notes, Note, foreign_key: :moderator_id)
|
||||
many_to_many(:memberships, Actor, join_through: Member)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks whether actor visibility is public.
|
||||
"""
|
||||
@spec is_public_visibility(Actor.t()) :: boolean
|
||||
def is_public_visibility(%Actor{visibility: visibility}) do
|
||||
visibility in [:public, :unlisted]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the display name if available, or the preferred username
|
||||
(with the eventual @domain suffix if it's a distant actor).
|
||||
"""
|
||||
@spec display_name(Actor.t()) :: String.t()
|
||||
def display_name(%Actor{name: name} = actor) when name in [nil, ""] do
|
||||
preferred_username_and_domain(actor)
|
||||
end
|
||||
|
||||
def display_name(%Actor{name: name}), do: name
|
||||
|
||||
@doc """
|
||||
Returns display name and username.
|
||||
"""
|
||||
@spec display_name_and_username(Actor.t()) :: String.t()
|
||||
def display_name_and_username(%Actor{name: name} = actor) when name in [nil, ""] do
|
||||
preferred_username_and_domain(actor)
|
||||
end
|
||||
|
||||
def display_name_and_username(%Actor{name: name} = actor) do
|
||||
"#{name} (#{preferred_username_and_domain(actor)})"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the preferred username with the eventual @domain suffix if it's
|
||||
a distant actor.
|
||||
"""
|
||||
@spec preferred_username_and_domain(Actor.t()) :: String.t()
|
||||
def preferred_username_and_domain(%Actor{preferred_username: preferred_username, domain: nil}) do
|
||||
preferred_username
|
||||
end
|
||||
|
||||
def preferred_username_and_domain(%Actor{preferred_username: preferred_username, domain: domain}) do
|
||||
"#{preferred_username}@#{domain}"
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
|
||||
def changeset(%Actor{} = actor, attrs) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(attrs, [
|
||||
:url,
|
||||
:outbox_url,
|
||||
:inbox_url,
|
||||
:shared_inbox_url,
|
||||
:following_url,
|
||||
:followers_url,
|
||||
:type,
|
||||
:name,
|
||||
:domain,
|
||||
:summary,
|
||||
:preferred_username,
|
||||
:keys,
|
||||
:manually_approves_followers,
|
||||
:suspended,
|
||||
:user_id
|
||||
])
|
||||
|> cast(attrs, @attrs)
|
||||
|> build_urls()
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> unique_username_validator()
|
||||
|> validate_required([:preferred_username, :keys, :suspended, :url])
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec update_changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
|
||||
def update_changeset(%Actor{} = actor, attrs) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(attrs, [
|
||||
:name,
|
||||
:summary,
|
||||
:keys,
|
||||
:manually_approves_followers,
|
||||
:suspended,
|
||||
:user_id
|
||||
])
|
||||
|> cast(attrs, @update_attrs)
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> validate_required([:preferred_username, :keys, :suspended, :url])
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
|> validate_required(@update_required_attrs)
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for person registration
|
||||
Changeset for person registration.
|
||||
"""
|
||||
@spec registration_changeset(struct(), map()) :: Ecto.Changeset.t()
|
||||
@spec registration_changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
|
||||
def registration_changeset(%Actor{} = actor, attrs) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(attrs, [
|
||||
:preferred_username,
|
||||
:domain,
|
||||
:name,
|
||||
:summary,
|
||||
:keys,
|
||||
:suspended,
|
||||
:url,
|
||||
:type,
|
||||
:user_id
|
||||
])
|
||||
|> cast(attrs, @registration_attrs)
|
||||
|> build_urls()
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
# Needed because following constraint can't work for domain null values (local)
|
||||
|> unique_username_validator()
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
|> validate_required([:preferred_username, :keys, :suspended, :url, :type])
|
||||
|> validate_required(@registration_required_attrs)
|
||||
end
|
||||
|
||||
# TODO : Use me !
|
||||
# @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
|
||||
@doc """
|
||||
Changeset for remote actor creation
|
||||
Changeset for remote actor creation.
|
||||
"""
|
||||
@spec remote_actor_creation(map()) :: Ecto.Changeset.t()
|
||||
def remote_actor_creation(params) do
|
||||
changes =
|
||||
@spec remote_actor_creation_changeset(map) :: Ecto.Changeset.t()
|
||||
def remote_actor_creation_changeset(attrs) do
|
||||
changeset =
|
||||
%Actor{}
|
||||
|> Ecto.Changeset.cast(params, [
|
||||
:url,
|
||||
:outbox_url,
|
||||
:inbox_url,
|
||||
:shared_inbox_url,
|
||||
:following_url,
|
||||
:followers_url,
|
||||
:type,
|
||||
:name,
|
||||
:domain,
|
||||
:summary,
|
||||
:preferred_username,
|
||||
:keys,
|
||||
:manually_approves_followers
|
||||
])
|
||||
|> validate_required([
|
||||
:url,
|
||||
:inbox_url,
|
||||
:type,
|
||||
:domain,
|
||||
:preferred_username,
|
||||
:keys
|
||||
])
|
||||
|> cast(attrs, @remote_actor_creation_attrs)
|
||||
|> validate_required(@remote_actor_creation_required_attrs)
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
# Needed because following constraint can't work for domain null values (local)
|
||||
|> unique_username_validator()
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
|> validate_length(:summary, max: 5000)
|
||||
|> validate_length(:preferred_username, max: 100)
|
||||
|
||||
Logger.debug("Remote actor creation")
|
||||
Logger.debug(inspect(changes))
|
||||
changes
|
||||
Logger.debug("Remote actor creation: #{inspect(changeset)}")
|
||||
|
||||
changeset
|
||||
end
|
||||
|
||||
def relay_creation(%{url: url, preferred_username: preferred_username} = _params) do
|
||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||
pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing()
|
||||
@doc """
|
||||
Changeset for relay creation.
|
||||
"""
|
||||
@spec relay_creation_changeset(map) :: Ecto.Changeset.t()
|
||||
def relay_creation_changeset(attrs) do
|
||||
relay_creation_attrs = build_relay_creation_attrs(attrs)
|
||||
|
||||
vars = %{
|
||||
"name" => Config.get([:instance, :name], "Mobilizon"),
|
||||
"summary" => Config.get(
|
||||
[:instance, :description],
|
||||
"An internal service actor for this Mobilizon instance"
|
||||
),
|
||||
"url" => url,
|
||||
"keys" => pem,
|
||||
"preferred_username" => preferred_username,
|
||||
"domain" => nil,
|
||||
"inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
|
||||
"followers_url" => "#{url}/followers",
|
||||
"following_url" => "#{url}/following",
|
||||
"shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
|
||||
"type" => :Application
|
||||
}
|
||||
|
||||
cast(%Actor{}, vars, [
|
||||
:type,
|
||||
:name,
|
||||
:summary,
|
||||
:url,
|
||||
:keys,
|
||||
:preferred_username,
|
||||
:domain,
|
||||
:inbox_url,
|
||||
:followers_url,
|
||||
:following_url,
|
||||
:shared_inbox_url
|
||||
])
|
||||
cast(%Actor{}, relay_creation_attrs, @relay_creation_attrs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -244,68 +276,48 @@ defmodule Mobilizon.Actors.Actor do
|
||||
@spec group_creation(struct(), map()) :: Ecto.Changeset.t()
|
||||
def group_creation(%Actor{} = actor, params) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(params, [
|
||||
:url,
|
||||
:outbox_url,
|
||||
:inbox_url,
|
||||
:shared_inbox_url,
|
||||
:type,
|
||||
:name,
|
||||
:domain,
|
||||
:summary,
|
||||
:preferred_username
|
||||
])
|
||||
|> cast(params, @group_creation_attrs)
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> build_urls(:Group)
|
||||
|> put_change(:domain, nil)
|
||||
|> put_change(:keys, Actors.create_keys())
|
||||
|> put_change(:keys, Crypto.generate_rsa_2048_private_key())
|
||||
|> put_change(:type, :Group)
|
||||
|> unique_username_validator()
|
||||
|> validate_required([:url, :outbox_url, :inbox_url, :type, :preferred_username])
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
|> validate_required(@group_creation_required_attrs)
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
|> validate_length(:summary, max: 5000)
|
||||
|> validate_length(:preferred_username, max: 100)
|
||||
end
|
||||
|
||||
# Needed because following constraint can't work for domain null values (local)
|
||||
@spec unique_username_validator(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp unique_username_validator(
|
||||
%Ecto.Changeset{changes: %{preferred_username: username} = changes} = changeset
|
||||
) do
|
||||
with nil <- Map.get(changes, :domain, nil),
|
||||
%Actor{preferred_username: _username} <- Actors.get_local_actor_by_name(username) do
|
||||
changeset |> add_error(:preferred_username, "Username is already taken")
|
||||
%Actor{preferred_username: _} <- Actors.get_local_actor_by_name(username) do
|
||||
add_error(changeset, :preferred_username, "Username is already taken")
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
# When we don't even have any preferred_username, don't even try validating preferred_username
|
||||
defp unique_username_validator(changeset) do
|
||||
changeset
|
||||
end
|
||||
defp unique_username_validator(changeset), do: changeset
|
||||
|
||||
@spec build_urls(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
|
||||
@spec build_urls(Ecto.Changeset.t(), ActorType.t()) :: Ecto.Changeset.t()
|
||||
defp build_urls(changeset, type \\ :Person)
|
||||
|
||||
defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, _type) do
|
||||
changeset
|
||||
|> put_change(
|
||||
:outbox_url,
|
||||
build_url(username, :outbox)
|
||||
)
|
||||
|> put_change(
|
||||
:followers_url,
|
||||
build_url(username, :followers)
|
||||
)
|
||||
|> put_change(
|
||||
:following_url,
|
||||
build_url(username, :following)
|
||||
)
|
||||
|> put_change(
|
||||
:inbox_url,
|
||||
build_url(username, :inbox)
|
||||
)
|
||||
|> put_change(:outbox_url, build_url(username, :outbox))
|
||||
|> put_change(:followers_url, build_url(username, :followers))
|
||||
|> put_change(:following_url, build_url(username, :following))
|
||||
|> put_change(:inbox_url, build_url(username, :inbox))
|
||||
|> put_change(:shared_inbox_url, "#{MobilizonWeb.Endpoint.url()}/inbox")
|
||||
|> put_change(:url, build_url(username, :page))
|
||||
end
|
||||
@@ -313,19 +325,19 @@ defmodule Mobilizon.Actors.Actor do
|
||||
defp build_urls(%Ecto.Changeset{} = changeset, _type), do: changeset
|
||||
|
||||
@doc """
|
||||
Build an AP URL for an actor
|
||||
Builds an AP URL for an actor.
|
||||
"""
|
||||
@spec build_url(String.t(), atom()) :: String.t()
|
||||
@spec build_url(String.t(), atom, keyword) :: String.t()
|
||||
def build_url(preferred_username, endpoint, args \\ [])
|
||||
|
||||
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
|
||||
|
||||
def build_url(preferred_username, :page, args) do
|
||||
Endpoint
|
||||
|> Routes.page_url(:actor, preferred_username, args)
|
||||
|> URI.decode()
|
||||
end
|
||||
|
||||
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
|
||||
|
||||
def build_url(preferred_username, endpoint, args)
|
||||
when endpoint in [:outbox, :following, :followers] do
|
||||
Endpoint
|
||||
@@ -333,267 +345,35 @@ defmodule Mobilizon.Actors.Actor do
|
||||
|> URI.decode()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a public key for a given ActivityPub actor ID (url)
|
||||
"""
|
||||
@spec get_public_key_for_url(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||
def get_public_key_for_url(url) do
|
||||
with {:ok, %Actor{keys: keys}} <- Actors.get_or_fetch_by_url(url),
|
||||
{:ok, public_key} <- prepare_public_key(keys) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
{:error, :pem_decode_error} ->
|
||||
Logger.error("Error while decoding PEM")
|
||||
{:error, :pem_decode_error}
|
||||
|
||||
_ ->
|
||||
Logger.error("Unable to fetch actor, so no keys for you")
|
||||
{:error, :actor_fetch_error}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert internal PEM encoded keys to public key format
|
||||
"""
|
||||
@spec prepare_public_key(String.t()) :: {:ok, tuple()} | {:error, :pem_decode_error}
|
||||
def prepare_public_key(public_key_code) do
|
||||
case :public_key.pem_decode(public_key_code) do
|
||||
[public_key_entry] ->
|
||||
{:ok, :public_key.pem_entry_decode(public_key_entry)}
|
||||
|
||||
_err ->
|
||||
{:error, :pem_decode_error}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get followers from an actor
|
||||
|
||||
If actor A and C both follow actor B, actor B's followers are A and C
|
||||
"""
|
||||
@spec get_followers(struct(), number(), number()) :: map()
|
||||
def get_followers(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do
|
||||
query =
|
||||
from(
|
||||
a in Actor,
|
||||
join: f in Follower,
|
||||
on: a.id == f.actor_id,
|
||||
where: f.target_actor_id == ^actor_id
|
||||
)
|
||||
|
||||
total = Task.async(fn -> Repo.aggregate(query, :count, :id) end)
|
||||
elements = Task.async(fn -> Repo.all(Page.paginate(query, page, limit)) end)
|
||||
|
||||
%{total: Task.await(total), elements: Task.await(elements)}
|
||||
end
|
||||
|
||||
defp get_full_followers_query(%Actor{id: actor_id} = _actor) do
|
||||
from(
|
||||
a in Actor,
|
||||
join: f in Follower,
|
||||
on: a.id == f.actor_id,
|
||||
where: f.target_actor_id == ^actor_id
|
||||
)
|
||||
end
|
||||
|
||||
@spec get_full_followers(struct()) :: list()
|
||||
def get_full_followers(%Actor{} = actor) do
|
||||
actor
|
||||
|> get_full_followers_query()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec get_full_external_followers(struct()) :: list()
|
||||
def get_full_external_followers(%Actor{} = actor) do
|
||||
actor
|
||||
|> get_full_followers_query()
|
||||
|> where([a], not is_nil(a.domain))
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get followings from an actor
|
||||
|
||||
If actor A follows actor B and C, actor A's followings are B and B
|
||||
"""
|
||||
@spec get_followings(struct(), number(), number()) :: list()
|
||||
def get_followings(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do
|
||||
query =
|
||||
from(
|
||||
a in Actor,
|
||||
join: f in Follower,
|
||||
on: a.id == f.target_actor_id,
|
||||
where: f.actor_id == ^actor_id
|
||||
)
|
||||
|
||||
total = Task.async(fn -> Repo.aggregate(query, :count, :id) end)
|
||||
elements = Task.async(fn -> Repo.all(Page.paginate(query, page, limit)) end)
|
||||
|
||||
%{total: Task.await(total), elements: Task.await(elements)}
|
||||
end
|
||||
|
||||
@spec get_full_followings(struct()) :: list()
|
||||
def get_full_followings(%Actor{id: actor_id} = _actor) do
|
||||
Repo.all(
|
||||
from(
|
||||
a in Actor,
|
||||
join: f in Follower,
|
||||
on: a.id == f.target_actor_id,
|
||||
where: f.actor_id == ^actor_id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the groups an actor is member of
|
||||
"""
|
||||
@spec get_groups_member_of(struct()) :: list()
|
||||
def get_groups_member_of(%Actor{id: actor_id}) do
|
||||
Repo.all(
|
||||
from(
|
||||
a in Actor,
|
||||
join: m in Member,
|
||||
on: a.id == m.parent_id,
|
||||
where: m.actor_id == ^actor_id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the members for a group actor
|
||||
"""
|
||||
@spec get_members_for_group(struct()) :: list()
|
||||
def get_members_for_group(%Actor{id: actor_id}) do
|
||||
Repo.all(
|
||||
from(
|
||||
a in Actor,
|
||||
join: m in Member,
|
||||
on: a.id == m.actor_id,
|
||||
where: m.parent_id == ^actor_id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor follow another
|
||||
"""
|
||||
@spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()}
|
||||
def follow(%Actor{} = followed, %Actor{} = follower, url \\ nil, approved \\ true) do
|
||||
with {:suspended, false} <- {:suspended, followed.suspended},
|
||||
# Check if followed has blocked follower
|
||||
{:already_following, false} <- {:already_following, following?(follower, followed)} do
|
||||
do_follow(follower, followed, approved, url)
|
||||
else
|
||||
{:already_following, %Follower{}} ->
|
||||
{:error, :already_following,
|
||||
"Could not follow actor: you are already following #{followed.preferred_username}"}
|
||||
|
||||
{:suspended, _} ->
|
||||
{:error, :suspended,
|
||||
"Could not follow actor: #{followed.preferred_username} has been suspended"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unfollow an actor (remove a `Mobilizon.Actors.Follower`)
|
||||
"""
|
||||
@spec unfollow(struct(), struct()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
|
||||
def unfollow(%Actor{} = followed, %Actor{} = follower) do
|
||||
case {:already_following, following?(follower, followed)} do
|
||||
{:already_following, %Follower{} = follow} ->
|
||||
Actors.delete_follower(follow)
|
||||
|
||||
{:already_following, false} ->
|
||||
{:error, "Could not unfollow actor: you are not following #{followed.preferred_username}"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec do_follow(struct(), struct(), boolean(), String.t()) ::
|
||||
{:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp do_follow(%Actor{} = follower, %Actor{} = followed, approved, url) do
|
||||
Logger.info(
|
||||
"Making #{follower.preferred_username} follow #{followed.preferred_username} (approved: #{
|
||||
approved
|
||||
})"
|
||||
)
|
||||
|
||||
Actors.create_follower(%{
|
||||
"actor_id" => follower.id,
|
||||
"target_actor_id" => followed.id,
|
||||
"approved" => approved,
|
||||
"url" => url
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether an actor is following another
|
||||
"""
|
||||
@spec following?(struct(), struct()) :: Follower.t() | false
|
||||
def following?(
|
||||
%Actor{} = follower_actor,
|
||||
%Actor{} = followed_actor
|
||||
) do
|
||||
case Actors.get_follower(followed_actor, follower_actor) do
|
||||
nil -> false
|
||||
%Follower{} = follow -> follow
|
||||
end
|
||||
end
|
||||
|
||||
@spec public_visibility?(struct()) :: boolean()
|
||||
def public_visibility?(%Actor{visibility: visibility}), do: visibility in [:public, :unlisted]
|
||||
|
||||
@doc """
|
||||
Return the preferred_username with the eventual @domain suffix if it's a distant actor
|
||||
"""
|
||||
@spec actor_acct_from_actor(struct()) :: String.t()
|
||||
def actor_acct_from_actor(%Actor{preferred_username: preferred_username, domain: domain}) do
|
||||
if is_nil(domain) do
|
||||
preferred_username
|
||||
else
|
||||
"#{preferred_username}@#{domain}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the display name if available, or the preferred_username (with the eventual @domain suffix if it's a distant actor).
|
||||
"""
|
||||
@spec display_name(struct()) :: String.t()
|
||||
def display_name(%Actor{name: name} = actor) do
|
||||
case name do
|
||||
nil -> actor_acct_from_actor(actor)
|
||||
"" -> actor_acct_from_actor(actor)
|
||||
name -> name
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return display name and username
|
||||
|
||||
## Examples
|
||||
iex> display_name_and_username(%Actor{name: "Thomas C", preferred_username: "tcit", domain: nil})
|
||||
"Thomas (tcit)"
|
||||
|
||||
iex> display_name_and_username(%Actor{name: "Thomas C", preferred_username: "tcit", domain: "framapiaf.org"})
|
||||
"Thomas (tcit@framapiaf.org)"
|
||||
|
||||
iex> display_name_and_username(%Actor{name: nil, preferred_username: "tcit", domain: "framapiaf.org"})
|
||||
"tcit@framapiaf.org"
|
||||
|
||||
"""
|
||||
@spec display_name_and_username(struct()) :: String.t()
|
||||
def display_name_and_username(%Actor{name: nil} = actor), do: actor_acct_from_actor(actor)
|
||||
def display_name_and_username(%Actor{name: ""} = actor), do: actor_acct_from_actor(actor)
|
||||
|
||||
def display_name_and_username(%Actor{name: name} = actor),
|
||||
do: name <> " (" <> actor_acct_from_actor(actor) <> ")"
|
||||
|
||||
@doc """
|
||||
Clear multiple caches for an actor
|
||||
"""
|
||||
# TODO: move to MobilizonWeb
|
||||
@spec clear_cache(struct()) :: {:ok, true}
|
||||
def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do
|
||||
Cachex.del(:activity_pub, "actor_" <> preferred_username)
|
||||
Cachex.del(:feed, "actor_" <> preferred_username)
|
||||
Cachex.del(:ics, "actor_" <> preferred_username)
|
||||
end
|
||||
|
||||
@spec build_relay_creation_attrs(map) :: map
|
||||
defp build_relay_creation_attrs(%{url: url, preferred_username: preferred_username}) do
|
||||
%{
|
||||
"name" => Config.get([:instance, :name], "Mobilizon"),
|
||||
"summary" =>
|
||||
Config.get(
|
||||
[:instance, :description],
|
||||
"An internal service actor for this Mobilizon instance"
|
||||
),
|
||||
"url" => url,
|
||||
"keys" => Crypto.generate_rsa_2048_private_key(),
|
||||
"preferred_username" => preferred_username,
|
||||
"domain" => nil,
|
||||
"inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
|
||||
"followers_url" => "#{url}/followers",
|
||||
"following_url" => "#{url}/following",
|
||||
"shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
|
||||
"type" => :Application
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,30 @@
|
||||
defmodule Mobilizon.Actors.Bot do
|
||||
@moduledoc """
|
||||
Represents a local bot
|
||||
Represents a local bot.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
source: String.t(),
|
||||
type: String.t(),
|
||||
actor: Actor.t(),
|
||||
user: User.t()
|
||||
}
|
||||
|
||||
@required_attrs [:source]
|
||||
@optional_attrs [:type, :actor_id, :user_id]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "bots" do
|
||||
field(:source, :string)
|
||||
field(:type, :string, default: :ics)
|
||||
|
||||
belongs_to(:actor, Actor)
|
||||
belongs_to(:user, User)
|
||||
|
||||
@@ -17,9 +32,10 @@ defmodule Mobilizon.Actors.Bot do
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
|
||||
def changeset(bot, attrs) do
|
||||
bot
|
||||
|> cast(attrs, [:source, :type, :actor_id, :user_id])
|
||||
|> validate_required([:source])
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,52 +1,63 @@
|
||||
defmodule Mobilizon.Actors.Follower do
|
||||
@moduledoc """
|
||||
Represents the following of an actor to another actor
|
||||
Represents the following of an actor to another actor.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Actors.Follower
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@type t :: %__MODULE__{
|
||||
approved: boolean,
|
||||
url: String.t(),
|
||||
target_actor: Actor.t(),
|
||||
actor: Actor.t()
|
||||
}
|
||||
|
||||
@required_attrs [:url, :approved, :target_actor_id, :actor_id]
|
||||
@attrs @required_attrs
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
schema "followers" do
|
||||
field(:approved, :boolean, default: false)
|
||||
field(:url, :string)
|
||||
|
||||
belongs_to(:target_actor, Actor)
|
||||
belongs_to(:actor, Actor)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Follower{} = member, attrs) do
|
||||
member
|
||||
|> cast(attrs, [:url, :approved, :target_actor_id, :actor_id])
|
||||
|> generate_url()
|
||||
|> validate_required([:url, :approved, :target_actor_id, :actor_id])
|
||||
|> unique_constraint(:target_actor_id, name: :followers_actor_target_actor_unique_index)
|
||||
@spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
|
||||
def changeset(follower, attrs) do
|
||||
follower
|
||||
|> cast(attrs, @attrs)
|
||||
|> ensure_url()
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:target_actor_id,
|
||||
name: :followers_actor_target_actor_unique_index
|
||||
)
|
||||
end
|
||||
|
||||
# If there's a blank URL that's because we're doing the first insert
|
||||
defp generate_url(%Ecto.Changeset{data: %Follower{url: nil}} = changeset) do
|
||||
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do
|
||||
case fetch_change(changeset, :url) do
|
||||
{:ok, _url} -> changeset
|
||||
:error -> do_generate_url(changeset)
|
||||
:error -> generate_url(changeset)
|
||||
end
|
||||
end
|
||||
|
||||
# Most time just go with the given URL
|
||||
defp generate_url(%Ecto.Changeset{} = changeset), do: changeset
|
||||
defp ensure_url(%Ecto.Changeset{} = changeset), do: changeset
|
||||
|
||||
defp do_generate_url(%Ecto.Changeset{} = changeset) do
|
||||
@spec generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp generate_url(%Ecto.Changeset{} = changeset) do
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
changeset
|
||||
|> put_change(
|
||||
:url,
|
||||
"#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}"
|
||||
)
|
||||
|> put_change(
|
||||
:id,
|
||||
uuid
|
||||
)
|
||||
|> put_change(:id, uuid)
|
||||
|> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,100 +1,59 @@
|
||||
import EctoEnum
|
||||
|
||||
defenum(Mobilizon.Actors.MemberRoleEnum, :member_role_type, [
|
||||
:not_approved,
|
||||
:member,
|
||||
:moderator,
|
||||
:administrator,
|
||||
:creator
|
||||
])
|
||||
|
||||
defmodule Mobilizon.Actors.Member do
|
||||
@moduledoc """
|
||||
Represents the membership of an actor to a group
|
||||
Represents the membership of an actor to a group.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Actors.{Actor, Member, MemberRole}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
role: MemberRole.t(),
|
||||
parent: Actor.t(),
|
||||
actor: Actor.t()
|
||||
}
|
||||
|
||||
@required_attrs [:parent_id, :actor_id]
|
||||
@optional_attrs [:role]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "members" do
|
||||
field(:role, Mobilizon.Actors.MemberRoleEnum, default: :member)
|
||||
field(:role, MemberRole, default: :member)
|
||||
|
||||
belongs_to(:parent, Actor)
|
||||
belongs_to(:actor, Actor)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Member{} = member, attrs) do
|
||||
member
|
||||
|> cast(attrs, [:role, :parent_id, :actor_id])
|
||||
|> validate_required([:parent_id, :actor_id])
|
||||
|> unique_constraint(:parent_id, name: :members_actor_parent_unique_index)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single member of an actor (for example a group)
|
||||
Gets the default member role depending on the actor openness.
|
||||
"""
|
||||
def get_member(actor_id, parent_id) do
|
||||
case Repo.get_by(Member, actor_id: actor_id, parent_id: parent_id) do
|
||||
nil -> {:error, :member_not_found}
|
||||
member -> {:ok, member}
|
||||
end
|
||||
end
|
||||
@spec get_default_member_role(Actor.t()) :: atom
|
||||
def get_default_member_role(%Actor{openness: :open}), do: :member
|
||||
def get_default_member_role(%Actor{}), do: :not_approved
|
||||
|
||||
@doc """
|
||||
Gets a single member of an actor (for example a group)
|
||||
Checks whether the actor can be joined to the group.
|
||||
"""
|
||||
def can_be_joined(%Actor{type: :Group, openness: :invite_only}), do: false
|
||||
def can_be_joined(%Actor{type: :Group}), do: true
|
||||
|
||||
@doc """
|
||||
Returns the list of administrator members for a group.
|
||||
"""
|
||||
def list_administrator_members_for_group(id, page \\ nil, limit \\ nil) do
|
||||
Repo.all(
|
||||
from(
|
||||
m in Member,
|
||||
where: m.parent_id == ^id and (m.role == ^:creator or m.role == ^:administrator),
|
||||
preload: [:actor]
|
||||
)
|
||||
|> Page.paginate(page, limit)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get all group ids where the actor_id is the last administrator
|
||||
"""
|
||||
def list_group_id_where_last_administrator(actor_id) do
|
||||
in_query =
|
||||
from(
|
||||
m in Member,
|
||||
where: m.actor_id == ^actor_id and (m.role == ^:creator or m.role == ^:administrator),
|
||||
select: m.parent_id
|
||||
)
|
||||
|
||||
Repo.all(
|
||||
from(
|
||||
m in Member,
|
||||
where: m.role == ^:creator or m.role == ^:administrator,
|
||||
join: m2 in subquery(in_query),
|
||||
on: m.parent_id == m2.parent_id,
|
||||
group_by: m.parent_id,
|
||||
select: m.parent_id,
|
||||
having: count(m.actor_id) == 1
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the member is an administrator (admin or creator) of the group
|
||||
Checks whether the member is an administrator (admin or creator) of the group.
|
||||
"""
|
||||
def is_administrator(%Member{role: :administrator}), do: {:is_admin, true}
|
||||
def is_administrator(%Member{role: :creator}), do: {:is_admin, true}
|
||||
def is_administrator(%Member{}), do: {:is_admin, false}
|
||||
|
||||
@doc false
|
||||
@spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
|
||||
def changeset(member, attrs) do
|
||||
member
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:parent_id, name: :members_actor_parent_unique_index)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,12 +18,6 @@ defmodule Mobilizon.Addresses do
|
||||
@spec query(Ecto.Query.t(), map) :: Ecto.Query.t()
|
||||
def query(queryable, _params), do: queryable
|
||||
|
||||
@doc """
|
||||
Returns the list of addresses.
|
||||
"""
|
||||
@spec list_addresses :: [Address.t()]
|
||||
def list_addresses, do: Repo.all(Address)
|
||||
|
||||
@doc """
|
||||
Gets a single address.
|
||||
"""
|
||||
@@ -72,6 +66,12 @@ defmodule Mobilizon.Addresses do
|
||||
@spec delete_address(Address.t()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_address(%Address{} = address), do: Repo.delete(address)
|
||||
|
||||
@doc """
|
||||
Returns the list of addresses.
|
||||
"""
|
||||
@spec list_addresses :: [Address.t()]
|
||||
def list_addresses, do: Repo.all(Address)
|
||||
|
||||
@doc """
|
||||
Searches addresses.
|
||||
|
||||
|
||||
@@ -8,16 +8,6 @@ defmodule Mobilizon.Admin do
|
||||
alias Mobilizon.Admin.ActionLog
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
|
||||
@doc """
|
||||
Returns the list of action logs.
|
||||
"""
|
||||
@spec list_action_logs(integer | nil, integer | nil) :: [ActionLog.t()]
|
||||
def list_action_logs(page \\ nil, limit \\ nil) do
|
||||
list_action_logs_query()
|
||||
|> Page.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a action_log.
|
||||
"""
|
||||
@@ -28,6 +18,16 @@ defmodule Mobilizon.Admin do
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of action logs.
|
||||
"""
|
||||
@spec list_action_logs(integer | nil, integer | nil) :: [ActionLog.t()]
|
||||
def list_action_logs(page \\ nil, limit \\ nil) do
|
||||
list_action_logs_query()
|
||||
|> Page.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec list_action_logs_query :: Ecto.Query.t()
|
||||
defp list_action_logs_query do
|
||||
from(r in ActionLog, preload: [:actor])
|
||||
|
||||
@@ -12,4 +12,17 @@ defmodule Mobilizon.Crypto do
|
||||
|> :crypto.strong_rand_bytes()
|
||||
|> Base.url_encode64()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generate RSA 2048-bit private key.
|
||||
"""
|
||||
@spec generate_rsa_2048_private_key :: String.t()
|
||||
def generate_rsa_2048_private_key do
|
||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||
|
||||
[entry]
|
||||
|> :public_key.pem_encode()
|
||||
|> String.trim_trailing()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,17 +21,6 @@ defmodule Mobilizon.Reports do
|
||||
@spec query(Ecto.Query.t(), map) :: Ecto.Query.t()
|
||||
def query(queryable, _params), do: queryable
|
||||
|
||||
@doc """
|
||||
Returns the list of reports.
|
||||
"""
|
||||
@spec list_reports(integer | nil, integer | nil, atom, atom) :: [Report.t()]
|
||||
def list_reports(page \\ nil, limit \\ nil, sort \\ :updated_at, direction \\ :asc) do
|
||||
list_reports_query()
|
||||
|> Page.paginate(page, limit)
|
||||
|> sort(sort, direction)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single report.
|
||||
"""
|
||||
@@ -90,17 +79,16 @@ defmodule Mobilizon.Reports do
|
||||
Deletes a report.
|
||||
"""
|
||||
@spec delete_report(Report.t()) :: {:ok, Report.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_report(%Report{} = report) do
|
||||
Repo.delete(report)
|
||||
end
|
||||
def delete_report(%Report{} = report), do: Repo.delete(report)
|
||||
|
||||
@doc """
|
||||
Returns the list of notes for a report.
|
||||
Returns the list of reports.
|
||||
"""
|
||||
@spec list_notes_for_report(Report.t()) :: [Note.t()]
|
||||
def list_notes_for_report(%Report{id: report_id}) do
|
||||
report_id
|
||||
|> list_notes_for_report_query()
|
||||
@spec list_reports(integer | nil, integer | nil, atom, atom) :: [Report.t()]
|
||||
def list_reports(page \\ nil, limit \\ nil, sort \\ :updated_at, direction \\ :asc) do
|
||||
list_reports_query()
|
||||
|> Page.paginate(page, limit)
|
||||
|> sort(sort, direction)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@@ -134,8 +122,21 @@ defmodule Mobilizon.Reports do
|
||||
Deletes a note.
|
||||
"""
|
||||
@spec delete_note(Note.t()) :: {:ok, Note.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_note(%Note{} = note) do
|
||||
Repo.delete(note)
|
||||
def delete_note(%Note{} = note), do: Repo.delete(note)
|
||||
|
||||
@doc """
|
||||
Returns the list of notes for a report.
|
||||
"""
|
||||
@spec list_notes_for_report(Report.t()) :: [Note.t()]
|
||||
def list_notes_for_report(%Report{id: report_id}) do
|
||||
report_id
|
||||
|> list_notes_for_report_query()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec report_by_url_query(String.t()) :: Ecto.Query.t()
|
||||
defp report_by_url_query(url) do
|
||||
from(r in Report, where: r.uri == ^url)
|
||||
end
|
||||
|
||||
@spec list_reports_query :: Ecto.Query.t()
|
||||
@@ -146,11 +147,6 @@ defmodule Mobilizon.Reports do
|
||||
)
|
||||
end
|
||||
|
||||
@spec report_by_url_query(String.t()) :: Ecto.Query.t()
|
||||
defp report_by_url_query(url) do
|
||||
from(r in Report, where: r.uri == ^url)
|
||||
end
|
||||
|
||||
@spec list_notes_for_report_query(integer | String.t()) :: Ecto.Query.t()
|
||||
defp list_notes_for_report_query(report_id) do
|
||||
from(
|
||||
|
||||
@@ -101,9 +101,7 @@ defmodule Mobilizon.Users do
|
||||
Deletes an user.
|
||||
"""
|
||||
@spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_user(%User{} = user) do
|
||||
Repo.delete(user)
|
||||
end
|
||||
def delete_user(%User{} = user), do: Repo.delete(user)
|
||||
|
||||
@doc """
|
||||
Get an user with its actors
|
||||
@@ -219,9 +217,7 @@ defmodule Mobilizon.Users do
|
||||
Counts users.
|
||||
"""
|
||||
@spec count_users :: integer
|
||||
def count_users do
|
||||
Repo.one(from(u in User, select: count(u.id)))
|
||||
end
|
||||
def count_users, do: Repo.one(from(u in User, select: count(u.id)))
|
||||
|
||||
@doc """
|
||||
Authenticate an user.
|
||||
|
||||
Reference in New Issue
Block a user