Commit 73e74770 authored by Alex Castaño's avatar Alex Castaño

Merge branch 'release/0.0.18'

parents 69ddc990 f87aa9c5
......@@ -38,3 +38,5 @@ config/secret.exs
cover
priv/repo/structure.sql
*.dump
......@@ -68,4 +68,5 @@ run: ## Run the app in Docker
docker run\
--env-file config/docker.env \
--expose 4000 -p 4000:4000 \
--rm -it moodlenet:latest
--link postgres \
--rm -it moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD)
......@@ -4042,7 +4042,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
......@@ -4457,7 +4458,8 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
......@@ -4513,6 +4515,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
......@@ -4556,12 +4559,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},
......
......@@ -81,3 +81,5 @@ config :moodle_net, MoodleNet.Repo,
database: "moodle_net_dev",
hostname: "localhost",
pool_size: 10
config :moodle_net, :ap_base_url, "http://dev.localhost:4000/activity_pub"
HOSTNAME=localhost
SECRET_KEY_BASE="U1QXlca4ZEZKb1o3HL/aUlznI1qstCNAQ6yme/lFbFIs0Iqiq/annZ+Ty8JyUCDc"
DATABASE_HOST=db
DATABASE_HOST=postgres
DATABASE_USER=postgres
DATABASE_PASS=postgres
DATABASE_NAME=moodle_net_prod
......@@ -8,3 +8,4 @@ PORT=4000
LANG=en_US.UTF-8
REPLACE_OS_VARS=true
ERLANG_COOKIE=moodle_net_cookie
AP_BASE_URL=http://localhost:4000/activity_pub
......@@ -27,3 +27,5 @@ config :phoenix_integration,
config :moodle_net, MoodleNet.Mailer,
adapter: Bamboo.TestAdapter
config :moodle_net, :ap_base_url, "http://test.localhost:4001/activity_pub"
......@@ -8,7 +8,8 @@ defmodule ActivityPub.CollectionAspect do
# assoc(:current, functional: true)
# assoc(:first, functional: true)
# assoc(:last, functional: true)
assoc(:items)
# assoc(:items)
field(:items, :any, virtual: true)
# FIXME private attribute for the field?
field(:__ordered__, :boolean)
......
defmodule ActivityPub.CollectionPageAspect do
use ActivityPub.Aspect, persistence: ActivityPub.SQLCollectionPageAspect
aspect do
# FIXME make just single
assoc(:part_of)
assoc(:next)
assoc(:prev)
end
end
......@@ -5,5 +5,6 @@ defmodule ActivityPub.Association do
functional: false,
type: :any,
autogenerated: false,
repeated: false,
inv: false
end
......@@ -69,10 +69,11 @@ defmodule ActivityPub.Builder do
defp build(:new, value, _, parent_key),
do: {:error, %BuildError{path: [parent_key], value: value, message: "is invalid"}}
defp build_new(%{"id" => value}, _, parent_key) do
msg = "is an autogenerated field"
build_error("id", value, msg, parent_key)
end
# FIXME !!! This was commented to create CollectionPage
# defp build_new(%{"id" => value}, _, parent_key) do
# msg = "is an autogenerated field"
# build_error("id", value, msg, parent_key)
# end
defp build_new(params, parent, parent_key) when is_map(params) do
{raw_context, params} = Map.pop(params, "@context")
......@@ -81,7 +82,7 @@ defmodule ActivityPub.Builder do
with {:ok, context} <- context(:new, raw_context, parent),
{:ok, type} <- Types.build(raw_type),
meta = Metadata.new(type),
entity = %{__ap__: meta, id: nil, type: type, "@context": context},
entity = %{__ap__: meta, id: params["id"], type: type, "@context": context},
{:ok, entity, params} <- merge_aspects_fields(entity, params),
{:ok, entity, extension_fields} <- merge_aspects_assocs(entity, params),
entity = Map.merge(entity, extension_fields) do
......@@ -163,12 +164,13 @@ defmodule ActivityPub.Builder do
end
end
defp cast_and_put(_entity, raw_value, %{autogenerated: true} = field_def) do
msg = "is an autogenerated field, but data is received"
field_name = to_string(field_def.name)
error = %BuildError{path: [field_name], value: raw_value, message: msg}
{:error, error}
end
# FIXME !!! This was commented to create CollectionPage
# defp cast_and_put(_entity, raw_value, %{autogenerated: true} = field_def) do
# msg = "is an autogenerated field, but data is received"
# field_name = to_string(field_def.name)
# error = %BuildError{path: [field_name], value: raw_value, message: msg}
# {:error, error}
# end
defp cast_and_put(entity, raw_value, %{type: LanguageValueType, functional: true} = field_def) do
lang = entity[:"@context"].language
......
......@@ -14,6 +14,12 @@ defmodule ActivityPub.Entity do
def fields_for(_, _), do: %{}
def fields(entity) when APG.is_entity(entity) do
entity
|> aspects()
|> Enum.flat_map(&fields_for(entity, &1))
end
def assocs_for(entity, aspect) when APG.has_aspect(entity, aspect),
do: Map.take(entity, aspect.__aspect__(:associations))
......@@ -35,10 +41,7 @@ defmodule ActivityPub.Entity do
end)
end
def local?(%{id: id} = e) when APG.is_entity(e) and not is_nil(id),
do: ActivityPub.UrlBuilder.local?(id)
def local?(%{id: nil} = e) when APG.has_local_id(e), do: true
def local?(%{id: nil} = e) when APG.is_entity(e), do: status(e) == :new
def local?(%{__ap__: ap} = e) when APG.is_entity(e), do: Metadata.local?(ap)
def status(%{__ap__: %{status: status}} = e) when APG.is_entity(e), do: status
......
......@@ -6,6 +6,7 @@ defmodule ActivityPub.Field do
type: nil,
default: nil,
autogenerated: false,
repeated: false,
virtual: false
def build(opts) do
......
......@@ -3,6 +3,7 @@ defmodule ActivityPub.Guards do
require APMG
defguard is_entity(e) when APMG.is_metadata(:erlang.map_get(:__ap__, e))
defguard is_local(e) when APMG.is_local(:erlang.map_get(:__ap__, e))
defguard has_type(e, type) when APMG.has_type(:erlang.map_get(:__ap__, e), type)
defguard has_aspect(e, aspect) when APMG.has_aspect(:erlang.map_get(:__ap__, e), aspect)
defguard has_status(e, status) when APMG.has_status(:erlang.map_get(:__ap__, e), status)
......
......@@ -6,7 +6,8 @@ defmodule ActivityPub.Metadata do
status: nil,
persistence: nil,
local_id: nil,
verified: false
verified: false,
local: true,
]
def new(type_list) do
......@@ -18,7 +19,8 @@ defmodule ActivityPub.Metadata do
aspects: aspects,
status: :new,
persistence: nil,
verified: true
verified: true,
local: true,
}
end
......@@ -29,6 +31,7 @@ defmodule ActivityPub.Metadata do
status: :not_loaded,
persistence: nil,
verified: false,
local: true,
local_id: nil
}
end
......@@ -38,6 +41,7 @@ defmodule ActivityPub.Metadata do
status: :not_loaded,
persistence: nil,
verified: true,
local: true,
local_id: local_id
}
end
......@@ -52,6 +56,7 @@ defmodule ActivityPub.Metadata do
status: :loaded,
persistence: sql,
local_id: sql.local_id,
local: sql.local,
verified: true
}
end
......@@ -66,6 +71,9 @@ defmodule ActivityPub.Metadata do
def local_id(%__MODULE__{local_id: local_id}), do: local_id
def local?(%__MODULE__{local: true}), do: true
def local?(%__MODULE__{}), do: false
def inspect(%__MODULE__{} = meta, opts) do
pruned = %{
status: meta.status,
......@@ -84,6 +92,7 @@ end
defmodule ActivityPub.Metadata.Guards do
defguard is_metadata(meta) when :erlang.map_get(:__struct__, meta) == ActivityPub.Metadata
defguard is_local(meta) when :erlang.map_get(:local, meta) == true
defguard has_type(meta, type)
when is_metadata(meta) and :erlang.map_get(type, :erlang.map_get(:types, meta))
......
......@@ -8,5 +8,6 @@ defmodule ActivityPub.SQL.Associations.Collection do
autogenerated: true,
table_name: "activity_pub_collection_items",
join_keys: [:subject_id, :target_id],
repeated: false,
foreign_key: nil
end
......@@ -26,12 +26,16 @@ defmodule ActivityPub.SQL.Paginate do
%{
limit: calc_limit(query_params),
after: query_params[:after] || query_params["after"],
before: query_params[:before] || query_params["before"]
after: query_params[:after] || query_params["after"] |> to_integer(),
before: query_params[:before] || query_params["before"] |> to_integer()
}
|> calc_order()
end
defp to_integer(binary) when is_binary(binary), do: String.to_integer(binary)
defp to_integer(integer) when is_integer(integer), do: integer
defp to_integer(nil), do: nil
defp calc_limit(query_params) do
Enum.min([query_params[:limit] || query_params["limit"] || 100, 100])
end
......
......@@ -29,6 +29,12 @@ defmodule ActivityPub.SQL.Query do
|> Repo.delete_all()
end
# FIXME this should not be here?
def update_all(%Ecto.Query{} = query, updates) do
query
|> Repo.update_all(updates)
end
def one(%Ecto.Query{} = query) do
query
# |> print_query()
......@@ -70,6 +76,11 @@ defmodule ActivityPub.SQL.Query do
def get_by_id(id, opts \\ []) when is_binary(id) do
case UrlBuilder.get_local_id(id) do
{:ok, {:page, collection_id, params}} ->
collection = get_by_local_id(collection_id, opts)
{:ok, page} = ActivityPub.CollectionPage.new(collection, params)
page
{:ok, local_id} ->
get_by_local_id(local_id, opts)
......@@ -128,7 +139,7 @@ defmodule ActivityPub.SQL.Query do
def without_type(%Ecto.Query{} = query, type) when is_binary(type) do
from([entity: entity] in query,
where: not(fragment("? @> array[?]", entity.type, ^type))
where: not fragment("? @> array[?]", entity.type, ^type)
)
end
......@@ -233,6 +244,16 @@ defmodule ActivityPub.SQL.Query do
when APG.is_entity(subject) and APG.has_status(subject, :loaded) and is_integer(target),
do: do_has?(subject, rel, target)
def belongs_to(%Ecto.Query{} = query, collection) when APG.has_type(collection, "Collection") do
collection_local_id = ActivityPub.local_id(collection)
from([entity: entity] in query,
inner_join: rel in "activity_pub_collection_items",
as: :items,
on: entity.local_id == rel.target_id,
where: rel.subject_id == ^collection_local_id
)
end
for sql_aspect <- ActivityPub.SQLAspect.all() do
Enum.map(sql_aspect.__sql_aspect__(:associations), fn
%ManyToMany{
......@@ -427,6 +448,11 @@ defmodule ActivityPub.SQL.Query do
def preload_assoc([], _preload), do: []
def preload_assoc(entity, :all) when APG.is_entity(entity) do
assoc_keys = Map.keys(Entity.assocs(entity))
preload_assoc(entity, assoc_keys)
end
def preload_assoc(entity, preload) when not is_list(preload),
do: preload_assoc(entity, List.wrap(preload))
......
......@@ -3,12 +3,4 @@ defmodule ActivityPub.SQLActivityAspect do
aspect: ActivityPub.ActivityAspect,
persistence_method: :table,
table_name: "activity_pub_activity_aspects"
# alias ActivityPub.SQLObject
# @primary_key {:local_id, :id, autogenerate: true}
# schema "activity_pub_activity_aspects" do
# # many_to_many :actor, SQLObject, join_through: "activity_pub_activity_actors",
# # join_keys: [local_id: :activity_id, object_id: :local_id]
# end
end
......@@ -3,8 +3,7 @@ defmodule ActivityPub.SQLAspect do
SQLObjectAspect,
SQLActorAspect,
SQLActivityAspect,
SQLCollectionAspect,
SQLResourceAspect
SQLCollectionAspect
}
alias ActivityPub.SQL.Associations.{ManyToMany, BelongsTo, Collection}
......@@ -15,7 +14,9 @@ defmodule ActivityPub.SQLAspect do
SQLActorAspect,
SQLActivityAspect,
SQLCollectionAspect,
SQLResourceAspect
MoodleNet.AP.SQLCommunityAspect,
MoodleNet.AP.SQLCollectionAspect,
MoodleNet.AP.SQLResourceAspect
]
# FIXME make this similar to aspect where the user can redifine
......@@ -140,6 +141,9 @@ defmodule ActivityPub.SQLAspect do
join_keys: [{subject_key, :local_id}, {target_key, :local_id}]
)
%Collection{repeated: true} ->
nil
%Collection{} = assoc ->
belongs_to(assoc.name, ActivityPub.SQLEntity, references: :local_id)
......@@ -188,6 +192,7 @@ defmodule ActivityPub.SQLAspect do
name: assoc.name,
type: assoc.type,
autogenerated: assoc.autogenerated,
repeated: assoc.repeated,
foreign_key: foreign_key
}
end
......
......@@ -4,17 +4,4 @@ defmodule ActivityPub.SQLObjectAspect do
use ActivityPub.SQLAspect,
aspect: ObjectAspect,
persistence_method: :fields
# sql_aspect do
# persistence_method(:fields)
# join_through_assoc(:attachment, table_name: "activity_pub_object_attachments",
# keys: {:subject_id, :target_id})
# virtual_col_assoc
# assoc(:name, VirtualCollectionAssoc)
# assoc(:name, VirtualCollectionAssoc)
# # assoc(:attachment, method: {:table, "activity_pub_object_attachments"},
# # keys: {:subject_id, :target_id}
# end
end
defmodule ActivityPub.Types do
alias ActivityPub.{
ObjectAspect,
ActorAspect,
ActivityAspect,
# LinkAspect,
CollectionAspect,
ResourceAspect,
}
alias ActivityPub.{ObjectAspect, ActorAspect, ActivityAspect, CollectionAspect}
alias ActivityPub.BuildError
@type_map %{
# "Link" => {[], [LinkAspect]},
"Link" => {[], []},
"Object" => {[], [ObjectAspect]},
"Collection" => {~w[Object], [CollectionAspect]},
"OrderedCollection" => {~w[Object Collection], []},
# "CollectionPage" => {~w[Object Collection], [CollectionPageAspect]},
"CollectionPage" => {~w[Object Collection], []},
"OrderedCollectionPage" => {~w[Object Collection OrderedCollection CollectionPage], []},
"Actor" => {~w[Object], [ActorAspect]},
......@@ -68,10 +59,9 @@ defmodule ActivityPub.Types do
"Tombstone" => {~w[Object], []},
"Video" => {~w[Object], []},
"Mention" => {~w[Link], []},
"MoodleNet:Community" => {~w[Object Actor Group Collection], []},
"MoodleNet:Collection" => {~w[Object Actor Group Collection], []},
# "MoodleNet:EducationalResource" => {~w[Link], []},
"MoodleNet:EducationalResource" => {~w[Object Page WebPage], [ResourceAspect]}
"MoodleNet:Community" => {~w[Object Actor Group], [MoodleNet.AP.CommunityAspect]},
"MoodleNet:Collection" => {~w[Object Actor Group], [MoodleNet.AP.CollectionAspect]},
"MoodleNet:EducationalResource" => {~w[Object Page WebPage], [MoodleNet.AP.ResourceAspect]}
}
def build(value) do
......
defmodule ActivityPub.UrlBuilder do
defp base_url() do
MoodleNetWeb.base_url()
Application.get_env(:moodle_net, :ap_base_url, MoodleNetWeb.base_url())
end
def id(local_id) do
"#{base_url()}/activity_pub/#{local_id}"
def id({:page, local_id, params}) do
id(local_id) <> "/page" <> params_to_query(params)
end
def id(local_id) when is_integer(local_id),
do: append_bar_if_needed(base_url()) <> to_string(local_id)
def local?(nil), do: false
def local?(id) when is_binary(id) do
......@@ -21,13 +24,50 @@ defmodule ActivityPub.UrlBuilder do
uri_id = URI.parse(id)
uri_base = URI.parse(base_url())
with true <- uri_id.scheme == uri_base.scheme and uri_id.host == uri_base.host and
uri_id.port == uri_base.port,
"/activity_pub/" <> local_id <- uri_id.path,
local_id = String.to_integer(local_id) do
{:ok, local_id}
with true <- same_base_url?(uri_base, uri_id),
{:ok, id_string} <- truncate_base_path(uri_base.path, uri_id.path),
{id, rest} <- Integer.parse(id_string) do
virtual_id(id, rest, uri_id.query)
else
_ -> :error
end
end
defp same_base_url?(uri_base, uri_id) do
uri_id.scheme == uri_base.scheme and uri_id.host == uri_base.host and
uri_id.port == uri_base.port
end
defp truncate_base_path(nil, uri_id_path), do: {:ok, uri_id_path}
defp truncate_base_path(base, path_id) do
base = append_bar_if_needed(base)
if String.starts_with?(path_id, base) do
{:ok, String.trim_leading(path_id, base)}
else
:error
end
end
defp append_bar_if_needed(base) do
if String.ends_with?(base, "/"), do: base, else: base <> "/"
end
defp virtual_id(id, "", nil), do: {:ok, id}
defp virtual_id(id, "/page", nil), do: {:ok, {:page, id, %{}}}
defp virtual_id(id, "/page", query), do: {:ok, {:page, id, URI.decode_query(query)}}
defp virtual_id(_, _, _), do: :error
defp params_to_query(nil), do: ""
defp params_to_query(params = %{}) do
params
|> Map.take(["before", "after", "limit"])
|> to_query()
end
defp to_query(params) when params == %{}, do: ""
defp to_query(params), do: "?" <> URI.encode_query(params)
end
defmodule ActivityPub.CollectionPage do
import ActivityPub.Guards
alias ActivityPub.UrlBuilder
alias ActivityPub.SQL.Query
defguardp is_local_collection(collection) when has_type(collection, "Collection") and is_local(collection)
def new(collection, params \\ %{}) when is_local_collection(collection) do
items = get_items(collection, params)
page_info = MoodleNet.page_info(items, params)
%{
id: id(collection, params),
type: "CollectionPage",
part_of: collection,
items: items,
total_items: length(items),
next: next_page(collection, page_info.older),
prev: prev_page(collection, page_info.newer),
}
|> ActivityPub.new()
end
def id(collection, params \\ %{}) when is_local_collection(collection),
do: UrlBuilder.id({:page, ActivityPub.local_id(collection), params})
defp get_items(collection, params) do
Query.new()
|> Query.belongs_to(collection)
|> Query.paginate_collection(params)
|> Query.all()
end
defp next_page(_collection, nil), do: nil
defp next_page(collection, cursor), do: id(collection, %{"after" => cursor})
defp prev_page(_collection, nil), do: nil
defp prev_page(collection, cursor), do: id(collection, %{"before" => cursor})
end
defmodule ActivityPubWeb.ActivityPubController do
use ActivityPubWeb, :controller
import ActivityPub.Guards
alias ActivityPub.SQL.Query
def show(conn, %{"id" => id}) do
id = String.to_integer(id)
case ActivityPub.get_by_local_id(id) do
entity when is_local(entity) ->
entity =
entity
|> Query.preload_aspect(:all)
|> Query.preload_assoc(:all)
render(conn, "show.json", entity: entity)
_ ->
send_resp(conn, :not_found, "")
end
end
def collection_page(conn, %{"id" => id}) do
id = String.to_integer(id)
case ActivityPub.get_by_local_id(id) do
collection when is_local(collection) and has_type(collection, "Collection") ->
{:ok, entity} = ActivityPub.CollectionPage.new(collection, conn.query_params)
render(conn, "show.json", entity: entity)
_ ->
send_resp(conn, :not_found, "")
end
end
end
defmodule ActivityPubWeb.Router do
use ActivityPubWeb, :router
pipeline :activity_pub do
plug(:accepts, ["activity+json", "json"])
end
scope "/", ActivityPubWeb do
pipe_through(:activity_pub)
get "/:id", ActivityPubController, :show
get "/:id/page", ActivityPubController, :collection_page
post "/shared_inbox", ActivityPubController, :shared_inbox, as: :shared_inbox
end
end
defmodule ActivityPubWeb.ActivityPubView do
use ActivityPubWeb, :view
alias ActivityPub.Entity
require ActivityPub.Guards, as: APG
def render("show.json", %{entity: entity, conn: conn}) do
# def render("activity_pub.json", %{entity: entity, conn: conn}) do
entity
|> Entity.aspects()
|> Enum.flat_map(&filter_by_aspect(entity, &1, conn))
|> Enum.into(%{})
|> set_type(entity.type)
|> set_context()
|> set_streams(entity)
|> set_public()
end
defp filter_by_aspect(entity, aspect, conn) do
fields_name = filter_fields_by_definition(aspect)
entity
|> Map.take(fields_name)
|> Enum.concat(Entity.assocs(entity))
|> Enum.filter(&filter_by_value/1)
|> normalize()
|> common_fields(entity, conn)
|> custom_fields(entity, aspect, conn)
end
defp common_fields(ret, entity, _conn) do
ret
|> Map.put("id", entity.id)
|> Map.put("type", entity.type)
|> Map.put("@context", entity["@context"])
|> Map.delete("likersCount")
end
defp custom_fields(ret, _entity, ActivityPub.ActorAspect, conn) do
ret
|> add_endpoints(conn)
end
defp custom_fields(ret, entity, _, _conn)
when APG.has_type(entity, "CollectionPage")
when APG.has_type(entity, "MoodleNet:Community")
when APG.has_type(entity, "MoodleNet:Collection"),
do: ret
defp custom_fields(ret, entity, ActivityPub.CollectionAspect, _conn)
when APG.has_type(entity, "CollectionPage"),
do: ret
defp custom_fields(ret, entity, ActivityPub.CollectionAspect, conn) do
ret
|> Map.put("first", ActivityPub.CollectionPage.id(entity))
# |> Map.delete("items")
end
defp custom_fields(ret, _, _, _), do: ret
defp add_endpoints(ret, conn) do
endpoints = %{"sharedInbox" => Routes.shared_inbox_url(conn, :shared_inbox)}
Map.put(ret, "endpoints", endpoints)
end
defp extension_fields(entity) do
entity
|> Entity.extension_fields()