Commit 89e27f9b authored by Alex Castaño's avatar Alex Castaño

Merge branch 'release/0.0.16'

parents 668a3c7b a2ff5e73
# MoodleNet Federated Server - based on the CommonsPub ActivityPub Server
# MoodleNet Federated Server
## About the project
[CommonsPub](http://commonspub.org/) is a generic federated server, based on the ActivityPub and ActivityStreams web standards.
The back-end is written in Elixir (running on the Erlang VM, and using the Phoenix web framework) to be highly performant and can run on low powered devices like a Raspberry Pi. Each app will likely have a bespoke front-end (though they're of course encouraged to share components).
It was forked from Pleroma with the intention of moving as much functionality as possible into frameworks/libraries, and generally turning it into a generic ActivityPub server that can power many different apps and use cases, all of them as interoperable as possible with each other, and any other ActivityPub-based fediverse app like Mastodon.
The first projects using it are:
* [MoodleNet](https://moodle.com/moodlenet) to empower communities of educators to connect, learn, and curate open content together
* [Open Cooperative Ecosystem](https://opencoopecosystem.net/) to empower economic activities driven by human and ecological needs rather than profit
This back-end is written in Elixir (running on the Erlang VM, and using the Phoenix web framework). The client API uses GraphQL. The main front-end is in React (in a [seperate repo](https://gitlab.com/moodlenet/clients/react?nav_source=navbar)).
This codebase was forked from [CommonsPub](http://commonspub.org/) (project to create a generic federated server, based on the ActivityPub and ActivityStreams web standards) which was originally forked from Pleroma.
## Installation
......@@ -31,8 +22,8 @@ GNU Make 4.2.1
Clone this repo and change into the directory:
```sh
$ git clone https://gitlab.com/CommonsPub/Server.git
$ cd Server
$ git clone https://gitlab.com/moodlenet/servers/federated.git
$ cd federated
```
Build the docker image:
......@@ -121,7 +112,7 @@ There is a `Makefile` with two commands:
## Running
By default, CommonsPub listens on port 4000 (TCP), so you can access it on http://localhost:4000/ (if you are on the same machine). In case of an error it will restart automatically.
By default, the back-end listens on port 4000 (TCP), so you can access it on http://localhost:4000/ (if you are on the same machine). In case of an error it will restart automatically.
# Configuring the server
......
......@@ -126,6 +126,12 @@ defmodule ActivityPub.SQL.Query do
)
end
def without_type(%Ecto.Query{} = query, type) when is_binary(type) do
from([entity: entity] in query,
where: not(fragment("? @> array[?]", entity.type, ^type))
)
end
def where(%Ecto.Query{} = query, clauses) do
from(e in query,
where: ^clauses
......
......@@ -46,9 +46,17 @@ defmodule MoodleNet do
# Community connections
defp outbox_query(actor) do
Query.new()
|> Query.with_type("Activity")
|> Query.without_type("Undo")
|> Query.belongs_to(:outbox, actor)
end
defp inbox_query(actor) do
Query.new()
|> Query.with_type("Activity")
|> Query.without_type("Undo")
|> Query.belongs_to(:inbox, actor)
end
......@@ -265,6 +273,7 @@ defmodule MoodleNet do
def local_activity_list(opts \\ %{}) do
Query.new()
|> Query.with_type("Activity")
|> Query.without_type("Undo")
|> Query.preload_aspect(:activity)
|> Query.paginate(opts)
|> Query.all()
......@@ -273,6 +282,7 @@ defmodule MoodleNet do
def local_activity_count() do
Query.new()
|> Query.with_type("Activity")
|> Query.without_type("Undo")
|> Query.count()
end
......@@ -290,6 +300,17 @@ defmodule MoodleNet do
)
end
def user_outbox_list(user, opts) do
outbox_query(user)
|> Query.paginate(opts)
|> Query.all()
end
def user_outbox_count(user) do
outbox_query(user)
|> Query.count()
end
def user_inbox_list(user, opts) do
inbox_query(user)
|> Query.paginate(opts)
......
......@@ -12,8 +12,8 @@ defmodule MoodleNetWeb.GraphQL.ActivitySchema do
field(:type, non_null(list_of(non_null(:string))))
field(:activity_type, :string)
field(:user, :user)
field(:object, :activity_object)
field(:user, :user, do: resolve(Resolver.with_assoc(:actor, single: true)))
field(:object, :activity_object, do: resolve(Resolver.with_assoc(:object, single: true, preload_assoc_individually: true)))
end
union :activity_object do
......@@ -39,13 +39,13 @@ defmodule MoodleNetWeb.GraphQL.ActivitySchema do
def prepare([e | _] = list, fields) when APG.has_type(e, "Activity") do
list
|> Query.preload_assoc([:actor, :object])
|> Query.preload_assoc([object: {[:all], [:object]}])
|> Enum.map(&prepare(&1, fields))
end
def prepare(e, _fields) when APG.has_type(e, "Activity") do
e
|> Query.preload_assoc([actor: {[:actor_aspect], []}, object: {[:all], [:object]}])
|> Query.preload_assoc([object: {[:all], [:object]}])
|> prepare_activity_fields()
|> Resolver.prepare_common_fields()
end
......@@ -53,10 +53,7 @@ defmodule MoodleNetWeb.GraphQL.ActivitySchema do
defp prepare_activity_fields(e) do
object = hd(e.object)
e
|> Map.put(:user, hd(e.actor))
|> Map.put(:object, object)
|> Map.put(:activity_type, resolve_activity_type(e, object))
Map.put(e, :activity_type, resolve_activity_type(e, object))
end
defp resolve_activity_type(activity, object)
......@@ -109,4 +106,9 @@ defmodule MoodleNetWeb.GraphQL.ActivitySchema do
defp resolve_activity_type(activity, object)
when APG.has_type(activity, "Like") and APG.has_type(object, "Note"),
do: "LikeComment"
defp resolve_activity_type(activity, object)
when APG.has_type(activity, "Undo"), do: "Undo"
defp resolve_activity_type(_, _), do: "UnknownActivity"
end
......@@ -35,7 +35,7 @@ defmodule MoodleNetWeb.GraphQL.CommentSchema do
resolve(Resolver.with_connection(:comment_liker))
end
field(:context, :comment_context, do: resolve(Resolver.with_assoc(:context, single: true)))
field(:context, :comment_context, do: resolve(Resolver.with_assoc(:context, single: true, preload_assoc_individually: true)))
end
union :comment_context do
......
......@@ -318,9 +318,14 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
preload_args = {assoc, fields}
args =
if Keyword.get(opts, :collection, false),
do: {__MODULE__, :preload_collection, preload_args},
else: {__MODULE__, :preload_assoc, preload_args}
cond do
Keyword.get(opts, :collection, false) ->
{__MODULE__, :preload_collection, preload_args}
Keyword.get(opts, :preload_assoc_individually, false) ->
{__MODULE__, :preload_assoc_individually, preload_args}
true ->
{__MODULE__, :preload_assoc, preload_args}
end
batch(
args,
......@@ -367,6 +372,31 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
end
end
# It is called from Absinthe
def preload_assoc_individually({assoc, fields}, parent_list) do
parent_list = Query.preload_assoc(parent_list, assoc)
child_list =
parent_list
|> Enum.flat_map(&Map.get(&1, assoc))
|> Enum.uniq_by(&Entity.local_id(&1))
child_map =
child_list
|> Enum.map(&prepare(&1, fields))
|> Enum.group_by(&Entity.local_id/1)
Map.new(parent_list, fn parent ->
children =
parent
|> Map.get(assoc)
|> Enum.map(&Entity.local_id/1)
|> Enum.flat_map(&child_map[&1])
{Entity.local_id(parent), children}
end)
end
# It is called from Absinthe
def preload_assoc({assoc, fields}, parent_list) do
parent_list = Query.preload_assoc(parent_list, assoc)
......@@ -377,7 +407,8 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
|> Enum.uniq_by(&Entity.local_id(&1))
child_map =
prepare(child_list, fields)
child_list
|> prepare(fields)
|> Enum.group_by(&Entity.local_id/1)
Map.new(parent_list, fn parent ->
......
......@@ -117,6 +117,13 @@ defmodule MoodleNetWeb.GraphQL.UserSchema do
arg(:after, :integer)
resolve(with_connection(:user_inbox))
end
field :outbox, non_null(:user_outbox_connection) do
arg(:limit, :integer)
arg(:before, :integer)
arg(:after, :integer)
resolve(with_connection(:user_outbox))
end
end
object :user_joined_communities_connection do
......@@ -153,6 +160,12 @@ defmodule MoodleNetWeb.GraphQL.UserSchema do
field(:total_count, non_null(:integer))
end
object :user_outbox_connection do
field(:page_info, non_null(:page_info))
field(:edges, list_of(:user_activities_edge))
field(:total_count, non_null(:integer))
end
object :user_activities_edge do
field(:cursor, non_null(:integer))
field(:node, :activity)
......
......@@ -4,7 +4,7 @@ defmodule MoodleNet.Mixfile do
def project do
[
app: :moodle_net,
version: "0.0.15",
version: "0.0.16",
elixir: "~> 1.7.4",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
......
......@@ -19,16 +19,22 @@ defmodule MoodleNetWeb.GraphQL.ActivitySchemaTest do
id
user {
id
name
preferredUsername
joinedCommunities { totalCount }
}
object {
__typename
... on Community {
id
name
preferredUsername
members { totalCount }
}
... on Collection {
id
name
preferredUsername
resources { totalCount }
}
... on Resource {
......
......@@ -602,7 +602,7 @@ defmodule MoodleNetWeb.GraphQL.CommunityTest do
cursor
node {
id
activity_type
activityType
}
}
totalCount
......@@ -628,42 +628,42 @@ defmodule MoodleNetWeb.GraphQL.CommunityTest do
assert [
%{
"node" => %{
"activity_type" => "CreateComment"
"activityType" => "CreateComment"
}
},
%{
"node" => %{
"activity_type" => "CreateComment"
"activityType" => "CreateComment"
}
},
%{
"node" => %{
"activity_type" => "UpdateResource"
"activityType" => "UpdateResource"
}
},
%{
"node" => %{
"activity_type" => "CreateResource"
"activityType" => "CreateResource"
}
},
%{
"node" => %{
"activity_type" => "UpdateCollection"
"activityType" => "UpdateCollection"
}
},
%{
"node" => %{
"activity_type" => "CreateCollection"
"activityType" => "CreateCollection"
}
},
%{
"node" => %{
"activity_type" => "UpdateCommunity"
"activityType" => "UpdateCommunity"
}
},
%{
"node" => %{
"activity_type" => "JoinCommunity"
"activityType" => "JoinCommunity"
}
}
] = edges
......
......@@ -603,7 +603,7 @@ defmodule MoodleNetWeb.GraphQL.UserSchemaTest do
cursor
node {
id
activity_type
activityType
}
}
totalCount
......@@ -629,57 +629,212 @@ defmodule MoodleNetWeb.GraphQL.UserSchemaTest do
assert [
%{
"node" => %{
"activity_type" => "LikeComment"
"activityType" => "LikeComment"
}
},
%{
"node" => %{
"activity_type" => "LikeComment"
"activityType" => "LikeComment"
}
},
%{
"node" => %{
"activity_type" => "CreateComment"
"activityType" => "CreateComment"
}
},
%{
"node" => %{
"activity_type" => "CreateComment"
"activityType" => "CreateComment"
}
},
%{
"node" => %{
"activity_type" => "UpdateResource"
"activityType" => "UpdateResource"
}
},
%{
"node" => %{
"activity_type" => "CreateResource"
"activityType" => "CreateResource"
}
},
%{
"node" => %{
"activity_type" => "UpdateCollection"
"activityType" => "UpdateCollection"
}
},
%{
"node" => %{
"activity_type" => "FollowCollection"
"activityType" => "FollowCollection"
}
},
%{
"node" => %{
"activity_type" => "CreateCollection"
"activityType" => "CreateCollection"
}
},
%{
"node" => %{
"activity_type" => "UpdateCommunity"
"activityType" => "UpdateCommunity"
}
},
%{
"node" => %{
"activity_type" => "JoinCommunity"
"activityType" => "JoinCommunity"
}
}
] = edges
end
@tag :user
test "outbox connection", %{conn: conn, actor: actor} do
community = Factory.community(actor)
MoodleNet.update_community(actor, community, %{name: "Name"})
collection = Factory.collection(actor, community)
MoodleNet.update_collection(actor, collection, %{name: "Name"})
MoodleNet.like_collection(actor, collection)
resource = Factory.resource(actor, collection)
MoodleNet.update_resource(actor, resource, %{name: "Name"})
MoodleNet.like_resource(actor, resource)
comment = Factory.comment(actor, collection)
reply = Factory.reply(actor, comment)
MoodleNet.like_comment(actor, comment)
MoodleNet.like_comment(actor, reply)
comment = Factory.comment(actor, community)
reply = Factory.reply(actor, comment)
MoodleNet.like_comment(actor, comment)
MoodleNet.like_comment(actor, reply)
local_id = local_id(actor)
query = """
{
user(localId: #{local_id}) {
outbox {
pageInfo {
startCursor
endCursor
}
edges {
cursor
node {
id
activityType
}
}
totalCount
}
}
}
"""
assert ret =
conn
|> post("/api/graphql", %{query: query})
|> json_response(200)
|> Map.fetch!("data")
|> Map.fetch!("user")
|> Map.fetch!("outbox")
assert %{
"pageInfo" => %{"startCursor" => nil, "endCursor" => nil},
"edges" => edges,
"totalCount" => 18
} = ret
assert [
%{
"node" => %{
"activityType" => "LikeComment"
}
},
%{
"node" => %{
"activityType" => "LikeComment"
}
},
%{
"node" => %{
"activityType" => "CreateComment"
}
},
%{
"node" => %{
"activityType" => "CreateComment"
}
},
%{
"node" => %{
"activityType" => "LikeComment"
}
},
%{
"node" => %{
"activityType" => "LikeComment"
}
},
%{
"node" => %{
"activityType" => "CreateComment"
}
},
%{
"node" => %{
"activityType" => "CreateComment"
}
},
%{
"node" => %{
"activityType" => "LikeResource"
}
},
%{
"node" => %{
"activityType" => "UpdateResource"
}
},
%{
"node" => %{
"activityType" => "CreateResource"
}
},
%{
"node" => %{
"activityType" => "LikeCollection"
}
},
%{
"node" => %{
"activityType" => "UpdateCollection"
}
},
%{
"node" => %{
"activityType" => "FollowCollection"
}
},
%{
"node" => %{
"activityType" => "CreateCollection"
}
},
%{
"node" => %{
"activityType" => "UpdateCommunity"
}
},
%{
"node" => %{
"activityType" => "JoinCommunity"
}
},
%{
"node" => %{
"activityType" => "CreateCommunity"
}
}
] = edges
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment