Refactoring of Actors context

This commit is contained in:
miffigriffy
2019-09-09 00:52:49 +02:00
parent 3a4a006c44
commit 4418275223
36 changed files with 1145 additions and 1345 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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])

View File

@@ -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

View File

@@ -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(

View File

@@ -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.