Change schema a bit

Closes #29

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2018-11-23 15:03:53 +01:00
parent 403a32e996
commit 9f9113f094
13 changed files with 449 additions and 191 deletions

View File

@@ -8,6 +8,12 @@ defenum(Mobilizon.Actors.ActorTypeEnum, :actor_type, [
:Service
])
defenum(Mobilizon.Actors.ActorOpennesssEnum, :openness, [
:invite_only,
:moderated,
:open
])
defmodule Mobilizon.Actors.Actor do
@moduledoc """
Represents an actor (local and remote actors)
@@ -42,6 +48,7 @@ defmodule Mobilizon.Actors.Actor do
field(:suspended, :boolean, default: false)
field(:avatar_url, :string)
field(:banner_url, :string)
# field(:openness, Mobilizon.Actors.ActorOpennesssEnum, default: :moderated)
many_to_many(:followers, Actor, join_through: Follower)
has_many(:organized_events, Event, foreign_key: :organizer_actor_id)
many_to_many(:memberships, Actor, join_through: Member)

View File

@@ -10,10 +10,12 @@ defmodule Mobilizon.Actors do
alias Mobilizon.Service.ActivityPub
@doc false
def data() do
Dataloader.Ecto.new(Repo, query: &query/2)
end
@doc false
def query(queryable, _params) do
queryable
end
@@ -312,14 +314,51 @@ defmodule Mobilizon.Actors do
if preload, do: Repo.preload(actor, [:followers]), else: actor
end
def get_actor_by_name(name) do
case String.split(name, "@") do
[name] ->
Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)))
@doc """
Get an actor by name
[name, domain] ->
Repo.get_by(Actor, preferred_username: name, domain: domain)
end
## Examples
iex> get_actor_by_name("tcit")
%Mobilizon.Actors.Actor{preferred_username: "tcit", domain: nil}
iex> get_actor_by_name("tcit@social.tcit.fr")
%Mobilizon.Actors.Actor{preferred_username: "tcit", domain: "social.tcit.fr"}
iex> get_actor_by_name("tcit", :Group)
nil
"""
@spec get_actor_by_name(String.t(), atom() | nil) :: Actor.t()
def get_actor_by_name(name, type \\ nil) do
# Base query
query = from(a in Actor)
# If we have Person / Group information
query =
if type in [:Person, :Group] do
from(a in query, where: a.type == ^type)
else
query
end
# If the name is a remote actor
query =
case String.split(name, "@") do
[name] -> do_get_actor_by_name(query, name)
[name, domain] -> do_get_actor_by_name(query, name, domain)
end
Repo.one(query)
end
# Get actor by username and domain is nil
defp do_get_actor_by_name(query, name) do
from(a in query, where: a.preferred_username == ^name and is_nil(a.domain))
end
# Get actor by username and domain
defp do_get_actor_by_name(query, name, domain) do
from(a in query, where: a.preferred_username == ^name and a.domain == ^domain)
end
def get_local_actor_by_name(name) do
@@ -331,17 +370,11 @@ defmodule Mobilizon.Actors do
Repo.preload(actor, :organized_events)
end
def get_actor_by_name_with_everything(name) do
actor =
case String.split(name, "@") do
[name] ->
Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)))
[name, domain] ->
Repo.one(from(a in Actor, where: a.preferred_username == ^name and a.domain == ^domain))
end
Repo.preload(actor, :organized_events)
@spec get_actor_by_name_with_everything(String.t(), atom() | nil) :: Actor.t()
def get_actor_by_name_with_everything(name, type \\ nil) do
name
|> get_actor_by_name(type)
|> Repo.preload(:organized_events)
end
def get_or_fetch_by_url(url, preload \\ false) do
@@ -394,6 +427,7 @@ defmodule Mobilizon.Actors do
@doc """
Find actors by their name or displayed name
"""
@spec find_actors_by_username_or_name(String.t(), integer(), integer()) :: list(Actor.t())
def find_actors_by_username_or_name(username, page \\ 1, limit \\ 10)
def find_actors_by_username_or_name("", _page, _limit), do: []
@@ -418,6 +452,7 @@ defmodule Mobilizon.Actors do
end
@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])?)*$/
@spec search(String.t()) :: {:ok, list(Actor.t())} | {:ok, []} | {:error, any()}
def search(name) do
# find already saved accounts
case find_actors_by_username_or_name(name) do

View File

@@ -23,7 +23,7 @@ defmodule Mobilizon.Events.Tag.TitleSlug do
nil ->
slug
_story ->
_tag ->
slug
|> Mobilizon.Slug.increment_slug()
|> build_unique_slug(changeset)
@@ -51,8 +51,8 @@ defmodule Mobilizon.Events.Tag do
def changeset(%Tag{} = tag, attrs) do
tag
|> cast(attrs, [:title])
|> validate_required([:title])
|> TitleSlug.maybe_generate_slug()
|> validate_required([:title, :slug])
|> TitleSlug.unique_constraint()
end
end

View File

@@ -12,14 +12,40 @@ defmodule MobilizonWeb.Resolvers.Actor do
end
end
@doc """
Find a person
"""
def find_person(_parent, %{preferred_username: name}, _resolution) do
case ActivityPub.find_or_make_person_from_nickname(name) do
{:ok, actor} ->
{:ok, actor}
_ ->
{:error, "Person with name #{name} not found"}
end
end
@doc """
Find a person
"""
def find_group(_parent, %{preferred_username: name}, _resolution) do
case ActivityPub.find_or_make_group_from_nickname(name) do
{:ok, actor} ->
{:ok, actor}
_ ->
{:error, "Group with name #{name} not found"}
end
end
@doc """
Returns the current actor for the currently logged-in user
"""
def get_current_actor(_parent, _args, %{context: %{current_user: user}}) do
def get_current_person(_parent, _args, %{context: %{current_user: user}}) do
{:ok, Actors.get_actor_for_user(user)}
end
def get_current_actor(_parent, _args, _resolution) do
{:error, "You need to be logged-in to view current actor"}
def get_current_person(_parent, _args, _resolution) do
{:error, "You need to be logged-in to view current person"}
end
end

View File

@@ -60,7 +60,7 @@ defmodule MobilizonWeb.Resolvers.User do
Mobilizon.Actors.Service.Activation.check_confirmation_token(token),
%Actor{} = actor <- Actors.get_actor_for_user(user),
{:ok, token, _} <- MobilizonWeb.Guardian.encode_and_sign(user) do
{:ok, %{token: token, user: user, actor: actor}}
{:ok, %{token: token, user: user, person: actor}}
end
end

View File

@@ -3,8 +3,8 @@ defmodule MobilizonWeb.Schema do
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
alias Mobilizon.{Actors, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Events.{Event, Comment, Participant}
import_types(MobilizonWeb.Schema.Custom.UUID)
import_types(Absinthe.Type.Custom)
@@ -12,18 +12,20 @@ defmodule MobilizonWeb.Schema do
alias MobilizonWeb.Resolvers
@desc "An ActivityPub actor"
object :actor do
@desc """
Represents a person identity
"""
object :person do
interfaces([:actor])
field(:user, :user, description: "The user this actor is associated to")
field(:member_of, list_of(:member), description: "The list of groups this person is member of")
field(:url, :string, description: "The ActivityPub actor's URL")
# We probably don't need all of that
# field(:outbox_url, :string, description: "The ActivityPub actor outbox_url")
# field(:inbox_url, :string)
# field(:following_url, :string)
# field(:followers_url, :string)
# field(:shared_inbox_url, :string)
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
field(:name, :string, description: "The actor's displayed name")
field(:domain, :string, description: "The actor's domain if (null if it's this instance)")
field(:local, :boolean, description: "If the actor is from this instance")
field(:summary, :string, description: "The actor's summary")
field(:preferred_username, :string, description: "The actor's preferred username")
field(:keys, :string, description: "The actors RSA Keys")
@@ -35,14 +37,126 @@ defmodule MobilizonWeb.Schema do
field(:suspended, :boolean, description: "If the actor is suspended")
field(:avatar_url, :string, description: "The actor's avatar url")
field(:banner_url, :string, description: "The actor's banner url")
# field(:followers, list_of(:follower))
# These one should have a privacy setting
field(:following, list_of(:follower), description: "List of followings")
field(:followers, list_of(:follower), description: "List of followers")
field(:followersCount, :integer, description: "Number of followers for this actor")
field(:followingCount, :integer, description: "Number of actors following this actor")
# This one should have a privacy setting
field(:organized_events, list_of(:event),
resolve: dataloader(Events),
description: "A list of the events this actor has organized"
)
end
@desc """
Represents a group of actors
"""
object :group do
interfaces([:actor])
field(:url, :string, description: "The ActivityPub actor's URL")
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
field(:name, :string, description: "The actor's displayed name")
field(:domain, :string, description: "The actor's domain if (null if it's this instance)")
field(:local, :boolean, description: "If the actor is from this instance")
field(:summary, :string, description: "The actor's summary")
field(:preferred_username, :string, description: "The actor's preferred username")
field(:keys, :string, description: "The actors RSA Keys")
field(:manually_approves_followers, :boolean,
description: "Whether the actors manually approves followers"
)
field(:suspended, :boolean, description: "If the actor is suspended")
field(:avatar_url, :string, description: "The actor's avatar url")
field(:banner_url, :string, description: "The actor's banner url")
# These one should have a privacy setting
field(:following, list_of(:follower), description: "List of followings")
field(:followers, list_of(:follower), description: "List of followers")
field(:followersCount, :integer, description: "Number of followers for this actor")
field(:followingCount, :integer, description: "Number of actors following this actor")
# This one should have a privacy setting
field(:organized_events, list_of(:event),
resolve: dataloader(Events),
description: "A list of the events this actor has organized"
)
field(:types, :group_type, description: "The type of group : Group, Community,…")
field(:openness, :openness,
description: "Whether the group is opened to all or has restricted access"
)
field(:members, non_null(list_of(:member)), description: "List of group members")
end
@desc """
Describes how an actor is opened to follows
"""
enum :openness do
value(:invite_only, description: "The actor can only be followed by invitation")
value(:moderated, description: "The actor needs to accept the following before it's effective")
value(:open, description: "The actor is open to followings")
end
@desc """
The types of Group that exist
"""
enum :group_type do
value(:group, description: "A private group of persons")
value(:community, description: "A public group of many actors")
end
@desc "An ActivityPub actor"
interface :actor do
field(:url, :string, description: "The ActivityPub actor's URL")
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
field(:name, :string, description: "The actor's displayed name")
field(:domain, :string, description: "The actor's domain if (null if it's this instance)")
field(:local, :boolean, description: "If the actor is from this instance")
field(:summary, :string, description: "The actor's summary")
field(:preferred_username, :string, description: "The actor's preferred username")
field(:keys, :string, description: "The actors RSA Keys")
field(:manually_approves_followers, :boolean,
description: "Whether the actors manually approves followers"
)
field(:suspended, :boolean, description: "If the actor is suspended")
field(:avatar_url, :string, description: "The actor's avatar url")
field(:banner_url, :string, description: "The actor's banner url")
# These one should have a privacy setting
field(:following, list_of(:follower), description: "List of followings")
field(:followers, list_of(:follower), description: "List of followers")
field(:followersCount, :integer, description: "Number of followers for this actor")
field(:followingCount, :integer, description: "Number of actors following this actor")
# This one should have a privacy setting
field(:organized_events, list_of(:event),
resolve: dataloader(Events),
description: "A list of the events this actor has organized"
)
# This one is for the person itself **only**
# field(:feed, list_of(:event), description: "List of events the actor sees in his or her feed")
# field(:memberships, list_of(:member))
field(:user, :user, description: "The user this actor is associated to")
resolve_type(fn
%Actor{type: :Person}, _ ->
:person
%Actor{type: :Group}, _ ->
:group
end)
end
@desc "The list of types an actor can be"
@@ -58,11 +172,12 @@ defmodule MobilizonWeb.Schema do
object :user do
field(:id, non_null(:id), description: "The user's ID")
field(:email, non_null(:string), description: "The user's email")
# , resolve: dataloader(:actors))
field(:actors, non_null(list_of(:actor)),
description: "The user's list of actors (identities)"
field(:profiles, non_null(list_of(:person)),
description: "The user's list of profiles (identities)"
)
# TODO: This shouldn't be an ID, but the actor itself
field(:default_actor_id, non_null(:integer), description: "The user's default actor")
field(:confirmed_at, :datetime,
@@ -86,89 +201,177 @@ defmodule MobilizonWeb.Schema do
@desc "A JWT and the associated user ID"
object :login do
field(:token, non_null(:string))
field(:user, non_null(:user))
field(:actor, non_null(:actor))
field(:token, non_null(:string), description: "A JWT Token for this session")
field(:user, non_null(:user), description: "The user associated to this session")
field(:person, non_null(:person), description: "The person associated to this session")
end
@desc "An event"
object :event do
field(:uuid, :uuid)
field(:url, :string)
field(:local, :boolean)
field(:title, :string)
field(:description, :string)
field(:begins_on, :datetime)
field(:ends_on, :datetime)
field(:state, :integer)
field(:status, :integer)
field(:public, :boolean)
field(:thumbnail, :string)
field(:large_image, :string)
field(:publish_at, :datetime)
field(:address_type, :address_type)
field(:online_address, :string)
field(:phone, :string)
field(:uuid, :uuid, description: "The Event UUID")
field(:url, :string, description: "The ActivityPub Event URL")
field(:local, :boolean, description: "Whether the event is local or not")
field(:title, :string, description: "The event's title")
field(:description, :string, description: "The event's description")
field(:begins_on, :datetime, description: "Datetime for when the event begins")
field(:ends_on, :datetime, description: "Datetime for when the event ends")
field(:state, :integer, description: "State of the event")
field(:status, :integer, description: "Status of the event")
field(:public, :boolean, description: "Whether the event is public or not")
# TODO replace me with picture object
field(:thumbnail, :string, description: "A thumbnail picture for the event")
# TODO replace me with banner
field(:large_image, :string, description: "A large picture for the event")
field(:publish_at, :datetime, description: "When the event was published")
field(:address_type, :address_type, description: "The type of the event's address")
# TODO implement these properly with an interface
# field(:online_address, :string, description: "???")
# field(:phone, :string, description: "")
field :organizer_actor, :actor do
resolve(dataloader(Actors))
end
field(:organizer_actor, :person,
resolve: dataloader(Actors),
description: "The event's organizer (as a person)"
)
field(:attributed_to, :actor)
field(:attributed_to, :actor, description: "Who the event is attributed to (often a group)")
# field(:tags, list_of(:tag))
field(:category, :category)
field(:category, :category, description: "The event's category")
field(:participants, list_of(:participant),
resolve: &Resolvers.Event.list_participants_for_event/3
resolve: &Resolvers.Event.list_participants_for_event/3,
description: "The event's participants"
)
# field(:tracks, list_of(:track))
# field(:sessions, list_of(:session))
# field(:physical_address, :address)
field(:updated_at, :datetime)
field(:created_at, :datetime)
field(:updated_at, :datetime, description: "When the event was last updated")
field(:created_at, :datetime, description: "When the event was created")
end
@desc "A comment"
object :comment do
field(:uuid, :uuid)
field(:url, :string)
field(:local, :boolean)
field(:content, :string)
field(:primaryLanguage, :string)
field(:replies, list_of(:comment))
field(:threadLanguages, non_null(list_of(:string)))
end
@desc "Represents a participant to an event"
object :participant do
# field(:event, :event, resolve: dataloader(Events))
# , resolve: dataloader(Actors)
field(:actor, :actor)
field(:role, :integer)
field(:event, :event,
resolve: dataloader(Events),
description: "The event which the actor participates in"
)
field(:actor, :actor, description: "The actor that participates to the event")
field(:role, :integer, description: "The role of this actor at this event")
end
@desc "The list of types an address can be"
enum :address_type do
value(:physical)
value(:url)
value(:phone)
value(:other)
value(:physical, description: "The address is physical, like a postal address")
value(:url, description: "The address is on the Web, like an URL")
value(:phone, description: "The address is a phone number for a conference")
value(:other, description: "The address is something else")
end
@desc "A category"
object :category do
field(:id, :id)
field(:description, :string)
field(:picture, :picture)
field(:title, :string)
field(:updated_at, :datetime)
field(:created_at, :datetime)
field(:id, :id, description: "The category's ID")
field(:description, :string, description: "The category's description")
field(:picture, :picture, description: "The category's picture")
field(:title, :string, description: "The category's title")
end
@desc "A picture"
object :picture do
field(:url, :string)
field(:url_thumbnail, :string)
field(:url, :string, description: "The URL for this picture")
field(:url_thumbnail, :string, description: "The URL for this picture's thumbnail")
end
@desc """
Represents a notification for an user
"""
object :notification do
field(:id, :integer, description: "The notification ID")
field(:user, :user, description: "The user to transmit the notification to")
field(:actor, :actor, description: "The notification target profile")
field(:activity_type, :integer,
description:
"Whether the notification is about a follow, group join, event change or comment"
)
field(:target_object, :object, description: "The object responsible for the notification")
field(:summary, :string, description: "Text inside the notification")
field(:seen, :boolean, description: "Whether or not the notification was seen by the user")
field(:published, :datetime, description: "Datetime when the notification was published")
end
@desc """
Represents a member of a group
"""
object :member do
field(:parent, :group, description: "Of which the profile is member")
field(:person, :person, description: "Which profile is member of")
field(:role, :integer, description: "The role of this membership")
field(:approved, :boolean, description: "Whether this membership has been approved")
end
@desc """
Represents an actor's follower
"""
object :follower do
field(:target_actor, :actor, description: "What or who the profile follows")
field(:actor, :actor, description: "Which profile follows")
field(:approved, :boolean,
description: "Whether the follow has been approved by the target actor"
)
end
union :object do
types([:event, :person, :group, :comment, :follower, :member, :participant])
resolve_type(fn
%Actor{type: :Person}, _ ->
:person
%Actor{type: :Group}, _ ->
:group
%Event{}, _ ->
:event
%Comment{}, _ ->
:comment
%Follower{}, _ ->
:follower
%Member{}, _ ->
:member
%Participant{}, _ ->
:participant
end)
end
@desc "A search result"
union :search_result do
types([:event, :actor])
types([:event, :person, :group])
resolve_type(fn
%Actor{}, _ ->
:actor
%Actor{type: :Person}, _ ->
:person
%Actor{type: :Group}, _ ->
:group
%Event{}, _ ->
:event
@@ -188,13 +391,16 @@ defmodule MobilizonWeb.Schema do
[Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
end
@desc """
Root Query
"""
query do
@desc "Get all events"
field :events, list_of(:event) do
resolve(&Resolvers.Event.list_events/3)
end
@desc "Search through events and actors"
@desc "Search through events, persons and groups"
field :search, list_of(:search_result) do
arg(:search, non_null(:string))
arg(:page, :integer, default_value: 1)
@@ -226,22 +432,25 @@ defmodule MobilizonWeb.Schema do
end
@desc "Get the current actor for the logged-in user"
field :logged_actor, :actor do
resolve(&Resolvers.Actor.get_current_actor/3)
field :logged_person, :person do
resolve(&Resolvers.Actor.get_current_person/3)
end
@desc "Get an actor"
field :actor, :actor do
@desc "Get a person"
field :person, :person do
arg(:preferred_username, non_null(:string))
resolve(&Resolvers.Actor.find_actor/3)
resolve(&Resolvers.Actor.find_person/3)
end
@desc "Get the list of categories"
field :categories, list_of(:category) do
field :categories, non_null(list_of(:category)) do
resolve(&Resolvers.Category.list_categories/3)
end
end
@desc """
Root Mutation
"""
mutation do
@desc "Create an event"
field :create_event, type: :event do
@@ -273,7 +482,7 @@ defmodule MobilizonWeb.Schema do
end
@desc "Create an user (returns an actor)"
field :create_user, type: :actor do
field :create_user, type: :person do
arg(:email, non_null(:string))
arg(:password, non_null(:string))
arg(:username, non_null(:string))

View File

@@ -219,15 +219,21 @@ defmodule Mobilizon.Service.ActivityPub do
@doc """
Find an actor in our local database or call Webfinger to find what's its AP ID is and then fetch it
"""
@spec find_or_make_actor_from_nickname(String.t()) :: tuple()
def find_or_make_actor_from_nickname(nickname) do
with %Actor{} = actor <- Actors.get_actor_by_name(nickname) do
@spec find_or_make_actor_from_nickname(String.t(), atom() | nil) :: tuple()
def find_or_make_actor_from_nickname(nickname, type \\ nil) do
with %Actor{} = actor <- Actors.get_actor_by_name(nickname, type) do
{:ok, actor}
else
nil -> make_actor_from_nickname(nickname)
end
end
@spec find_or_make_person_from_nickname(String.t()) :: tuple()
def find_or_make_person_from_nickname(nick), do: find_or_make_actor_from_nickname(nick, :Person)
@spec find_or_make_group_from_nickname(String.t()) :: tuple()
def find_or_make_group_from_nickname(nick), do: find_or_make_actor_from_nickname(nick, :Group)
@doc """
Create an actor inside our database from username, using Webfinger to find out it's AP ID and then fetch it
"""