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 defmodule Mobilizon.Actors.Actor do
@moduledoc """ @moduledoc """
Represents an actor (local and remote actors) Represents an actor (local and remote).
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query
alias Mobilizon.Actors alias Mobilizon.{Actors, Config, Crypto}
alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Actors.{Actor, ActorOpenness, ActorType, ActorVisibility, Follower, Member}
alias Mobilizon.Config
alias Mobilizon.Events.{Event, FeedToken} alias Mobilizon.Events.{Event, FeedToken}
alias Mobilizon.Media.File alias Mobilizon.Media.File
alias Mobilizon.Reports.{Report, Note} alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Router.Helpers, as: Routes
@ -46,7 +19,97 @@ defmodule Mobilizon.Actors.Actor do
require Logger 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 schema "actors" do
field(:url, :string) field(:url, :string)
@ -55,187 +118,156 @@ defmodule Mobilizon.Actors.Actor do
field(:following_url, :string) field(:following_url, :string)
field(:followers_url, :string) field(:followers_url, :string)
field(:shared_inbox_url, :string) field(:shared_inbox_url, :string)
field(:type, Mobilizon.Actors.ActorTypeEnum, default: :Person) field(:type, ActorType, default: :Person)
field(:name, :string) field(:name, :string)
field(:domain, :string, default: nil) field(:domain, :string, default: nil)
field(:summary, :string) field(:summary, :string)
field(:preferred_username, :string) field(:preferred_username, :string)
field(:keys, :string) field(:keys, :string)
field(:manually_approves_followers, :boolean, default: false) field(:manually_approves_followers, :boolean, default: false)
field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated) field(:openness, ActorOpenness, default: :moderated)
field(:visibility, Mobilizon.Actors.ActorVisibilityEnum, default: :private) field(:visibility, ActorVisibility, default: :private)
field(:suspended, :boolean, default: false) 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(:followers, Follower, foreign_key: :target_actor_id)
has_many(:followings, Follower, foreign_key: :actor_id) has_many(:followings, Follower, foreign_key: :actor_id)
has_many(:organized_events, Event, foreign_key: :organizer_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) 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(:created_reports, Report, foreign_key: :reporter_id)
has_many(:subject_reports, Report, foreign_key: :reported_id) has_many(:subject_reports, Report, foreign_key: :reported_id)
has_many(:report_notes, Note, foreign_key: :moderator_id) has_many(:report_notes, Note, foreign_key: :moderator_id)
many_to_many(:memberships, Actor, join_through: Member)
timestamps() timestamps()
end 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 @doc false
@spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
def changeset(%Actor{} = actor, attrs) do def changeset(%Actor{} = actor, attrs) do
actor actor
|> Ecto.Changeset.cast(attrs, [ |> cast(attrs, @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
])
|> build_urls() |> build_urls()
|> cast_embed(:avatar) |> cast_embed(:avatar)
|> cast_embed(:banner) |> cast_embed(:banner)
|> unique_username_validator() |> unique_username_validator()
|> validate_required([:preferred_username, :keys, :suspended, :url]) |> validate_required(@required_attrs)
|> 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) |> unique_constraint(:url, name: :actors_url_index)
end end
@doc false @doc false
@spec update_changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
def update_changeset(%Actor{} = actor, attrs) do def update_changeset(%Actor{} = actor, attrs) do
actor actor
|> Ecto.Changeset.cast(attrs, [ |> cast(attrs, @update_attrs)
:name,
:summary,
:keys,
:manually_approves_followers,
:suspended,
:user_id
])
|> cast_embed(:avatar) |> cast_embed(:avatar)
|> cast_embed(:banner) |> cast_embed(:banner)
|> validate_required([:preferred_username, :keys, :suspended, :url]) |> validate_required(@update_required_attrs)
|> 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) |> unique_constraint(:url, name: :actors_url_index)
end end
@doc """ @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 def registration_changeset(%Actor{} = actor, attrs) do
actor actor
|> Ecto.Changeset.cast(attrs, [ |> cast(attrs, @registration_attrs)
:preferred_username,
:domain,
:name,
:summary,
:keys,
:suspended,
:url,
:type,
:user_id
])
|> build_urls() |> build_urls()
|> cast_embed(:avatar) |> cast_embed(:avatar)
|> cast_embed(:banner) |> cast_embed(:banner)
# Needed because following constraint can't work for domain null values (local)
|> unique_username_validator() |> 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) |> unique_constraint(:url, name: :actors_url_index)
|> validate_required([:preferred_username, :keys, :suspended, :url, :type]) |> validate_required(@registration_required_attrs)
end 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 """ @doc """
Changeset for remote actor creation Changeset for remote actor creation.
""" """
@spec remote_actor_creation(map()) :: Ecto.Changeset.t() @spec remote_actor_creation_changeset(map) :: Ecto.Changeset.t()
def remote_actor_creation(params) do def remote_actor_creation_changeset(attrs) do
changes = changeset =
%Actor{} %Actor{}
|> Ecto.Changeset.cast(params, [ |> cast(attrs, @remote_actor_creation_attrs)
:url, |> validate_required(@remote_actor_creation_required_attrs)
: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_embed(:avatar) |> cast_embed(:avatar)
|> cast_embed(:banner) |> cast_embed(:banner)
# Needed because following constraint can't work for domain null values (local)
|> unique_username_validator() |> 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) |> unique_constraint(:url, name: :actors_url_index)
|> validate_length(:summary, max: 5000) |> validate_length(:summary, max: 5000)
|> validate_length(:preferred_username, max: 100) |> validate_length(:preferred_username, max: 100)
Logger.debug("Remote actor creation") Logger.debug("Remote actor creation: #{inspect(changeset)}")
Logger.debug(inspect(changes))
changes changeset
end end
def relay_creation(%{url: url, preferred_username: preferred_username} = _params) do @doc """
key = :public_key.generate_key({:rsa, 2048, 65_537}) Changeset for relay creation.
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) """
pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing() @spec relay_creation_changeset(map) :: Ecto.Changeset.t()
def relay_creation_changeset(attrs) do
relay_creation_attrs = build_relay_creation_attrs(attrs)
vars = %{ cast(%Actor{}, relay_creation_attrs, @relay_creation_attrs)
"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
])
end end
@doc """ @doc """
@ -244,68 +276,48 @@ defmodule Mobilizon.Actors.Actor do
@spec group_creation(struct(), map()) :: Ecto.Changeset.t() @spec group_creation(struct(), map()) :: Ecto.Changeset.t()
def group_creation(%Actor{} = actor, params) do def group_creation(%Actor{} = actor, params) do
actor actor
|> Ecto.Changeset.cast(params, [ |> cast(params, @group_creation_attrs)
:url,
:outbox_url,
:inbox_url,
:shared_inbox_url,
:type,
:name,
:domain,
:summary,
:preferred_username
])
|> cast_embed(:avatar) |> cast_embed(:avatar)
|> cast_embed(:banner) |> cast_embed(:banner)
|> build_urls(:Group) |> build_urls(:Group)
|> put_change(:domain, nil) |> put_change(:domain, nil)
|> put_change(:keys, Actors.create_keys()) |> put_change(:keys, Crypto.generate_rsa_2048_private_key())
|> put_change(:type, :Group) |> put_change(:type, :Group)
|> unique_username_validator() |> unique_username_validator()
|> validate_required([:url, :outbox_url, :inbox_url, :type, :preferred_username]) |> validate_required(@group_creation_required_attrs)
|> 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) |> unique_constraint(:url, name: :actors_url_index)
|> validate_length(:summary, max: 5000) |> validate_length(:summary, max: 5000)
|> validate_length(:preferred_username, max: 100) |> validate_length(:preferred_username, max: 100)
end 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( defp unique_username_validator(
%Ecto.Changeset{changes: %{preferred_username: username} = changes} = changeset %Ecto.Changeset{changes: %{preferred_username: username} = changes} = changeset
) do ) do
with nil <- Map.get(changes, :domain, nil), with nil <- Map.get(changes, :domain, nil),
%Actor{preferred_username: _username} <- Actors.get_local_actor_by_name(username) do %Actor{preferred_username: _} <- Actors.get_local_actor_by_name(username) do
changeset |> add_error(:preferred_username, "Username is already taken") add_error(changeset, :preferred_username, "Username is already taken")
else else
_ -> changeset _ -> changeset
end end
end end
# When we don't even have any preferred_username, don't even try validating preferred_username # When we don't even have any preferred_username, don't even try validating preferred_username
defp unique_username_validator(changeset) do defp unique_username_validator(changeset), do: changeset
changeset
end
@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(changeset, type \\ :Person)
defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, _type) do defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, _type) do
changeset changeset
|> put_change( |> put_change(:outbox_url, build_url(username, :outbox))
:outbox_url, |> put_change(:followers_url, build_url(username, :followers))
build_url(username, :outbox) |> put_change(:following_url, build_url(username, :following))
) |> put_change(:inbox_url, build_url(username, :inbox))
|> 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(:shared_inbox_url, "#{MobilizonWeb.Endpoint.url()}/inbox")
|> put_change(:url, build_url(username, :page)) |> put_change(:url, build_url(username, :page))
end end
@ -313,19 +325,19 @@ defmodule Mobilizon.Actors.Actor do
defp build_urls(%Ecto.Changeset{} = changeset, _type), do: changeset defp build_urls(%Ecto.Changeset{} = changeset, _type), do: changeset
@doc """ @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(preferred_username, endpoint, args \\ [])
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
def build_url(preferred_username, :page, args) do def build_url(preferred_username, :page, args) do
Endpoint Endpoint
|> Routes.page_url(:actor, preferred_username, args) |> Routes.page_url(:actor, preferred_username, args)
|> URI.decode() |> URI.decode()
end end
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
def build_url(preferred_username, endpoint, args) def build_url(preferred_username, endpoint, args)
when endpoint in [:outbox, :following, :followers] do when endpoint in [:outbox, :following, :followers] do
Endpoint Endpoint
@ -333,267 +345,35 @@ defmodule Mobilizon.Actors.Actor do
|> URI.decode() |> URI.decode()
end 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 """ @doc """
Clear multiple caches for an actor Clear multiple caches for an actor
""" """
# TODO: move to MobilizonWeb
@spec clear_cache(struct()) :: {:ok, true} @spec clear_cache(struct()) :: {:ok, true}
def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do
Cachex.del(:activity_pub, "actor_" <> preferred_username) Cachex.del(:activity_pub, "actor_" <> preferred_username)
Cachex.del(:feed, "actor_" <> preferred_username) Cachex.del(:feed, "actor_" <> preferred_username)
Cachex.del(:ics, "actor_" <> preferred_username) Cachex.del(:ics, "actor_" <> preferred_username)
end 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 end

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,30 @@
defmodule Mobilizon.Actors.Bot do defmodule Mobilizon.Actors.Bot do
@moduledoc """ @moduledoc """
Represents a local bot Represents a local bot.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User 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 schema "bots" do
field(:source, :string) field(:source, :string)
field(:type, :string, default: :ics) field(:type, :string, default: :ics)
belongs_to(:actor, Actor) belongs_to(:actor, Actor)
belongs_to(:user, User) belongs_to(:user, User)
@ -17,9 +32,10 @@ defmodule Mobilizon.Actors.Bot do
end end
@doc false @doc false
@spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
def changeset(bot, attrs) do def changeset(bot, attrs) do
bot bot
|> cast(attrs, [:source, :type, :actor_id, :user_id]) |> cast(attrs, @attrs)
|> validate_required([:source]) |> validate_required(@required_attrs)
end end
end end

View File

@ -1,52 +1,63 @@
defmodule Mobilizon.Actors.Follower do defmodule Mobilizon.Actors.Follower do
@moduledoc """ @moduledoc """
Represents the following of an actor to another actor Represents the following of an actor to another actor.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Actors.Follower
alias Mobilizon.Actors.Actor 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 schema "followers" do
field(:approved, :boolean, default: false) field(:approved, :boolean, default: false)
field(:url, :string) field(:url, :string)
belongs_to(:target_actor, Actor) belongs_to(:target_actor, Actor)
belongs_to(:actor, Actor) belongs_to(:actor, Actor)
end end
@doc false @doc false
def changeset(%Follower{} = member, attrs) do @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
member def changeset(follower, attrs) do
|> cast(attrs, [:url, :approved, :target_actor_id, :actor_id]) follower
|> generate_url() |> cast(attrs, @attrs)
|> validate_required([:url, :approved, :target_actor_id, :actor_id]) |> ensure_url()
|> unique_constraint(:target_actor_id, name: :followers_actor_target_actor_unique_index) |> validate_required(@required_attrs)
|> unique_constraint(:target_actor_id,
name: :followers_actor_target_actor_unique_index
)
end end
# If there's a blank URL that's because we're doing the first insert # 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 case fetch_change(changeset, :url) do
{:ok, _url} -> changeset {:ok, _url} -> changeset
:error -> do_generate_url(changeset) :error -> generate_url(changeset)
end end
end end
# Most time just go with the given URL # 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() uuid = Ecto.UUID.generate()
changeset changeset
|> put_change( |> put_change(:id, uuid)
:url, |> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}")
"#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}"
)
|> put_change(
:id,
uuid
)
end end
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 defmodule Mobilizon.Actors.Member do
@moduledoc """ @moduledoc """
Represents the membership of an actor to a group Represents the membership of an actor to a group.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member, MemberRole}
alias Mobilizon.Storage.{Page, Repo}
@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 schema "members" do
field(:role, Mobilizon.Actors.MemberRoleEnum, default: :member) field(:role, MemberRole, default: :member)
belongs_to(:parent, Actor) belongs_to(:parent, Actor)
belongs_to(:actor, Actor) belongs_to(:actor, Actor)
timestamps() timestamps()
end 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 """ @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 @spec get_default_member_role(Actor.t()) :: atom
case Repo.get_by(Member, actor_id: actor_id, parent_id: parent_id) do def get_default_member_role(%Actor{openness: :open}), do: :member
nil -> {:error, :member_not_found} def get_default_member_role(%Actor{}), do: :not_approved
member -> {:ok, member}
end
end
@doc """ @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, openness: :invite_only}), do: false
def can_be_joined(%Actor{type: :Group}), do: true def can_be_joined(%Actor{type: :Group}), do: true
@doc """ @doc """
Returns the list of administrator members for a group. Checks whether the member is an administrator (admin or creator) of the 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
""" """
def is_administrator(%Member{role: :administrator}), do: {:is_admin, true} def is_administrator(%Member{role: :administrator}), do: {:is_admin, true}
def is_administrator(%Member{role: :creator}), do: {:is_admin, true} def is_administrator(%Member{role: :creator}), do: {:is_admin, true}
def is_administrator(%Member{}), do: {:is_admin, false} 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 end

View File

@ -18,12 +18,6 @@ defmodule Mobilizon.Addresses do
@spec query(Ecto.Query.t(), map) :: Ecto.Query.t() @spec query(Ecto.Query.t(), map) :: Ecto.Query.t()
def query(queryable, _params), do: queryable 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 """ @doc """
Gets a single address. Gets a single address.
""" """
@ -72,6 +66,12 @@ defmodule Mobilizon.Addresses do
@spec delete_address(Address.t()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()} @spec delete_address(Address.t()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def delete_address(%Address{} = address), do: Repo.delete(address) 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 """ @doc """
Searches addresses. Searches addresses.

View File

@ -8,16 +8,6 @@ defmodule Mobilizon.Admin do
alias Mobilizon.Admin.ActionLog alias Mobilizon.Admin.ActionLog
alias Mobilizon.Storage.{Page, Repo} 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 """ @doc """
Creates a action_log. Creates a action_log.
""" """
@ -28,6 +18,16 @@ defmodule Mobilizon.Admin do
|> Repo.insert() |> Repo.insert()
end 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() @spec list_action_logs_query :: Ecto.Query.t()
defp list_action_logs_query do defp list_action_logs_query do
from(r in ActionLog, preload: [:actor]) from(r in ActionLog, preload: [:actor])

View File

@ -12,4 +12,17 @@ defmodule Mobilizon.Crypto do
|> :crypto.strong_rand_bytes() |> :crypto.strong_rand_bytes()
|> Base.url_encode64() |> Base.url_encode64()
end 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 end

View File

@ -21,17 +21,6 @@ defmodule Mobilizon.Reports do
@spec query(Ecto.Query.t(), map) :: Ecto.Query.t() @spec query(Ecto.Query.t(), map) :: Ecto.Query.t()
def query(queryable, _params), do: queryable 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 """ @doc """
Gets a single report. Gets a single report.
""" """
@ -90,17 +79,16 @@ defmodule Mobilizon.Reports do
Deletes a report. Deletes a report.
""" """
@spec delete_report(Report.t()) :: {:ok, Report.t()} | {:error, Ecto.Changeset.t()} @spec delete_report(Report.t()) :: {:ok, Report.t()} | {:error, Ecto.Changeset.t()}
def delete_report(%Report{} = report) do def delete_report(%Report{} = report), do: Repo.delete(report)
Repo.delete(report)
end
@doc """ @doc """
Returns the list of notes for a report. Returns the list of reports.
""" """
@spec list_notes_for_report(Report.t()) :: [Note.t()] @spec list_reports(integer | nil, integer | nil, atom, atom) :: [Report.t()]
def list_notes_for_report(%Report{id: report_id}) do def list_reports(page \\ nil, limit \\ nil, sort \\ :updated_at, direction \\ :asc) do
report_id list_reports_query()
|> list_notes_for_report_query() |> Page.paginate(page, limit)
|> sort(sort, direction)
|> Repo.all() |> Repo.all()
end end
@ -134,8 +122,21 @@ defmodule Mobilizon.Reports do
Deletes a note. Deletes a note.
""" """
@spec delete_note(Note.t()) :: {:ok, Note.t()} | {:error, Ecto.Changeset.t()} @spec delete_note(Note.t()) :: {:ok, Note.t()} | {:error, Ecto.Changeset.t()}
def delete_note(%Note{} = note) do def delete_note(%Note{} = note), do: Repo.delete(note)
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 end
@spec list_reports_query :: Ecto.Query.t() @spec list_reports_query :: Ecto.Query.t()
@ -146,11 +147,6 @@ defmodule Mobilizon.Reports do
) )
end 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() @spec list_notes_for_report_query(integer | String.t()) :: Ecto.Query.t()
defp list_notes_for_report_query(report_id) do defp list_notes_for_report_query(report_id) do
from( from(

View File

@ -101,9 +101,7 @@ defmodule Mobilizon.Users do
Deletes an user. Deletes an user.
""" """
@spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} @spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def delete_user(%User{} = user) do def delete_user(%User{} = user), do: Repo.delete(user)
Repo.delete(user)
end
@doc """ @doc """
Get an user with its actors Get an user with its actors
@ -219,9 +217,7 @@ defmodule Mobilizon.Users do
Counts users. Counts users.
""" """
@spec count_users :: integer @spec count_users :: integer
def count_users do def count_users, do: Repo.one(from(u in User, select: count(u.id)))
Repo.one(from(u in User, select: count(u.id)))
end
@doc """ @doc """
Authenticate an user. Authenticate an user.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -91,7 +91,7 @@ defmodule MobilizonWeb.Resolvers.Report do
when is_moderator(role) do when is_moderator(role) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id), with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id),
%Report{} = report <- Reports.get_report(report_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} <- {:ok, %Note{} = note} <-
MobilizonWeb.API.Reports.create_report_note(report, moderator, content) do MobilizonWeb.API.Reports.create_report_note(report, moderator, content) do
{:ok, note} {:ok, note}
@ -106,7 +106,7 @@ defmodule MobilizonWeb.Resolvers.Report do
when is_moderator(role) do when is_moderator(role) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id), with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id),
%Note{} = note <- Reports.get_note(note_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} <- {:ok, %Note{} = note} <-
MobilizonWeb.API.Reports.delete_report_note(note, moderator) do MobilizonWeb.API.Reports.delete_report_note(note, moderator) do
{:ok, %{id: note.id}} {:ok, %{id: note.id}}

View File

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

View File

@ -113,6 +113,29 @@ defmodule Mobilizon.Service.ActivityPub do
end end
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 """ @doc """
Create an activity of type "Create" 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 def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{url: follow_url}} <- with {:ok, %Follower{url: follow_url}} <-
Actor.follow(followed, follower, activity_id, false), Actors.follow(followed, follower, activity_id, false),
activity_follow_id <- activity_follow_id <-
activity_id || follow_url, activity_id || follow_url,
data <- make_follow_data(followed, follower, activity_follow_id), 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() @spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do 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 # We recreate the follow activity
data <- data <-
make_follow_data( make_follow_data(
@ -466,7 +489,7 @@ defmodule Mobilizon.Service.ActivityPub do
def make_actor_from_url(url, preload \\ false) do def make_actor_from_url(url, preload \\ false) do
case fetch_and_prepare_actor_from_url(url) do case fetch_and_prepare_actor_from_url(url) do
{:ok, data} -> {:ok, data} ->
Actors.insert_or_update_actor(data, preload) Actors.upsert_actor(data, preload)
# Request returned 410 # Request returned 410
{:error, :actor_deleted} -> {:error, :actor_deleted} ->
@ -529,7 +552,7 @@ defmodule Mobilizon.Service.ActivityPub do
followers = followers =
if actor.followers_url in activity.recipients do if actor.followers_url in activity.recipients do
Actor.get_full_external_followers(actor) Actors.get_full_external_followers(actor)
else else
[] []
end 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 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.Actors.Actor
alias Mobilizon.Events.Comment, as: CommentModel alias Mobilizon.Events.Comment, as: CommentModel
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
@ -20,7 +19,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
@impl Converter @impl Converter
@spec as_to_model_data(map()) :: map() @spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do 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("Inserting full comment")
Logger.debug(inspect(object)) Logger.debug(inspect(object))

View File

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

View File

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

View File

@ -44,7 +44,7 @@ defmodule Mobilizon.Service.Export.Feed do
@spec fetch_actor_event_feed(String.t()) :: String.t() @spec fetch_actor_event_feed(String.t()) :: String.t()
defp fetch_actor_event_feed(name) do defp fetch_actor_event_feed(name) do
with %Actor{} = actor <- Actors.get_local_actor_by_name(name), 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, events, _count} <- Events.get_public_events_for_actor(actor) do
{:ok, build_actor_feed(actor, events)} {:ok, build_actor_feed(actor, events)}
else else

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -222,8 +222,8 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert data["type"] == "Follow" assert data["type"] == "Follow"
assert data["id"] == "https://social.tcit.fr/users/tcit#follows/2" assert data["id"] == "https://social.tcit.fr/users/tcit#follows/2"
actor = Actors.get_actor_with_everything(actor.id) actor = Actors.get_actor_with_preload(actor.id)
assert Actor.following?(Actors.get_actor_by_url!(data["actor"], true), actor) assert Actors.following?(Actors.get_actor_by_url!(data["actor"], true), actor)
end end
# test "it works for incoming follow requests from hubzilla" do # 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" assert data["actor"] == "https://social.tcit.fr/users/tcit"
{:ok, followed} = Actors.get_actor_by_url(data["actor"]) {:ok, followed} = Actors.get_actor_by_url(data["actor"])
refute Actor.following?(followed, actor) refute Actors.following?(followed, actor)
end end
# test "it works for incoming blocks" do # test "it works for incoming blocks" do
@ -581,10 +581,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
follower = insert(:actor) follower = insert(:actor)
followed = insert(:actor) followed = insert(:actor)
refute Actor.following?(follower, followed) refute Actors.following?(follower, followed)
{:ok, follow_activity, _} = ActivityPub.follow(follower, followed) {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
assert Actor.following?(follower, followed) assert Actors.following?(follower, followed)
accept_data = accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json") 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) {:ok, follower} = Actors.get_actor_by_url(follower.url)
assert Actor.following?(follower, followed) assert Actors.following?(follower, followed)
end end
test "it works for incoming accepts which are referenced by IRI only" do 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) {:ok, follower} = Actors.get_actor_by_url(follower.url)
assert Actor.following?(follower, followed) assert Actors.following?(follower, followed)
end end
test "it fails for incoming accepts which cannot be correlated" do 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) {:ok, follower} = Actors.get_actor_by_url(follower.url)
refute Actor.following?(follower, followed) refute Actors.following?(follower, followed)
end end
test "it fails for incoming rejects which cannot be correlated" do 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) {:ok, follower} = Actors.get_actor_by_url(follower.url)
refute Actor.following?(follower, followed) refute Actors.following?(follower, followed)
end end
test "it works for incoming rejects which are referenced by IRI only" do 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) {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
assert Actor.following?(follower, followed) assert Actors.following?(follower, followed)
reject_data = reject_data =
File.read!("test/fixtures/mastodon-reject-activity.json") 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) {:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data)
refute Actor.following?(follower, followed) refute Actors.following?(follower, followed)
end end
test "it rejects activities without a valid ID" do 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
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Storage.Page
alias MobilizonWeb.API.Search alias MobilizonWeb.API.Search
import Mock import Mock
@ -13,7 +15,7 @@ defmodule MobilizonWeb.API.SearchTest do
test "search an user by username" do test "search an user by username" do
with_mock ActivityPub, with_mock ActivityPub,
find_or_make_actor_from_nickname: fn "toto@domain.tld" -> {:ok, %Actor{id: 42}} end do 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) Search.search_actors("toto@domain.tld", 1, 10, :Person)
assert_called(ActivityPub.find_or_make_actor_from_nickname("toto@domain.tld")) 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 test "search something by URL" do
with_mock ActivityPub, with_mock ActivityPub,
fetch_object_from_url: fn "https://social.tcit.fr/users/tcit" -> {:ok, %Actor{id: 42}} end do 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) 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")) 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 test "search actors" do
with_mock Actors, with_mock Actors,
find_and_count_actors_by_username_or_name: fn "toto", _type, 1, 10 -> build_actors_by_username_or_name_page: fn "toto", _type, 1, 10 ->
%{total: 1, elements: [%Actor{id: 42}]} %Page{total: 1, elements: [%Actor{id: 42}]}
end do end do
assert {:ok, %{total: 1, elements: [%Actor{id: 42}]}} = assert {:ok, %{total: 1, elements: [%Actor{id: 42}]}} =
Search.search_actors("toto", 1, 10, :Person) 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
end end
test "search events" do test "search events" do
with_mock Events, with_mock Events,
find_and_count_events_by_name: fn "toto", 1, 10 -> 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 end do
assert {:ok, %{total: 1, elements: [%Event{title: "super toto event"}]}} = assert {:ok, %{total: 1, elements: [%Event{title: "super toto event"}]}} =
Search.search_events("toto", 1, 10) 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 test "it returns the followers in a collection", %{conn: conn} do
actor = insert(:actor, visibility: :public) actor = insert(:actor, visibility: :public)
actor2 = insert(:actor) actor2 = insert(:actor)
Actor.follow(actor, actor2) Actors.follow(actor, actor2)
result = result =
conn conn
@ -190,7 +190,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
test "it returns no followers for a private actor", %{conn: conn} do test "it returns no followers for a private actor", %{conn: conn} do
actor = insert(:actor, visibility: :private) actor = insert(:actor, visibility: :private)
actor2 = insert(:actor) actor2 = insert(:actor)
Actor.follow(actor, actor2) Actors.follow(actor, actor2)
result = result =
conn conn
@ -205,7 +205,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
Enum.each(1..15, fn _ -> Enum.each(1..15, fn _ ->
other_actor = insert(:actor) other_actor = insert(:actor)
Actor.follow(actor, other_actor) Actors.follow(actor, other_actor)
end) end)
result = result =
@ -229,7 +229,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
test "it returns the followings in a collection", %{conn: conn} do test "it returns the followings in a collection", %{conn: conn} do
actor = insert(:actor) actor = insert(:actor)
actor2 = insert(:actor, visibility: :public) actor2 = insert(:actor, visibility: :public)
Actor.follow(actor, actor2) Actors.follow(actor, actor2)
result = result =
conn conn
@ -242,7 +242,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
test "it returns no followings for a private actor", %{conn: conn} do test "it returns no followings for a private actor", %{conn: conn} do
actor = insert(:actor) actor = insert(:actor)
actor2 = insert(:actor, visibility: :private) actor2 = insert(:actor, visibility: :private)
Actor.follow(actor, actor2) Actors.follow(actor, actor2)
result = result =
conn conn
@ -257,7 +257,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
Enum.each(1..15, fn _ -> Enum.each(1..15, fn _ ->
other_actor = insert(:actor) other_actor = insert(:actor)
Actor.follow(other_actor, actor) Actors.follow(other_actor, actor)
end) end)
result = result =
@ -322,7 +322,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
# Enum.each(1..15, fn _ -> # Enum.each(1..15, fn _ ->
# actor = Repo.get(Actor, actor.id) # actor = Repo.get(Actor, actor.id)
# other_actor = insert(:actor) # other_actor = insert(:actor)
# Actor.follow(actor, other_actor) # Actors.follow(actor, other_actor)
# end) # end)
# result = # result =

View File

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