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.

View File

@ -32,7 +32,7 @@ defmodule MobilizonWeb.API.Follows do
def accept(%Actor{} = follower, %Actor{} = followed) do
with %Follower{approved: false, id: follow_id, url: follow_url} = follow <-
Actor.following?(follower, followed),
Actors.following?(follower, followed),
activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}",
data <-
ActivityPub.Utils.make_follow_data(followed, follower, follow_url),

View File

@ -1,20 +1,21 @@
defmodule MobilizonWeb.API.Search do
@moduledoc """
API for Search
API for search.
"""
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors.ActorType
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Storage.Page
require Logger
@doc """
Search actors
Searches actors.
"""
@spec search_actors(String.t(), integer(), integer(), String.t()) ::
{:ok, %{total: integer(), elements: list(Actor.t())}} | {:error, any()}
@spec search_actors(String.t(), integer | nil, integer | nil, ActorType.t()) ::
{:ok, Page.t()} | {:error, String.t()}
def search_actors(search, page \\ 1, limit \\ 10, result_type) do
search = String.trim(search)
@ -22,31 +23,33 @@ defmodule MobilizonWeb.API.Search do
search == "" ->
{:error, "Search can't be empty"}
# Some URLs could be domain.tld/@username, so keep this condition above handle_search? function
url_search?(search) ->
# If this is not an actor, skip
# Some URLs could be domain.tld/@username, so keep this condition above
# the `is_handle` function
is_url(search) ->
# skip, if it's not an actor
case process_from_url(search) do
%{:total => total, :elements => [%Actor{}] = elements} ->
{:ok, %{total: total, elements: elements}}
%Page{total: _total, elements: _elements} = page ->
{:ok, page}
_ ->
{:ok, %{total: 0, elements: []}}
end
handle_search?(search) ->
is_handle(search) ->
{:ok, process_from_username(search)}
true ->
{:ok,
Actors.find_and_count_actors_by_username_or_name(search, [result_type], page, limit)}
page = Actors.build_actors_by_username_or_name_page(search, [result_type], page, limit)
{:ok, page}
end
end
@doc """
Search events
"""
@spec search_events(String.t(), integer(), integer()) ::
{:ok, %{total: integer(), elements: list(Event.t())}} | {:error, any()}
@spec search_events(String.t(), integer | nil, integer | nil) ::
{:ok, Page.t()} | {:error, String.t()}
def search_events(search, page \\ 1, limit \\ 10) do
search = String.trim(search)
@ -54,11 +57,11 @@ defmodule MobilizonWeb.API.Search do
search == "" ->
{:error, "Search can't be empty"}
url_search?(search) ->
# If this is not an event, skip
is_url(search) ->
# skip, if it's w not an actor
case process_from_url(search) do
{total = total, [%Event{} = elements]} ->
{:ok, %{total: total, elements: elements}}
%Page{total: _total, elements: _elements} = page ->
{:ok, page}
_ ->
{:ok, %{total: 0, elements: []}}
@ -70,43 +73,36 @@ defmodule MobilizonWeb.API.Search do
end
# If the search string is an username
@spec process_from_username(String.t()) :: %{total: integer(), elements: [Actor.t()]}
@spec process_from_username(String.t()) :: Page.t()
defp process_from_username(search) do
case ActivityPub.find_or_make_actor_from_nickname(search) do
{:ok, actor} ->
%{total: 1, elements: [actor]}
%Page{total: 1, elements: [actor]}
{:error, _err} ->
Logger.debug(fn -> "Unable to find or make actor '#{search}'" end)
%{total: 0, elements: []}
%Page{total: 0, elements: []}
end
end
# If the search string is an URL
@spec process_from_url(String.t()) :: %{
total: integer(),
elements: [Actor.t() | Event.t() | Comment.t()]
}
@spec process_from_url(String.t()) :: Page.t()
defp process_from_url(search) do
case ActivityPub.fetch_object_from_url(search) do
{:ok, object} ->
%{total: 1, elements: [object]}
%Page{total: 1, elements: [object]}
{:error, _err} ->
Logger.debug(fn -> "Unable to find or make object from URL '#{search}'" end)
%{total: 0, elements: []}
%Page{total: 0, elements: []}
end
end
# Is the search an URL search?
@spec url_search?(String.t()) :: boolean
defp url_search?(search) do
String.starts_with?(search, "https://") or String.starts_with?(search, "http://")
end
@spec is_url(String.t()) :: boolean
defp is_url(search), do: String.starts_with?(search, ["http://", "https://"])
# Is the search an handle search?
@spec handle_search?(String.t()) :: boolean
defp handle_search?(search) do
String.match?(search, ~r/@/)
end
@spec is_handle(String.t()) :: boolean
defp is_handle(search), do: String.match?(search, ~r/@/)
end

View File

@ -31,7 +31,7 @@ defmodule MobilizonWeb.ActivityPubController do
def following(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page),
%Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
%Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("following.json", %{actor: actor, page: page}))
@ -39,7 +39,7 @@ defmodule MobilizonWeb.ActivityPubController do
end
def following(conn, %{"name" => name}) do
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("following.json", %{actor: actor}))
@ -48,7 +48,7 @@ defmodule MobilizonWeb.ActivityPubController do
def followers(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page),
%Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
%Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("followers.json", %{actor: actor, page: page}))
@ -56,7 +56,7 @@ defmodule MobilizonWeb.ActivityPubController do
end
def followers(conn, %{"name" => name}) do
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("followers.json", %{actor: actor}))

View File

@ -76,7 +76,7 @@ defmodule MobilizonWeb.Resolvers.Group do
) do
with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Member.get_member(actor_id, group.id),
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
{:is_admin, true} <- Member.is_administrator(member),
group <- Actors.delete_group!(group) do
{:ok, %{id: group.id}}
@ -109,9 +109,9 @@ defmodule MobilizonWeb.Resolvers.Group do
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:error, :member_not_found} <- Member.get_member(actor.id, group.id),
{:error, :member_not_found} <- Actors.get_member(actor.id, group.id),
{:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
role <- Mobilizon.Actors.get_default_member_role(group),
role <- Member.get_default_member_role(group),
{:ok, _} <- Actors.create_member(%{parent_id: group.id, actor_id: actor.id, role: role}) do
{
:ok,
@ -149,7 +149,7 @@ defmodule MobilizonWeb.Resolvers.Group do
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Member.get_member(actor.id, group_id),
{:ok, %Member{} = member} <- Actors.get_member(actor.id, group_id),
{:only_administrator, false} <-
{:only_administrator, check_that_member_is_not_last_administrator(group_id, actor_id)},
{:ok, _} <-
@ -176,7 +176,7 @@ defmodule MobilizonWeb.Resolvers.Group do
# and that it's the actor requesting leaving the group we return true
@spec check_that_member_is_not_last_administrator(integer(), integer()) :: boolean()
defp check_that_member_is_not_last_administrator(group_id, actor_id) do
case Member.list_administrator_members_for_group(group_id) do
case Actors.list_administrator_members_for_group(group_id) do
[%Member{actor: %Actor{id: member_actor_id}}] ->
actor_id == member_actor_id

View File

@ -2,12 +2,13 @@ defmodule MobilizonWeb.Resolvers.Person do
@moduledoc """
Handles the person-related GraphQL calls
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Users.User
alias Mobilizon.Users
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Users
alias Mobilizon.Users.User
@doc """
Find a person
@ -206,7 +207,7 @@ defmodule MobilizonWeb.Resolvers.Person do
# We check that the actor is not the last administrator/creator of a group
@spec last_admin_of_a_group?(integer()) :: boolean()
defp last_admin_of_a_group?(actor_id) do
length(Member.list_group_id_where_last_administrator(actor_id)) > 0
length(Actors.list_group_id_where_last_administrator(actor_id)) > 0
end
@spec proxify_avatar(Actor.t()) :: Actor.t()

View File

@ -91,7 +91,7 @@ defmodule MobilizonWeb.Resolvers.Report do
when is_moderator(role) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id),
%Report{} = report <- Reports.get_report(report_id),
%Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id),
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
{:ok, %Note{} = note} <-
MobilizonWeb.API.Reports.create_report_note(report, moderator, content) do
{:ok, note}
@ -106,7 +106,7 @@ defmodule MobilizonWeb.Resolvers.Report do
when is_moderator(role) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id),
%Note{} = note <- Reports.get_note(note_id),
%Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id),
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
{:ok, %Note{} = note} <-
MobilizonWeb.API.Reports.delete_report_note(note, moderator) do
{:ok, %{id: note.id}}

View File

@ -1,6 +1,7 @@
defmodule MobilizonWeb.ActivityPub.ActorView do
use MobilizonWeb, :view
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils
@ -47,8 +48,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("following.json", %{actor: actor, page: page}) do
%{total: total, elements: following} =
if Actor.public_visibility?(actor),
do: Actor.get_followings(actor, page),
if Actor.is_public_visibility(actor),
do: Actors.get_followings(actor, page),
else: @private_visibility_empty_collection
following
@ -58,8 +59,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("following.json", %{actor: actor}) do
%{total: total, elements: following} =
if Actor.public_visibility?(actor),
do: Actor.get_followings(actor),
if Actor.is_public_visibility(actor),
do: Actors.get_followings(actor),
else: @private_visibility_empty_collection
%{
@ -73,8 +74,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("followers.json", %{actor: actor, page: page}) do
%{total: total, elements: followers} =
if Actor.public_visibility?(actor),
do: Actor.get_followers(actor, page),
if Actor.is_public_visibility(actor),
do: Actors.get_followers(actor, page),
else: @private_visibility_empty_collection
followers
@ -84,8 +85,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("followers.json", %{actor: actor}) do
%{total: total, elements: followers} =
if Actor.public_visibility?(actor),
do: Actor.get_followers(actor),
if Actor.is_public_visibility(actor),
do: Actors.get_followers(actor),
else: @private_visibility_empty_collection
%{
@ -99,7 +100,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("outbox.json", %{actor: actor, page: page}) do
%{total: total, elements: followers} =
if Actor.public_visibility?(actor),
if Actor.is_public_visibility(actor),
do: ActivityPub.fetch_public_activities_for_actor(actor, page),
else: @private_visibility_empty_collection
@ -110,7 +111,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("outbox.json", %{actor: actor}) do
%{total: total, elements: followers} =
if Actor.public_visibility?(actor),
if Actor.is_public_visibility(actor),
do: ActivityPub.fetch_public_activities_for_actor(actor),
else: @private_visibility_empty_collection

View File

@ -113,6 +113,29 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
@doc """
Getting an actor from url, eventually creating it
"""
@spec get_or_fetch_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
def get_or_fetch_by_url(url, preload \\ false) do
case Actors.get_actor_by_url(url, preload) do
{:ok, %Actor{} = actor} ->
{:ok, actor}
_ ->
case make_actor_from_url(url, preload) do
{:ok, %Actor{} = actor} ->
{:ok, actor}
_ ->
Logger.warn("Could not fetch by AP id")
{:error, "Could not fetch by AP id"}
end
end
end
@doc """
Create an activity of type "Create"
"""
@ -279,7 +302,7 @@ defmodule Mobilizon.Service.ActivityPub do
"""
def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{url: follow_url}} <-
Actor.follow(followed, follower, activity_id, false),
Actors.follow(followed, follower, activity_id, false),
activity_follow_id <-
activity_id || follow_url,
data <- make_follow_data(followed, follower, activity_follow_id),
@ -298,7 +321,7 @@ defmodule Mobilizon.Service.ActivityPub do
"""
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower),
with {:ok, %Follower{id: follow_id}} <- Actors.unfollow(followed, follower),
# We recreate the follow activity
data <-
make_follow_data(
@ -466,7 +489,7 @@ defmodule Mobilizon.Service.ActivityPub do
def make_actor_from_url(url, preload \\ false) do
case fetch_and_prepare_actor_from_url(url) do
{:ok, data} ->
Actors.insert_or_update_actor(data, preload)
Actors.upsert_actor(data, preload)
# Request returned 410
{:error, :actor_deleted} ->
@ -529,7 +552,7 @@ defmodule Mobilizon.Service.ActivityPub do
followers =
if actor.followers_url in activity.recipients do
Actor.get_full_external_followers(actor)
Actors.get_full_external_followers(actor)
else
[]
end

View File

@ -4,7 +4,6 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
This module allows to convert events from ActivityStream format to our own internal one, and back
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment, as: CommentModel
alias Mobilizon.Events.Event
@ -20,7 +19,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
@impl Converter
@spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do
{:ok, %Actor{id: actor_id}} = Actors.get_or_fetch_by_url(object["actor"])
{:ok, %Actor{id: actor_id}} = ActivityPub.get_or_fetch_by_url(object["actor"])
Logger.debug("Inserting full comment")
Logger.debug(inspect(object))

View File

@ -24,7 +24,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def follow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.follow(local_actor, target_actor) do
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
@ -37,7 +37,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def unfollow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.unfollow(local_actor, target_actor) do
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
@ -50,7 +50,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def accept(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.accept(target_actor, local_actor) do
{:ok, activity}
end
@ -58,7 +58,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
# def reject(target_instance) do
# with %Actor{} = local_actor <- get_actor(),
# {:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
# {:ok, %Actor{} = target_actor} <- Activity.get_or_fetch_by_url(target_instance),
# {:ok, activity} <- Follows.reject(target_actor, local_actor) do
# {:ok, activity}
# end

View File

@ -139,7 +139,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
Logger.info("Handle incoming to create notes")
with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(data["actor"]) do
with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(data["actor"]) do
Logger.debug("found actor")
Logger.debug(inspect(actor))
@ -163,7 +163,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object} = data) do
Logger.info("Handle incoming to create event")
with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(data["actor"]) do
with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(data["actor"]) do
Logger.debug("found actor")
Logger.debug(inspect(actor))
@ -187,8 +187,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
) do
with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true),
{:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower),
with {:ok, %Actor{} = followed} <- ActivityPub.get_or_fetch_by_url(followed, true),
{:ok, %Actor{} = follower} <- ActivityPub.get_or_fetch_by_url(follower),
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
{:ok, activity, object}
else
@ -207,7 +207,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
} = data
) do
with actor_url <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor_url),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor_url),
{:object_not_found, {:ok, activity, object}} <-
{:object_not_found,
do_handle_incoming_accept_following(accepted_object, actor) ||
@ -236,7 +236,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
%{"type" => "Reject", "object" => rejected_object, "actor" => _actor, "id" => id} = data
) do
with actor_url <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor_url),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor_url),
{:object_not_found, {:ok, activity, object}} <-
{:object_not_found,
do_handle_incoming_reject_following(rejected_object, actor) ||
@ -279,7 +279,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
) do
with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
public <- Visibility.is_public?(data),
{:ok, activity, object} <- ActivityPub.announce(actor, object, id, false, public) do
@ -347,7 +347,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
} = data
) do
with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
{:ok, activity, object} <-
ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
@ -451,7 +451,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# } = data
# ) do
# with actor <- get_actor(data),
# %Actor{} = actor <- Actors.get_or_fetch_by_url(actor),
# %Actor{} = actor <- ActivityPub.get_or_fetch_by_url(actor),
# {:ok, object} <- fetch_obj_helper(object_id) || fetch_obj_helper(object_id),
# {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
# {:ok, activity}

View File

@ -44,7 +44,7 @@ defmodule Mobilizon.Service.Export.Feed do
@spec fetch_actor_event_feed(String.t()) :: String.t()
defp fetch_actor_event_feed(name) do
with %Actor{} = actor <- Actors.get_local_actor_by_name(name),
{:visibility, true} <- {:visibility, Actor.public_visibility?(actor)},
{:visibility, true} <- {:visibility, Actor.is_public_visibility(actor)},
{:ok, events, _count} <- Events.get_public_events_for_actor(actor) do
{:ok, build_actor_feed(actor, events)}
else

View File

@ -44,7 +44,7 @@ defmodule Mobilizon.Service.Export.ICalendar do
"""
@spec export_public_actor(Actor.t()) :: String.t()
def export_public_actor(%Actor{} = actor) do
with true <- Actor.public_visibility?(actor),
with true <- Actor.is_public_visibility(actor),
{:ok, events, _} <- Events.get_public_events_for_actor(actor) do
{:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()}
end

View File

@ -7,20 +7,23 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do
@moduledoc """
Adapter for the `HTTPSignatures` lib that handles signing and providing public keys to verify HTTPSignatures
"""
@behaviour HTTPSignatures.Adapter
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
require Logger
def key_id_to_actor_url(key_id) do
uri =
URI.parse(key_id)
%{path: path} = uri =
key_id
|> URI.parse()
|> Map.put(:fragment, nil)
uri =
if not is_nil(uri.path) and String.ends_with?(uri.path, "/publickey") do
Map.put(uri, :path, String.replace(uri.path, "/publickey", ""))
if not is_nil(path) do
Map.put(uri, :path, String.trim_trailing(path, "/publickey"))
else
uri
end
@ -28,11 +31,47 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do
URI.to_string(uri)
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)}
_ ->
{:error, :pem_decode_error}
end
end
@doc """
Gets a public key for a given ActivityPub actor ID (url).
"""
@spec get_public_key_for_url(String.t()) ::
{:ok, String.t()} | {:error, :actor_fetch_error | :pem_decode_error}
def get_public_key_for_url(url) do
with {:ok, %Actor{keys: keys}} <- ActivityPub.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
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid),
:ok <- Logger.debug("Fetching public key for #{actor_id}"),
{:ok, public_key} <- Actor.get_public_key_for_url(actor_id) do
{:ok, public_key} <- get_public_key_for_url(actor_id) do
{:ok, public_key}
else
e ->
@ -45,7 +84,7 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do
actor_id <- key_id_to_actor_url(kid),
:ok <- Logger.debug("Refetching public key for #{actor_id}"),
{:ok, _actor} <- ActivityPub.make_actor_from_url(actor_id),
{:ok, public_key} <- Actor.get_public_key_for_url(actor_id) do
{:ok, public_key} <- get_public_key_for_url(actor_id) do
{:ok, public_key}
else
e ->
@ -53,12 +92,12 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do
end
end
def sign(%Actor{} = actor, headers) do
def sign(%Actor{keys: keys} = actor, headers) do
Logger.debug("Signing on behalf of #{actor.url}")
Logger.debug("headers")
Logger.debug(inspect(headers))
with {:ok, key} <- actor.keys |> Actor.prepare_public_key() do
with {:ok, key} <- prepare_public_key(keys) do
HTTPSignatures.sign(key, actor.url <> "#main-key", headers)
end
end

View File

@ -184,9 +184,9 @@ defmodule Mobilizon.Mixfile do
Models: [
Mobilizon.Actors,
Mobilizon.Actors.Actor,
Mobilizon.Actors.ActorOpennessEnum,
Mobilizon.Actors.ActorTypeEnum,
Mobilizon.Actors.MemberRoleEnum,
Mobilizon.Actors.ActorOpenness,
Mobilizon.Actors.ActorType,
Mobilizon.Actors.MemberRole,
Mobilizon.Actors.Bot,
Mobilizon.Actors.Follower,
Mobilizon.Actors.Member,

View File

@ -18,7 +18,7 @@ defmodule Mobilizon.Repo.Migrations.MoveFromAccountToActor do
drop(table("groups"))
rename(table("accounts"), to: table("actors"))
Mobilizon.Actors.ActorTypeEnum.create_type()
Mobilizon.Actors.ActorType.create_type()
rename(table("actors"), :username, to: :name)
rename(table("actors"), :description, to: :summary)
rename(table("actors"), :display_name, to: :preferred_username)
@ -86,7 +86,7 @@ defmodule Mobilizon.Repo.Migrations.MoveFromAccountToActor do
modify(:display_name, :string, null: true)
end
Mobilizon.Actors.ActorTypeEnum.drop_type()
Mobilizon.Actors.ActorType.drop_type()
rename(table("events"), :organizer_actor_id, to: :organizer_account_id)

View File

@ -1,12 +1,12 @@
defmodule Mobilizon.Repo.Migrations.MoveMemberRoleToEnum do
use Ecto.Migration
alias Mobilizon.Actors.MemberRoleEnum
alias Mobilizon.Actors.MemberRole
def up do
MemberRoleEnum.create_type()
MemberRole.create_type()
alter table(:members) do
add(:role_tmp, MemberRoleEnum.type(), default: "member")
add(:role_tmp, MemberRole.type(), default: "member")
end
execute("UPDATE members set role_tmp = 'member' where role = 0")
@ -39,7 +39,7 @@ defmodule Mobilizon.Repo.Migrations.MoveMemberRoleToEnum do
remove(:role)
end
MemberRoleEnum.drop_type()
MemberRole.drop_type()
rename(table(:members), :role_tmp, to: :role)
end

View File

@ -1,12 +1,12 @@
defmodule Mobilizon.Repo.Migrations.ActorGroupOpenness do
use Ecto.Migration
alias Mobilizon.Actors.ActorOpennessEnum
alias Mobilizon.Actors.ActorOpenness
def up do
ActorOpennessEnum.create_type()
ActorOpenness.create_type()
alter table(:actors) do
add(:openness, ActorOpennessEnum.type(), default: "moderated")
add(:openness, ActorOpenness.type(), default: "moderated")
end
end

View File

@ -1,13 +1,13 @@
defmodule Mobilizon.Repo.Migrations.AddVisibilityToActor do
use Ecto.Migration
alias Mobilizon.Actors.ActorVisibilityEnum
alias Mobilizon.Actors.ActorVisibility
def up do
ActorVisibilityEnum.create_type()
ActorVisibility.create_type()
alter table(:actors) do
add(:visibility, ActorVisibilityEnum.type(), default: "private")
add(:visibility, ActorVisibility.type(), default: "private")
end
end
@ -16,6 +16,6 @@ defmodule Mobilizon.Repo.Migrations.AddVisibilityToActor do
remove(:visibility)
end
ActorVisibilityEnum.drop_type()
ActorVisibility.drop_type()
end
end

View File

@ -1,11 +1,15 @@
defmodule Mobilizon.ActorsTest do
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
use Mobilizon.DataCase
import Mobilizon.Factory
alias Mobilizon.{Actors, Config, Users}
alias Mobilizon.Actors.{Actor, Member, Follower, Bot}
alias Mobilizon.Media.File, as: FileModel
import Mobilizon.Factory
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Storage.Page
describe "actors" do
@valid_attrs %{
@ -40,8 +44,6 @@ defmodule Mobilizon.ActorsTest do
}
@remote_account_url "https://social.tcit.fr/users/tcit"
@remote_account_username "tcit"
@remote_account_domain "social.tcit.fr"
setup do
user = insert(:user)
@ -70,14 +72,14 @@ defmodule Mobilizon.ActorsTest do
assert actor_id == Users.get_actor_for_user(user).id
end
test "get_actor_with_everything/1 returns the actor with it's organized events", %{
test "get_actor_with_preload/1 returns the actor with it's organized events", %{
actor: actor
} do
assert Actors.get_actor_with_everything(actor.id).organized_events == []
assert Actors.get_actor_with_preload(actor.id).organized_events == []
event = insert(:event, organizer_actor: actor)
event_found_id =
Actors.get_actor_with_everything(actor.id).organized_events |> hd |> Map.get(:id)
Actors.get_actor_with_preload(actor.id).organized_events |> hd |> Map.get(:id)
assert event_found_id == event.id
end
@ -97,7 +99,7 @@ defmodule Mobilizon.ActorsTest do
preferred_username: preferred_username,
domain: domain,
avatar: %FileModel{name: picture_name} = _picture
} = _actor} = Actors.get_or_fetch_by_url(@remote_account_url)
} = _actor} = ActivityPub.get_or_fetch_by_url(@remote_account_url)
assert picture_name == "avatar"
@ -111,51 +113,51 @@ defmodule Mobilizon.ActorsTest do
end
end
test "get_local_actor_by_name_with_everything!/1 returns the local actor with it's organized events",
test "get_local_actor_by_name_with_preload!/1 returns the local actor with it's organized events",
%{
actor: actor
} do
assert Actors.get_local_actor_by_name_with_everything(actor.preferred_username).organized_events ==
assert Actors.get_local_actor_by_name_with_preload(actor.preferred_username).organized_events ==
[]
event = insert(:event, organizer_actor: actor)
event_found_id =
Actors.get_local_actor_by_name_with_everything(actor.preferred_username).organized_events
Actors.get_local_actor_by_name_with_preload(actor.preferred_username).organized_events
|> hd
|> Map.get(:id)
assert event_found_id == event.id
end
test "get_actor_by_name_with_everything!/1 returns the local actor with it's organized events",
test "get_actor_by_name_with_preload!/1 returns the local actor with it's organized events",
%{
actor: actor
} do
assert Actors.get_actor_by_name_with_everything(actor.preferred_username).organized_events ==
assert Actors.get_actor_by_name_with_preload(actor.preferred_username).organized_events ==
[]
event = insert(:event, organizer_actor: actor)
event_found_id =
Actors.get_actor_by_name_with_everything(actor.preferred_username).organized_events
Actors.get_actor_by_name_with_preload(actor.preferred_username).organized_events
|> hd
|> Map.get(:id)
assert event_found_id == event.id
end
test "get_actor_by_name_with_everything!/1 returns the remote actor with it's organized events" do
test "get_actor_by_name_with_preload!/1 returns the remote actor with it's organized events" do
use_cassette "actors/remote_actor_mastodon_tcit" do
with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(@remote_account_url) do
assert Actors.get_actor_by_name_with_everything(
with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(@remote_account_url) do
assert Actors.get_actor_by_name_with_preload(
"#{actor.preferred_username}@#{actor.domain}"
).organized_events == []
event = insert(:event, organizer_actor: actor)
event_found_id =
Actors.get_actor_by_name_with_everything(
Actors.get_actor_by_name_with_preload(
"#{actor.preferred_username}@#{actor.domain}"
).organized_events
|> hd
@ -166,42 +168,21 @@ defmodule Mobilizon.ActorsTest do
end
end
test "get_or_fetch_by_url/1 returns the local actor for the url", %{
actor: %Actor{preferred_username: preferred_username} = actor
} do
with {:ok, %Actor{domain: domain} = actor} <- Actors.get_or_fetch_by_url(actor.url) do
assert preferred_username == actor.preferred_username
assert is_nil(domain)
end
end
test "get_or_fetch_by_url/1 returns the remote actor for the url" do
use_cassette "actors/remote_actor_mastodon_tcit" do
with {:ok, %Actor{preferred_username: preferred_username, domain: domain}} <-
Actors.get_or_fetch_by_url!(@remote_account_url) do
assert preferred_username == @remote_account_username
assert domain == @remote_account_domain
end
end
end
test "test find_local_by_username/1 returns local actors with similar usernames", %{
test "test get_local_actor_by_username/1 returns local actors with similar usernames", %{
actor: actor
} do
actor2 = insert(:actor, preferred_username: "tcit")
[%Actor{id: actor_found_id} | tail] = Actors.find_local_by_username("tcit")
[%Actor{id: actor_found_id} | tail] = Actors.get_local_actor_by_username("tcit")
%Actor{id: actor2_found_id} = hd(tail)
assert MapSet.new([actor_found_id, actor2_found_id]) == MapSet.new([actor.id, actor2.id])
end
test "test find_and_count_actors_by_username_or_name/4 returns actors with similar usernames",
%{
actor: %Actor{id: actor_id}
} do
test "test build_actors_by_username_or_name_page/4 returns actors with similar usernames",
%{actor: %Actor{id: actor_id}} do
use_cassette "actors/remote_actor_mastodon_tcit" do
with {:ok, %Actor{id: actor2_id}} <- Actors.get_or_fetch_by_url(@remote_account_url) do
%{total: 2, elements: actors} =
Actors.find_and_count_actors_by_username_or_name("tcit", [:Person])
with {:ok, %Actor{id: actor2_id}} <- ActivityPub.get_or_fetch_by_url(@remote_account_url) do
%Page{total: 2, elements: actors} =
Actors.build_actors_by_username_or_name_page("tcit", [:Person])
actors_ids = actors |> Enum.map(& &1.id)
@ -210,35 +191,13 @@ defmodule Mobilizon.ActorsTest do
end
end
test "test find_and_count_actors_by_username_or_name/4 returns actors with similar names" do
test "test build_actors_by_username_or_name_page/4 returns actors with similar names" do
%{total: 0, elements: actors} =
Actors.find_and_count_actors_by_username_or_name("ohno", [:Person])
Actors.build_actors_by_username_or_name_page("ohno", [:Person])
assert actors == []
end
test "test get_public_key_for_url/1 with local actor", %{actor: actor} do
assert Actor.get_public_key_for_url(actor.url) ==
actor.keys |> Mobilizon.Actors.Actor.prepare_public_key()
end
@remote_actor_key {:ok,
{:RSAPublicKey,
20_890_513_599_005_517_665_557_846_902_571_022_168_782_075_040_010_449_365_706_450_877_170_130_373_892_202_874_869_873_999_284_399_697_282_332_064_948_148_602_583_340_776_692_090_472_558_740_998_357_203_838_580_321_412_679_020_304_645_826_371_196_718_081_108_049_114_160_630_664_514_340_729_769_453_281_682_773_898_619_827_376_232_969_899_348_462_205_389_310_883_299_183_817_817_999_273_916_446_620_095_414_233_374_619_948_098_516_821_650_069_821_783_810_210_582_035_456_563_335_930_330_252_551_528_035_801_173_640_288_329_718_719_895_926_309_416_142_129_926_226_047_930_429_802_084_560_488_897_717_417_403_272_782_469_039_131_379_953_278_833_320_195_233_761_955_815_307_522_871_787_339_192_744_439_894_317_730_207_141_881_699_363_391_788_150_650_217_284_777_541_358_381_165_360_697_136_307_663_640_904_621_178_632_289_787,
65_537}}
test "test get_public_key_for_url/1 with remote actor" do
use_cassette "actors/remote_actor_mastodon_tcit" do
assert Actor.get_public_key_for_url(@remote_account_url) == @remote_actor_key
end
end
test "test get_public_key_for_url/1 with remote actor and bad key" do
use_cassette "actors/remote_actor_mastodon_tcit_actor_deleted" do
assert Actor.get_public_key_for_url(@remote_account_url) ==
{:error, :actor_fetch_error}
end
end
test "create_actor/1 with valid data creates a actor" do
assert {:ok, %Actor{} = actor} = Actors.create_actor(@valid_attrs)
assert actor.summary == "some description"
@ -351,10 +310,6 @@ defmodule Mobilizon.ActorsTest do
"/" <> banner_path
)
end
test "change_actor/1 returns a actor changeset", %{actor: actor} do
assert %Ecto.Changeset{} = Actors.change_actor(actor)
end
end
describe "groups" do
@ -505,8 +460,8 @@ defmodule Mobilizon.ActorsTest do
assert {:ok, %Follower{} = follower} = Actors.create_follower(valid_attrs)
assert follower.approved == true
assert %{total: 1, elements: [target_actor]} = Actor.get_followings(actor)
assert %{total: 1, elements: [actor]} = Actor.get_followers(target_actor)
assert %{total: 1, elements: [target_actor]} = Actors.get_followings(actor)
assert %{total: 1, elements: [actor]} = Actors.get_followers(target_actor)
end
test "create_follower/1 with valid data but same actors fails to create a follower", %{
@ -554,33 +509,28 @@ defmodule Mobilizon.ActorsTest do
assert_raise Ecto.NoResultsError, fn -> Actors.get_follower!(follower.id) end
end
test "change_follower/1 returns a follower changeset", context do
follower = create_test_follower(context)
assert %Ecto.Changeset{} = Actors.change_follower(follower)
end
test "follow/3 makes an actor follow another", %{actor: actor, target_actor: target_actor} do
# Preloading followers/followings
actor = Actors.get_actor_with_everything(actor.id)
target_actor = Actors.get_actor_with_everything(target_actor.id)
actor = Actors.get_actor_with_preload(actor.id)
target_actor = Actors.get_actor_with_preload(target_actor.id)
{:ok, follower} = Actor.follow(target_actor, actor)
{:ok, follower} = Actors.follow(target_actor, actor)
assert follower.actor.id == actor.id
# Referesh followers/followings
actor = Actors.get_actor_with_everything(actor.id)
target_actor = Actors.get_actor_with_everything(target_actor.id)
actor = Actors.get_actor_with_preload(actor.id)
target_actor = Actors.get_actor_with_preload(target_actor.id)
assert target_actor.followers |> Enum.map(& &1.actor_id) == [actor.id]
assert actor.followings |> Enum.map(& &1.target_actor_id) == [target_actor.id]
# Test if actor is already following target actor
assert {:error, :already_following, msg} = Actor.follow(target_actor, actor)
assert {:error, :already_following, msg} = Actors.follow(target_actor, actor)
assert msg =~ "already following"
# Test if target actor is suspended
target_actor = %{target_actor | suspended: true}
assert {:error, :suspended, msg} = Actor.follow(target_actor, actor)
assert {:error, :suspended, msg} = Actors.follow(target_actor, actor)
assert msg =~ "suspended"
end
end
@ -620,8 +570,8 @@ defmodule Mobilizon.ActorsTest do
assert {:ok, %Member{} = member} = Actors.create_member(valid_attrs)
assert member.role == :member
assert [group] = Actor.get_groups_member_of(actor)
assert [actor] = Actor.get_members_for_group(group)
assert [group] = Actors.get_groups_member_of(actor)
assert [actor] = Actors.get_members_for_group(group)
end
test "create_member/1 with valid data but same actors fails to create a member", %{
@ -666,10 +616,5 @@ defmodule Mobilizon.ActorsTest do
assert {:ok, %Member{}} = Actors.delete_member(member)
assert_raise Ecto.NoResultsError, fn -> Actors.get_member!(member.id) end
end
test "change_member/1 returns a member changeset", context do
member = create_test_member(context)
assert %Ecto.Changeset{} = Actors.change_member(member)
end
end
end

View File

@ -11,7 +11,6 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors
alias Mobilizon.Service.HTTPSignatures.Signature
alias Mobilizon.Service.ActivityPub
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
@ -48,7 +47,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
test "returns an actor from url" do
use_cassette "activity_pub/fetch_framapiaf.org_users_tcit" do
assert {:ok, %Actor{preferred_username: "tcit", domain: "framapiaf.org"}} =
Actors.get_or_fetch_by_url("https://framapiaf.org/users/tcit")
ActivityPub.get_or_fetch_by_url("https://framapiaf.org/users/tcit")
end
end
end

View File

@ -222,8 +222,8 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert data["type"] == "Follow"
assert data["id"] == "https://social.tcit.fr/users/tcit#follows/2"
actor = Actors.get_actor_with_everything(actor.id)
assert Actor.following?(Actors.get_actor_by_url!(data["actor"], true), actor)
actor = Actors.get_actor_with_preload(actor.id)
assert Actors.following?(Actors.get_actor_by_url!(data["actor"], true), actor)
end
# test "it works for incoming follow requests from hubzilla" do
@ -498,7 +498,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert data["actor"] == "https://social.tcit.fr/users/tcit"
{:ok, followed} = Actors.get_actor_by_url(data["actor"])
refute Actor.following?(followed, actor)
refute Actors.following?(followed, actor)
end
# test "it works for incoming blocks" do
@ -581,10 +581,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
follower = insert(:actor)
followed = insert(:actor)
refute Actor.following?(follower, followed)
refute Actors.following?(follower, followed)
{:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
assert Actor.following?(follower, followed)
assert Actors.following?(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
@ -605,7 +605,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
{:ok, follower} = Actors.get_actor_by_url(follower.url)
assert Actor.following?(follower, followed)
assert Actors.following?(follower, followed)
end
test "it works for incoming accepts which are referenced by IRI only" do
@ -627,7 +627,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
{:ok, follower} = Actors.get_actor_by_url(follower.url)
assert Actor.following?(follower, followed)
assert Actors.following?(follower, followed)
end
test "it fails for incoming accepts which cannot be correlated" do
@ -646,7 +646,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
{:ok, follower} = Actors.get_actor_by_url(follower.url)
refute Actor.following?(follower, followed)
refute Actors.following?(follower, followed)
end
test "it fails for incoming rejects which cannot be correlated" do
@ -665,7 +665,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
{:ok, follower} = Actors.get_actor_by_url(follower.url)
refute Actor.following?(follower, followed)
refute Actors.following?(follower, followed)
end
test "it works for incoming rejects which are referenced by IRI only" do
@ -674,7 +674,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
{:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
assert Actor.following?(follower, followed)
assert Actors.following?(follower, followed)
reject_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
@ -684,7 +684,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
{:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data)
refute Actor.following?(follower, followed)
refute Actors.following?(follower, followed)
end
test "it rejects activities without a valid ID" do

View File

@ -6,6 +6,8 @@ defmodule MobilizonWeb.API.SearchTest do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Storage.Page
alias MobilizonWeb.API.Search
import Mock
@ -13,7 +15,7 @@ defmodule MobilizonWeb.API.SearchTest do
test "search an user by username" do
with_mock ActivityPub,
find_or_make_actor_from_nickname: fn "toto@domain.tld" -> {:ok, %Actor{id: 42}} end do
assert {:ok, %{total: 1, elements: [%Actor{id: 42}]}} ==
assert {:ok, %Page{total: 1, elements: [%Actor{id: 42}]}} ==
Search.search_actors("toto@domain.tld", 1, 10, :Person)
assert_called(ActivityPub.find_or_make_actor_from_nickname("toto@domain.tld"))
@ -23,7 +25,7 @@ defmodule MobilizonWeb.API.SearchTest do
test "search something by URL" do
with_mock ActivityPub,
fetch_object_from_url: fn "https://social.tcit.fr/users/tcit" -> {:ok, %Actor{id: 42}} end do
assert {:ok, %{total: 1, elements: [%Actor{id: 42}]}} ==
assert {:ok, %Page{total: 1, elements: [%Actor{id: 42}]}} ==
Search.search_actors("https://social.tcit.fr/users/tcit", 1, 10, :Person)
assert_called(ActivityPub.fetch_object_from_url("https://social.tcit.fr/users/tcit"))
@ -32,20 +34,20 @@ defmodule MobilizonWeb.API.SearchTest do
test "search actors" do
with_mock Actors,
find_and_count_actors_by_username_or_name: fn "toto", _type, 1, 10 ->
%{total: 1, elements: [%Actor{id: 42}]}
build_actors_by_username_or_name_page: fn "toto", _type, 1, 10 ->
%Page{total: 1, elements: [%Actor{id: 42}]}
end do
assert {:ok, %{total: 1, elements: [%Actor{id: 42}]}} =
Search.search_actors("toto", 1, 10, :Person)
assert_called(Actors.find_and_count_actors_by_username_or_name("toto", [:Person], 1, 10))
assert_called(Actors.build_actors_by_username_or_name_page("toto", [:Person], 1, 10))
end
end
test "search events" do
with_mock Events,
find_and_count_events_by_name: fn "toto", 1, 10 ->
%{total: 1, elements: [%Event{title: "super toto event"}]}
%Page{total: 1, elements: [%Event{title: "super toto event"}]}
end do
assert {:ok, %{total: 1, elements: [%Event{title: "super toto event"}]}} =
Search.search_events("toto", 1, 10)

View File

@ -177,7 +177,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
test "it returns the followers in a collection", %{conn: conn} do
actor = insert(:actor, visibility: :public)
actor2 = insert(:actor)
Actor.follow(actor, actor2)
Actors.follow(actor, actor2)
result =
conn
@ -190,7 +190,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
test "it returns no followers for a private actor", %{conn: conn} do
actor = insert(:actor, visibility: :private)
actor2 = insert(:actor)
Actor.follow(actor, actor2)
Actors.follow(actor, actor2)
result =
conn
@ -205,7 +205,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
Enum.each(1..15, fn _ ->
other_actor = insert(:actor)
Actor.follow(actor, other_actor)
Actors.follow(actor, other_actor)
end)
result =
@ -229,7 +229,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
test "it returns the followings in a collection", %{conn: conn} do
actor = insert(:actor)
actor2 = insert(:actor, visibility: :public)
Actor.follow(actor, actor2)
Actors.follow(actor, actor2)
result =
conn
@ -242,7 +242,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
test "it returns no followings for a private actor", %{conn: conn} do
actor = insert(:actor)
actor2 = insert(:actor, visibility: :private)
Actor.follow(actor, actor2)
Actors.follow(actor, actor2)
result =
conn
@ -257,7 +257,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
Enum.each(1..15, fn _ ->
other_actor = insert(:actor)
Actor.follow(other_actor, actor)
Actors.follow(other_actor, actor)
end)
result =
@ -322,7 +322,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
# Enum.each(1..15, fn _ ->
# actor = Repo.get(Actor, actor.id)
# other_actor = insert(:actor)
# Actor.follow(actor, other_actor)
# Actors.follow(actor, other_actor)
# end)
# result =

View File

@ -1,10 +1,13 @@
defmodule Mobilizon.Factory do
@moduledoc """
Factory for fixtures with ExMachina
Factory for fixtures with ExMachina.
"""
# with Ecto
use ExMachina.Ecto, repo: Mobilizon.Storage.Repo
alias Mobilizon.Actors.Actor
alias Mobilizon.Crypto
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Upload
@ -21,10 +24,6 @@ defmodule Mobilizon.Factory do
end
def actor_factory 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()
preferred_username = sequence("thomas")
%Mobilizon.Actors.Actor{
@ -32,7 +31,7 @@ defmodule Mobilizon.Factory do
domain: nil,
followers: [],
followings: [],
keys: pem,
keys: Crypto.generate_rsa_2048_private_key(),
type: :Person,
avatar: build(:file, name: "Avatar"),
banner: build(:file, name: "Banner"),

View File

@ -22,7 +22,7 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do
{:ok, target_actor} = Actors.get_actor_by_url(target_instance)
refute is_nil(target_actor.domain)
assert Actor.following?(local_actor, target_actor)
assert Actors.following?(local_actor, target_actor)
end
end
end
@ -36,11 +36,11 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do
%Actor{} = local_actor = Relay.get_actor()
{:ok, %Actor{} = target_actor} = Actors.get_actor_by_url(target_instance)
assert %Follower{} = Actor.following?(local_actor, target_actor)
assert %Follower{} = Actors.following?(local_actor, target_actor)
Mix.Tasks.Mobilizon.Relay.run(["unfollow", target_instance])
refute Actor.following?(local_actor, target_actor)
refute Actors.following?(local_actor, target_actor)
end
end
end