Commit 68242434 authored by Alex Castaño's avatar Alex Castaño

Merge branch 'release/0.0.8'

parents 846ce0ca 2eedea1d
......@@ -29,9 +29,19 @@ build_with_cache: ## Build the Docker image
-t moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) .
@echo moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD)
tag_latest:
tag:
@echo docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:latest
docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:latest
@docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:latest
@echo docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:$(APP_VSN)
@docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:$(APP_VSN)
push:
@echo docker push moodlenet/moodlenet:latest
@docker push moodlenet/moodlenet:latest
@echo docker push moodlenet/moodlenet:$(APP_VSN)
@docker push moodlenet/moodlenet:$(APP_VSN)
@echo docker push moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD)
@docker push moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD)
run: ## Run the app in Docker
docker run\
......
......@@ -41,5 +41,8 @@ defmodule ActivityPub.ObjectAspect do
# adding because it is easier
assoc(:likers)
field(:likers_count, :integer, autogenerated: true)
field(:followed, :boolean, virtual: true)
field(:liked, :boolean, virtual: true)
end
end
......@@ -5,7 +5,8 @@ defmodule ActivityPub.Field do
functional: true,
type: nil,
default: nil,
autogenerated: false
autogenerated: false,
virtual: false
def build(opts) do
opts = add_default_value(opts)
......
......@@ -52,7 +52,9 @@ defmodule ActivityPub.SQL.Query do
def get_by_id(id, opts \\ []) when is_binary(id) do
case UrlBuilder.get_local_id(id) do
{:ok, local_id} -> get_by_local_id(local_id, opts)
{:ok, local_id} ->
get_by_local_id(local_id, opts)
:error ->
new()
|> where(id: id)
......
defmodule ActivityPub.SQLAspect do
alias ActivityPub.{SQLObjectAspect, SQLActorAspect, SQLActivityAspect, SQLCollectionAspect, SQLResourceAspect}
alias ActivityPub.{
SQLObjectAspect,
SQLActorAspect,
SQLActivityAspect,
SQLCollectionAspect,
SQLResourceAspect
}
alias ActivityPub.SQL.Associations.{ManyToMany, BelongsTo, Collection}
def all(), do: [SQLObjectAspect, SQLActorAspect, SQLActivityAspect, SQLCollectionAspect, SQLResourceAspect]
def all(),
do: [
SQLObjectAspect,
SQLActorAspect,
SQLActivityAspect,
SQLCollectionAspect,
SQLResourceAspect
]
# FIXME make this similar to aspect where the user can redifine
# assocs and fields to be persisted in another way than the default!
......@@ -106,7 +119,12 @@ defmodule ActivityPub.SQLAspect do
type = if field_def.functional, do: field_def.type, else: {:array, field_def.type}
field(name, type)
opts =
field_def
|> Map.take([:virtual, :default])
|> Keyword.new()
field(name, type, opts)
end
end
end
......
......@@ -2,6 +2,8 @@ defmodule MoodleNet do
import ActivityPub.Guards
alias ActivityPub.SQL.Query
alias MoodleNet.Policy
def list_communities(opts \\ %{}) do
Query.new()
|> Query.with_type("MoodleNet:Community")
......@@ -45,6 +47,24 @@ defmodule MoodleNet do
list_resources(ActivityPub.Entity.local_id(entity), opts)
end
def list_threads(context_id, opts \\ %{}) do
Query.new()
|> Query.with_type("Note")
|> Query.has(:context, context_id)
|> has_no_replies()
|> Query.paginate(opts)
|> Query.all()
end
defp has_no_replies(query) do
import Ecto.Query, only: [from: 2]
from([entity: entity] in query,
left_join: rel in fragment("activity_pub_object_in_reply_tos"),
on: entity.local_id == rel.subject_id,
where: is_nil(rel.target_id)
)
end
def list_comments(context_id, opts \\ %{}) do
Query.new()
|> Query.with_type("Note")
......@@ -61,11 +81,13 @@ defmodule MoodleNet do
|> Query.all()
end
def create_community(attrs) do
def create_community(actor, attrs) do
attrs = Map.put(attrs, "type", "MoodleNet:Community")
with {:ok, entity} <- ActivityPub.new(attrs) do
ActivityPub.insert(entity)
with {:ok, entity} <- ActivityPub.new(attrs),
{:ok, entity} <- ActivityPub.insert(entity),
{:ok, true} <- MoodleNet.join_community(actor, entity) do
{:ok, entity}
end
end
......@@ -121,14 +143,18 @@ defmodule MoodleNet do
:ok
end
def create_collection(community, attrs) when has_type(community, "MoodleNet:Community") do
def create_collection(actor, community, attrs)
when has_type(community, "MoodleNet:Community") do
attrs =
attrs
|> Map.put(:type, "MoodleNet:Collection")
|> Map.put(:attributed_to, [community])
with {:ok, entity} <- ActivityPub.new(attrs) do
ActivityPub.insert(entity)
with :ok <- Policy.create_collection?(actor, community, attrs),
{:ok, entity} <- ActivityPub.new(attrs),
{:ok, entity} <- ActivityPub.insert(entity),
{:ok, true} <- follow_collection(actor, entity) do
{:ok, entity}
end
end
......@@ -158,14 +184,17 @@ defmodule MoodleNet do
:ok
end
def create_resource(_actor, collection, attrs)
def create_resource(actor, collection, attrs)
when has_type(collection, "MoodleNet:Collection") do
attrs =
attrs
|> Map.put(:type, "MoodleNet:EducationalResource")
|> Map.put(:attributed_to, [collection])
with {:ok, entity} <- ActivityPub.new(attrs) do
collection = Query.preload_assoc(collection, :attributed_to)
with :ok <- Policy.create_resource?(actor, collection, attrs),
{:ok, entity} <- ActivityPub.new(attrs) do
ActivityPub.insert(entity)
end
end
......@@ -186,9 +215,10 @@ defmodule MoodleNet do
end
def copy_resource(actor, resource, collection) do
resource = resource
|> Query.preload_aspect(:resource)
|> Query.preload_assoc([:icon])
resource =
resource
|> Query.preload_aspect(:resource)
|> Query.preload_assoc([:icon])
attrs =
Map.take(resource, [
......@@ -210,6 +240,7 @@ defmodule MoodleNet do
:time_required,
:typical_age_range
])
url = get_in(resource, [:icon, Access.at(0), :url])
attrs = Map.put(attrs, :icon, %{type: "Image", url: url})
create_resource(actor, collection, attrs)
......@@ -218,6 +249,8 @@ defmodule MoodleNet do
def create_thread(author, context, attrs)
when has_type(author, "Person") and has_type(context, "MoodleNet:Community")
when has_type(author, "Person") and has_type(context, "MoodleNet:Collection") do
context = preload_community(context)
attrs
|> Map.put(:context, [context])
|> Map.put(:attributed_to, [author])
......@@ -226,7 +259,11 @@ defmodule MoodleNet do
def create_reply(author, in_reply_to, attrs)
when has_type(author, "Person") and has_type(in_reply_to, "Note") do
context = Query.new() |> Query.belongs_to(:context, in_reply_to) |> Query.one()
context =
Query.new()
|> Query.belongs_to(:context, in_reply_to)
|> Query.one()
|> preload_community()
attrs
|> Map.put(:context, [context])
......@@ -237,8 +274,11 @@ defmodule MoodleNet do
defp create_comment(attrs) do
attrs = attrs |> Map.put("type", "Note")
[context] = attrs[:context]
[actor] = attrs[:attributed_to]
with {:ok, entity} <- ActivityPub.new(attrs) do
with :ok <- Policy.create_comment?(actor, context, attrs),
{:ok, entity} <- ActivityPub.new(attrs) do
ActivityPub.insert(entity)
end
end
......@@ -251,8 +291,19 @@ defmodule MoodleNet do
end
end
def follow(follower, following) do
params = %{type: "Follow", actor: follower, object: following}
def join_community(actor, community)
when has_type(actor, "Person") and has_type(community, "MoodleNet:Community") do
params = %{type: "Follow", actor: actor, object: community}
with {:ok, activity} = ActivityPub.new(params),
{:ok, _activity} <- ActivityPub.apply(activity) do
{:ok, true}
end
end
def follow_collection(actor, collection)
when has_type(actor, "Person") and has_type(collection, "MoodleNet:Collection") do
params = %{type: "Follow", actor: actor, object: collection}
with {:ok, activity} = ActivityPub.new(params),
{:ok, _activity} <- ActivityPub.apply(activity) do
......@@ -260,6 +311,30 @@ defmodule MoodleNet do
end
end
def like_comment(actor, comment)
when has_type(actor, "Person") and has_type(comment, "Note") do
comment = preload_community(comment)
attrs = %{type: "Like", actor: actor, object: comment}
with :ok <- Policy.like_comment?(actor, comment, attrs),
{:ok, activity} = ActivityPub.new(attrs),
{:ok, _activity} <- ActivityPub.apply(activity) do
{:ok, true}
end
end
def like_resource(actor, resource)
when has_type(actor, "Person") and has_type(resource, "MoodleNet:EducationalResource") do
resource = preload_community(resource)
attrs = %{type: "Like", actor: actor, object: resource}
with :ok <- Policy.like_resource?(actor, resource, attrs),
{:ok, activity} = ActivityPub.new(attrs),
{:ok, _activity} <- ActivityPub.apply(activity) do
{:ok, true}
end
end
def like(liker, liked) do
params = %{type: "Like", actor: liker, object: liked}
......@@ -290,9 +365,13 @@ defmodule MoodleNet do
end
defp find_current_relation(subject, relation, object) do
if Query.has?(subject, relation, object),
do: :ok,
else: {:error, {:not_found, nil, "Activity"}}
if Query.has?(subject, relation, object) do
:ok
else
subject_id = ActivityPub.Entity.local_id(subject)
object_id = ActivityPub.Entity.local_id(object)
{:error, {:not_found, [subject_id, object_id], "Activity"}}
end
end
defp find_activity(type, actor, object) do
......@@ -303,11 +382,30 @@ defmodule MoodleNet do
|> Query.last()
|> case do
nil ->
{:error, {:not_found, nil, "Activity"}}
actor_id = ActivityPub.Entity.local_id(actor)
object_id = ActivityPub.Entity.local_id(object)
{:error, {:not_found, [actor_id, object_id], "Activity"}}
activity ->
activity = Query.preload_assoc(activity, actor: {[:actor], []}, object: {[:actor], []})
{:ok, activity}
end
end
defp preload_community(community) when has_type(community, "MoodleNet:Community"),
do: community
defp preload_community(collection) when has_type(collection, "MoodleNet:Collection"),
do: Query.preload_assoc(collection, :attributed_to)
defp preload_community(resource) when has_type(resource, "MoodleNet:EducationalResource") do
Query.preload_assoc(resource, attributed_to: :attributed_to)
end
defp preload_community(comment) when has_type(comment, "Note") do
comment = Query.preload_assoc(comment, :context)
[context] = comment.context
context = preload_community(context)
%{comment | context: [context]}
end
end
......@@ -218,6 +218,7 @@ defmodule MoodleNet.Accounts do
end
def is_email_in_whitelist?(email) do
Repo.get(WhitelistEmail, email) != nil
String.ends_with?(email, "@moodle.com") ||
Repo.get(WhitelistEmail, email) != nil
end
end
......@@ -44,7 +44,7 @@ defmodule MoodleNet.Factory do
"icon" => attributes(:image),
"preferred_username" => Faker.Internet.user_name(),
"summary" => Faker.Lorem.sentence(),
"primaryLanguage" => "es",
"primary_language" => "es",
}
end
......@@ -55,7 +55,7 @@ defmodule MoodleNet.Factory do
"url" => Faker.Internet.url(),
"summary" => Faker.Lorem.sentence(),
"icon" => attributes(:image),
"primaryLanguage" => "es",
"primary_language" => "es",
"same_as" => "https://hq.moodle.net/r/98765",
"in_language" => ["en-GB"],
"public_access" => true,
......@@ -131,21 +131,21 @@ defmodule MoodleNet.Factory do
app
end
def community(attrs \\ %{}) do
def community(actor, attrs \\ %{}) do
attrs = attributes(:community, attrs)
{:ok, c} = MoodleNet.create_community(attrs)
{:ok, c} = MoodleNet.create_community(actor, attrs)
c
end
def collection(community, attrs \\ %{}) do
def collection(actor, community, attrs \\ %{}) do
attrs = attributes(:collection, attrs)
{:ok, c} = MoodleNet.create_collection(community, attrs)
{:ok, c} = MoodleNet.create_collection(actor, community, attrs)
c
end
def resource(context, attrs \\ %{}) do
def resource(actor, context, attrs \\ %{}) do
attrs = attributes(:resource, attrs)
{:ok, c} = MoodleNet.create_resource(nil, context, attrs)
{:ok, c} = MoodleNet.create_resource(actor, context, attrs)
c
end
......
defmodule MoodleNet.Policy do
import ActivityPub.Guards
alias ActivityPub.SQL.Query
def create_collection?(actor, community, _attrs)
when has_type(community, "MoodleNet:Community") and has_type(actor, "Person") do
actor_follows!(actor, community)
end
def create_resource?(actor, collection, _attrs)
when has_type(collection, "MoodleNet:Collection") and has_type(actor, "Person") do
community = get_community(collection)
actor_follows!(actor, community)
end
def create_comment?(actor, community, _attrs)
when has_type(community, "MoodleNet:Community") and has_type(actor, "Person") do
actor_follows!(actor, community)
end
def create_comment?(actor, collection, _attrs)
when has_type(collection, "MoodleNet:Collection") and has_type(actor, "Person") do
community = get_community(collection)
actor_follows!(actor, community)
end
def like_comment?(actor, comment, _attrs)
when has_type(comment, "Note") and has_type(actor, "Person") do
community = get_community(comment)
actor_follows!(actor, community)
end
def like_resource?(actor, resource, _attrs)
when has_type(resource, "MoodleNet:EducationalResource") and has_type(actor, "Person") do
community = get_community(resource)
actor_follows!(actor, community)
end
defp actor_follows!(actor, object) do
if Query.has?(actor, :following, object), do: :ok, else: {:error, :forbidden}
end
defp get_community(comment) when has_type(comment, "Note") do
[context] = comment.context
get_community(context)
end
defp get_community(resource) when has_type(resource, "MoodleNet:EducationalResource") do
[collection] = resource.attributed_to
get_community(collection)
end
defp get_community(collection) when has_type(collection, "MoodleNet:Collection") do
[community] = collection.attributed_to
community
end
defp get_community(community) when has_type(community, "MoodleNet:Community"), do: community
end
......@@ -32,8 +32,6 @@ defmodule MoodleNet.ReleaseTasks do
def migrate_db(_) do
start_apps()
Enum.each(@repos, &create_repo/1)
start_repos()
Enum.each(@repos, &migrate_repo/1)
......@@ -65,7 +63,6 @@ defmodule MoodleNet.ReleaseTasks do
def seed_db(_) do
start_apps()
Enum.each(@repos, &create_repo/1)
start_repos()
......
......@@ -42,6 +42,12 @@ defmodule MoodleNetWeb.GraphQL.Schema do
resolve(MoodleNetSchema.resolve_by_id_and_type("MoodleNet:EducationalResource"))
end
@desc "Get list of threads"
field :threads, non_null(list_of(non_null(:comment))) do
arg(:context_local_id, non_null(:integer))
resolve(&MoodleNetSchema.list_threads/2)
end
@desc "Get list of comments"
field :comments, non_null(list_of(non_null(:comment))) do
arg(:context_local_id, non_null(:integer))
......@@ -170,28 +176,52 @@ defmodule MoodleNetWeb.GraphQL.Schema do
resolve(&MoodleNetSchema.delete_user/2)
end
@desc "Follow an actor"
field :follow, type: :boolean do
arg(:actor_local_id, non_null(:integer))
resolve(&MoodleNetSchema.create_follow/2)
@desc "Join a community"
field :join_community, type: :boolean do
arg(:community_local_id, non_null(:integer))
resolve(&MoodleNetSchema.join_community/2)
end
@desc "Unfollow an actor"
field :unfollow, type: :boolean do
arg(:actor_local_id, non_null(:integer))
resolve(&MoodleNetSchema.destroy_follow/2)
@desc "Undo join a community"
field :undo_join_community, type: :boolean do
arg(:community_local_id, non_null(:integer))
resolve(&MoodleNetSchema.undo_join_community/2)
end
@desc "Follow a collection"
field :follow_collection, type: :boolean do
arg(:collection_local_id, non_null(:integer))
resolve(&MoodleNetSchema.follow_collection/2)
end
@desc "Undo follow a collection"
field :undo_follow_collection, type: :boolean do
arg(:collection_local_id, non_null(:integer))
resolve(&MoodleNetSchema.undo_follow_collection/2)
end
@desc "Like a comment"
field :like_comment, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.like_comment/2)
end
@desc "Like a resource"
field :like_resource, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.like_resource/2)
end
@desc "Like an object"
field :like, type: :boolean do
@desc "Undo a previous like to a comment"
field :undo_like_comment, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.create_like/2)
resolve(&MoodleNetSchema.undo_like_comment/2)
end
@desc "Unlike an object"
field :unlike, type: :boolean do
@desc "Undo a previous like to a resource"
field :undo_like_resource, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.destroy_like/2)
resolve(&MoodleNetSchema.undo_like_resource/2)
end
@desc "Login"
......
......@@ -108,6 +108,8 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
field(:published, :string)
field(:updated, :string)
field(:followed, non_null(:boolean), do: resolve(with_bool_join(:follow)))
end
input_object :community_input do
......@@ -158,6 +160,8 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
field(:published, :string)
field(:updated, :string)
field(:followed, non_null(:boolean), do: resolve(with_bool_join(:follow)))
end
input_object :collection_input do
......@@ -287,6 +291,16 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
{:ok, resources}
end
def list_threads(%{context_local_id: context_local_id}, info) do
fields = requested_fields(info)
comments =
MoodleNet.list_threads(context_local_id)
|> prepare(fields)
{:ok, comments}
end
def list_comments(%{context_local_id: context_local_id}, info) do
fields = requested_fields(info)
......@@ -392,7 +406,8 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
def create_community(%{community: attrs}, info) do
attrs = set_icon(attrs)
with {:ok, community} = MoodleNet.create_community(attrs) do
with {:ok, actor} <- current_actor(info),
{:ok, community} <- MoodleNet.create_community(actor, attrs) do
fields = requested_fields(info)
{:ok, prepare(community, fields)}
end
......@@ -417,25 +432,34 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
|> Errors.handle_error()
end
def create_follow(%{actor_local_id: id}, info) do
with {:ok, follower} <- current_actor(info),
{:ok, following} <- fetch(id, "Actor") do
MoodleNet.follow(follower, following)
def join_community(%{community_local_id: id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, community} <- fetch(id, "MoodleNet:Community") do
MoodleNet.join_community(actor, community)
end
|> Errors.handle_error()
end
def destroy_follow(%{actor_local_id: id}, info) do
with {:ok, follower} <- current_actor(info) do
MoodleNet.undo_follow(follower, id)
def undo_join_community(%{community_local_id: id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, community} <- fetch(id, "MoodleNet:Community") do
MoodleNet.undo_follow(actor, community)
end
|> Errors.handle_error()
end
def create_like(%{local_id: id}, info) do
with {:ok, liker} <- current_actor(info),
{:ok, liked} <- fetch(id) do
MoodleNet.like(liker, liked)
def follow_collection(%{collection_local_id: id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, collection} <- fetch(id, "MoodleNet:Collection") do
MoodleNet.follow_collection(actor, collection)
end
|> Errors.handle_error()
end
def undo_follow_collection(%{collection_local_id: id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, collection} <- fetch(id, "MoodleNet:Collection") do
MoodleNet.undo_follow(actor, collection)
end
|> Errors.handle_error()
end
......@@ -447,10 +471,43 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
|> Errors.handle_error()
end
def like_comment(%{local_id: comment_id}, info) do
with {:ok, liker} <- current_actor(info),
{:ok, comment} <- fetch(comment_id, "Note") do
MoodleNet.like_comment(liker, comment)
end
|> Errors.handle_error()
end
def like_resource(%{local_id: resource_id}, info) do
with {:ok, liker} <- current_actor(info),
{:ok, resource} <- fetch(resource_id, "MoodleNet:EducationalResource") do
MoodleNet.like_resource(liker, resource)
end
|> Errors.handle_error()
end
def undo_like_comment(%{local_id: comment_id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, comment} <- fetch(comment_id, "Note") do
MoodleNet.undo_like(actor, comment)
end
|> Errors.handle_error()
end
def undo_like_resource(%{local_id: resource_id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, resource} <- fetch(resource_id, "MoodleNet:EducationalResource") do
MoodleNet.undo_like(actor, resource)
end
|> Errors.handle_error()
end
def create_collection(%{collection: attrs, community_local_id: comm_id}, info) do
with {:ok, community} <- fetch(comm_id, "MoodleNet:Community"),
with {:ok, actor} <- current_actor(info),
{:ok, community} <- fetch(comm_id, "MoodleNet:Community"),
attrs = set_icon(attrs),
{:ok, collection} = MoodleNet.create_collection(community, attrs) do
{:ok, collection} <- MoodleNet.create_collection(actor, community, attrs) do
fields = requested_fields(info)
{:ok, prepare(collection, fields)}
end
......@@ -554,16 +611,6 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
|> Query.one()
end
defp fetch(local_id, type \\ nil)
defp fetch(local_id, nil) do
ActivityPub.SQLEntity.get_by_local_id(local_id)
|> case do
nil -> Errors.not_found_error(local_id, nil)
obj -> {:ok, obj}
end
end
defp fetch(local_id, type) do
case get_by_id_and_type(local_id, type) do
nil -> Errors.not_found_error(local_id, type)
......@@ -800,6 +847,22 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
end
end
defp with_bool_join(:follow) do
fn parent, _, info ->