Commit 7fd52212 authored by Alex Castaño's avatar Alex Castaño

Merge branch 'release/0.0.2'

parents 40e0c8cd 5d39f32c
_build/
deps/
.git/
.gitignore
Dockerfile
Makefile
README*
test/
priv/static/
erlang 21.1
elixir 1.7.3-otp-21
elixir 1.7.4-otp-21
nodejs 10.13.0
# The version of Alpine to use for the final image
# This should match the version of Alpine that the `elixir:1.7.4-alpine` image uses
ARG ALPINE_VERSION=3.8
# The following are build arguments used to change variable parts of the image.
# The name of your application/release (required)
ARG APP_NAME
# The version of the application we are building (required)
ARG APP_VSN
FROM elixir:1.7.4-alpine as deps-getter
ENV HOME=/opt/app/ TERM=xterm MIX_ENV=prod
WORKDIR /opt/app
# dependencies for comeonin
RUN apk add --no-cache build-base cmake curl git
# Cache elixir deps
COPY mix.exs mix.lock ./
RUN mix do local.hex --force, local.rebar --force, deps.get, deps.compile
##################3
# Asset builder
##################3
FROM node:10.13.0 as asset-builder
ENV HOME=/opt/app
WORKDIR $HOME
COPY --from=deps-getter $HOME/deps $HOME/deps
WORKDIR $HOME/assets
COPY assets/package-lock.json assets/package.json ./
RUN npm install
COPY assets/ ./
RUN npm run-script deploy
##################3
# Builder
##################3
FROM deps-getter as builder
ENV HOME=/opt/app/ TERM=xterm MIX_ENV=prod
WORKDIR $HOME
COPY . .
# Digest precompiled assets
COPY --from=asset-builder $HOME/priv/static/ $HOME/priv/static/
RUN mix do phx.digest, release --env=prod --verbose --no-tar
# From this line onwards, we're in a new image, which will be the image used in production
FROM alpine:${ALPINE_VERSION}
# The name of your application/release (required)
ARG APP_NAME
ARG APP_VSN
RUN apk update && \
apk add --no-cache \
bash \
openssl-dev
ENV REPLACE_OS_VARS=true \
APP_NAME=${APP_NAME} \
APP_VSN=${APP_VSN}
WORKDIR /opt/app
COPY --from=builder /opt/app/_build/prod/rel/${APP_NAME} /opt/app
CMD trap 'exit' INT; /opt/app/bin/${APP_NAME} foreground
.PHONY: help
APP_NAME ?= `grep 'app:' mix.exs | sed -e 's/\[//g' -e 's/ //g' -e 's/app://' -e 's/[:,]//g'`
APP_VSN ?= `grep 'version:' mix.exs | cut -d '"' -f2`
APP_BUILD ?= `git rev-parse --short HEAD`
help:
@echo "$(APP_NAME):$(APP_VSN)-$(APP_BUILD)"
@perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
build: ## Build the Docker image
@echo APP_NAME=$(APP_NAME)
@echo APP_VSN=$(APP_VSN)
docker build \
--no-cache \
--build-arg APP_NAME=$(APP_NAME) \
--build-arg APP_VSN=$(APP_VSN) \
-t moodlenet:$(APP_VSN)-$(APP_BUILD) \
-t moodlenet:latest .
@echo moodlenet:$(APP_VSN)-$(APP_BUILD)
@echo moodlenet:latest
build_with_cache: ## Build the Docker image
@echo APP_NAME=$(APP_NAME)
@echo APP_VSN=$(APP_VSN)
docker build \
--build-arg APP_NAME=$(APP_NAME) \
--build-arg APP_VSN=$(APP_VSN) \
-t moodlenet:$(APP_VSN)-$(APP_BUILD) \
-t moodlenet:latest .
@echo moodlenet:$(APP_VSN)-$(APP_BUILD)
@echo moodlenet:latest
run: ## Run the app in Docker
docker run\
--env-file config/docker.env \
--expose 4000 -p 4000:4000 \
--rm -it moodlenet:latest
# Pub of the Commons
# CommonsPub
## About the project
The Pub of the Commons (otherwise known as `CommonsPub`) is a generic federated server, based on the ActivityPub and ActivityStreams web standards.
CommonsPub 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).
......@@ -19,8 +19,49 @@ The first projects using it are:
### With Docker (recommended)
See the [server-deploy repo](https://gitlab.com/OpenCoop/CommonsPub/server-deploy), for a docker-compose based setup process.
The docker image can be found in: https://hub.docker.com/r/moodlenet/moodlenet/
The docker images needs the environment variables to work.
An updated list of them can be found in the file `config/docker.env` in this same repository.
The easiest way to launch the docker image is using the `docker-compose` tool.
The `docker-compose.yml` uses the previous `config/docker.env` to launch a `moodlenet` container
and all the dependencies, currently, only a postgres container is needed it.
#### Docker commands
The first time you launch the docker instance the database is not created.
There are several commands to make the first launch easier.
We will use `docker-compose` to show the commands:
* `docker-compose run --rm web bin/moodle_net create_db` creates the database
* `docker-compose run --rm web bin/moodle_net migrate_db` creates the database and runs the migrations
* `docker-compose run --rm web bin/moodle_net drop_db` drops the database
Other important commands are:
* `docker-compose up` launches the service, by default at the port 4000.
* `docker-compose run --rm web /bin/sh` runs a simple shell inside of the container, useful to explore the image
* `docker-compose run --rm web bin/moodle_net console` runs an `iex` console
* `docker-compose exec web bin/moodle_net remote_console` runs an `iex` console when the service is already running.
* `docker-compose run --rm web bin/moodle_net help` returns all the possible commands
There is a command that currently is not working: `seed_db`.
The reason is that to generate ActivityPub ID we need the URL where the server is running,
but `Phoenix` is not launched in this command.
However, we can still do it.
To seed the database we can run the following command in an `iex` console:
`iex> MoodleNet.ReleaseTasks.seed_db([])`
#### Build Docker image
There is a `Makefile` with two commands:
* `make build` which builds the docker image in `moodlenet:latest` and `moodlenet:$VERSION-$BUILD`
* `make run` which can be used to run the docker built docker image without `docker-compose`
---
### Manual installation
......@@ -54,7 +95,7 @@ See the [server-deploy repo](https://gitlab.com/OpenCoop/CommonsPub/server-deplo
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.
### Frontends
Pub of the Commons does not ship with a front-end, as each use case will likely have a customised client app, though compatibility between clients and not reinventing the wheel (such as sharing React.js components) is encouraged.
CommonsPub does not ship with a front-end, as each use case will likely have a customised client app, though compatibility between clients and not reinventing the wheel (such as sharing React.js components) is encouraged.
### As systemd service (with provided .service file)
[Not tested with system reboot yet!] You'll also want to set up the server to be run as a systemd service. Example .service file can be found in `installation/moodle_net.service` you can put it in `/etc/systemd/system/`.
......
......@@ -20,7 +20,8 @@ defmodule ActivityPub.EntityTest do
content: "This is a content",
name: "This is my name",
end_time: "2015-01-01T06:00:00-08:00",
new_field: "extra"
new_field: "extra",
url: "https://alex.gitlab.com/profile"
}
assert {:ok, entity} = Entity.parse(map)
......@@ -33,6 +34,8 @@ defmodule ActivityPub.EntityTest do
assert entity[:new_field] == map.new_field
assert entity["new_field"] == map.new_field
assert entity[:url] == [map.url]
end
test "activities" do
......
......@@ -8,6 +8,19 @@ defmodule MoodleNetTest do
assert community[:name] == %{"und" => attrs["name"]}
assert community[:content] == %{"und" => attrs["content"]}
assert community[:followers_count] == 0
assert [icon] = community[:icon]
assert [url] = icon[:url]
assert url
import Ecto.Query
from( a in "activity_pub_icons",
select: {a.target_id, a.subject_id})
|> Repo.all()
|> IO.inspect()
a = ActivityPub.SQL.get_by_local_id(community[:local_id])
|> ActivityPub.SQL.preload(:icon)
|> IO.inspect()
end
end
......@@ -15,9 +28,33 @@ defmodule MoodleNetTest do
test "works" do
community = Factory.community()
attrs = Factory.attributes(:collection)
assert {:ok, community} = MoodleNet.create_collection(community, attrs)
assert community[:name] == %{"und" => attrs["name"]}
assert community[:content] == %{"und" => attrs["content"]}
assert {:ok, collection} = MoodleNet.create_collection(community, attrs)
assert collection[:name] == %{"und" => attrs["name"]}
assert collection[:content] == %{"und" => attrs["content"]}
assert [icon] = collection[:icon]
assert [url] = icon[:url]
assert url
end
end
describe "create_resource" do
test "works" do
community = Factory.community()
collection = Factory.collection(community)
attrs = Factory.attributes(:resource)
assert {:ok, resource} = MoodleNet.create_resource(collection, attrs)
assert resource[:name] == %{"und" => attrs["name"]}
assert resource[:content] == %{"und" => attrs["content"]}
end
end
describe "create_comment" do
test "works" do
community = Factory.community()
actor = Factory.actor()
attrs = Factory.attributes(:comment)
assert {:ok, comment} = MoodleNet.create_comment(actor, community, attrs)
assert comment[:content] == %{"und" => attrs["content"]}
end
end
......@@ -57,11 +94,55 @@ defmodule MoodleNetTest do
community_2 = Factory.community()
collection_2 = Factory.collection(community_2)
assert [loaded_col] = MoodleNet.list_collection(community)
assert [loaded_col] = MoodleNet.list_collections(community)
assert collection[:local_id] == loaded_col[:local_id]
assert [loaded_col] = MoodleNet.list_collection(community_2)
assert [loaded_col] = MoodleNet.list_collections(community_2)
assert collection_2[:local_id] == loaded_col[:local_id]
assert [loaded_com] = MoodleNet.list_communities_with_collection(collection)
assert community[:local_id] == loaded_com[:local_id]
end
end
describe "list_resources" do
test "works" do
community = Factory.community()
collection = Factory.collection(community)
collection_2 = Factory.collection(community)
resource = Factory.resource(collection)
resource_2 = Factory.resource(collection_2)
assert [loaded_col] = MoodleNet.list_resources(collection)
assert resource[:local_id] == loaded_col[:local_id]
assert [loaded_col] = MoodleNet.list_resources(collection_2)
assert resource_2[:local_id] == loaded_col[:local_id]
end
end
describe "list_comments" do
test "works" do
community = Factory.community()
community_2 = Factory.community()
actor = Factory.actor()
actor_2 = Factory.actor()
comment = Factory.comment(actor, community)
comment_2 = Factory.comment(actor_2, community_2)
assert [loaded_com] = MoodleNet.list_comments(%{context: community[:local_id]})
assert comment[:local_id] == loaded_com[:local_id]
assert [loaded_com] = MoodleNet.list_comments(%{context: community_2[:local_id]})
assert comment_2[:local_id] == loaded_com[:local_id]
assert [loaded_com] = MoodleNet.list_comments(%{attributed_to: actor[:local_id]})
assert comment[:local_id] == loaded_com[:local_id]
assert [loaded_com] = MoodleNet.list_comments(%{attributed_to: actor_2[:local_id]})
assert comment_2[:local_id] == loaded_com[:local_id]
end
end
end
......@@ -117,6 +117,8 @@ config :moodle_net, :suggestions,
limit: 23,
web: "https://vinayaka.distsn.org/?{{host}}+{{user}}"
config :moodle_net, MoodleNetWeb.Gettext, default_locale: "en", locales: ~w(en es)
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
HOSTNAME=localhost
SECRET_KEY_BASE="U1QXlca4ZEZKb1o3HL/aUlznI1qstCNAQ6yme/lFbFIs0Iqiq/annZ+Ty8JyUCDc"
DATABASE_HOST=db
DATABASE_USER=postgres
DATABASE_PASS=postgres
DATABASE_NAME=moodle_net_prod
PORT=4000
LANG=en_US.UTF-8
REPLACE_OS_VARS=true
ERLANG_COOKIE=moodle_net_cookie
......@@ -14,8 +14,13 @@ use Mix.Config
# manifest is generated by the mix phoenix.digest task
# which you typically run after static files are built.
config :moodle_net, MoodleNetWeb.Endpoint,
http: [port: 4000],
protocol: "http"
http: [port: {:system, "PORT"}],
# This is critical for ensuring web-sockets properly authorize.
url: [host: {:system, "HOSTNAME"}, port: {:system, "PORT"}],
cache_static_manifest: "priv/static/cache_manifest.json",
server: true,
root: ".",
version: Application.spec(:moodle_net, :vsn)
# Do not print debug messages in production
config :logger, level: :info
......@@ -60,4 +65,4 @@ config :logger, level: :info
# Finally import the config/prod.secret.exs
# which should be versioned separately.
import_config "prod.secret.exs"
# import_config "prod.secret.exs"
version: '3.5'
services:
web:
# Pull the image from dockerhub
# image: "moodlenet/moodlenet:latest"
# Build the image first with: make build
image: "moodlenet:latest"
ports:
- "4000:4000"
env_file:
- config/docker.env
depends_on:
- db
db:
image: postgres:11-alpine
# volumes:
# - "./volumes/postgres:/var/lib/postgresql/data"
# ports:
# - "5432:5432"
env_file:
- config/docker.env
defmodule ActivityPub do
alias ActivityPub.{IRI, Object}
alias MoodleNet.Repo
defdelegate new(params), to: ActivityPub.Builder
defdelegate insert(params), to: ActivityPub.SQLEntity
defdelegate get_by_local_id(params), to: ActivityPub.SQLEntity
defdelegate get_by_id(params), to: ActivityPub.SQLEntity
defdelegate reload(params), to: ActivityPub.SQL.Query
defdelegate apply(params), to: ActivityPub.ApplyAction
defdelegate parse(attrs), to: ActivityPub.Entity
defdelegate persist(entity), to: ActivityPub.SQL
defdelegate load(local_id), to: ActivityPub.SQL
# @doc """
# Returns true if the given argument is a valid ActivityPub IRI,
# otherwise, returns false.
@doc """
Returns true if the given argument is a valid ActivityPub IRI,
otherwise, returns false.
# ## Examples
## Examples
# iex> ActivityPub.valid_iri?(nil)
# false
iex> ActivityPub.valid_iri?(nil)
false
# iex> ActivityPub.valid_iri?("https://social.example/")
# true
iex> ActivityPub.valid_iri?("https://social.example/")
false
# iex> ActivityPub.valid_iri?("https://social.example/alyssa/")
# true
# """
# @spec valid_iri?(String.t()) :: boolean
# def valid_iri?(iri), do: validate_iri(iri) == :ok
iex> ActivityPub.valid_iri?("https://social.example/alyssa/")
true
"""
@spec valid_iri?(String.t()) :: boolean
def valid_iri?(iri), do: validate_iri(iri) == :ok
# @doc """
# Verifies the given argument is an ActivityPub valid IRI
# and returns the reason if not.
@doc """
Verifies the given argument is an ActivityPub valid IRI
and returns the reason if not.
# ## Examples
## Examples
# iex> ActivityPub.validate_iri(nil)
# {:error, :not_string}
iex> ActivityPub.validate_iri(nil)
{:error, :not_string}
# iex> ActivityPub.validate_iri("social.example")
# {:error, :invalid_scheme}
iex> ActivityPub.validate_iri("social.example")
{:error, :invalid_scheme}
# iex> ActivityPub.validate_iri("https://")
# {:error, :invalid_host}
iex> ActivityPub.validate_iri("https://")
{:error, :invalid_host}
iex> ActivityPub.validate_iri("https://social.example/alyssa")
:ok
"""
@spec validate_iri(String.t()) ::
:ok
| {:error, :invalid_scheme}
| {:error, :invalid_host}
| {:error, :not_string}
def validate_iri(iri), do: IRI.validate(iri)
def is_local?(iri) do
true
end
# iex> ActivityPub.validate_iri("https://social.example/alyssa")
# :ok
# """
# @spec validate_iri(String.t()) ::
# :ok
# | {:error, :invalid_scheme}
# | {:error, :invalid_host}
# | {:error, :not_string}
# def validate_iri(iri), do: IRI.validate(iri)
# alias ActivityPub.Actor
# alias Ecto.Multi
......@@ -87,24 +83,21 @@ defmodule ActivityPub do
# Multi.delete_all(multi, key, query)
# end
@doc """
Returns an object given and ID.
Options:
* `:cache` when is `true`, it uses cache to try to get the object.
This is the first option.
Default value is `true`.
* `:database` when is `true`, it uses the database like second option get the object.
This is the second option, so it is only used when cache is disabled or it couldn't be found.
Default value is `true`.
* `:external` when is `true`, it makes a request to an external server to get the object.
This is the third option, so it is only used when the database is disabled or it couldn't be found.
Default value is `true`.
"""
@spec get_object(binary, map | Keyword.t()) ::
{:ok, Object.t()} | {:error, :not_found} | {:error, :invalid_id}
def get_object(id, opts \\ %{cache: true, database: true, external: true})
def get_object(id, opts) do
end
# @doc """
# Returns an object given and ID.
# Options:
# * `:cache` when is `true`, it uses cache to try to get the object.
# This is the first option.
# Default value is `true`.
# * `:database` when is `true`, it uses the database like second option get the object.
# This is the second option, so it is only used when cache is disabled or it couldn't be found.
# Default value is `true`.
# * `:external` when is `true`, it makes a request to an external server to get the object.
# This is the third option, so it is only used when the database is disabled or it couldn't be found.
# Default value is `true`.
# """
# @spec get_object(binary, map | Keyword.t()) ::
# {:ok, Object.t()} | {:error, :not_found} | {:error, :invalid_id}
# def get_object(id, opts \\ %{cache: true, database: true, external: true})
end
defmodule ActivityPub.ApplyAction do
import ActivityPub.Guards
alias ActivityPub.SQL.{CollectionStatement, Query}
alias ActivityPub.SQLEntity
def apply(entity) when not has_type(entity, "Activity"),
do: raise(ArgumentError, "Only an Activity can be applied, received: #{inspect(entity.type)}")
def apply(activity) when has_type(activity, "Activity") do
with {:ok, activity} <- persist(activity),
:ok <- side_effect(activity),
:ok <- insert_into_inbox(activity),
:ok <- insert_into_outbox(activity),
:ok <- federate(activity),
do: {:ok, activity}
end
defp persist(activity) when has_status(activity, :new),
do: SQLEntity.insert(activity)
defp persist(activity) when has_status(activity, :loaded),
do: {:ok, activity}
defp side_effect(follow) when has_type(follow, "Follow") do
# FIXME verify type of actors and objects
following_collections =
follow.actor
|> Query.preload_assoc(:following)
|> Enum.map(& &1.following)
followers_collections =
follow.object
|> Query.preload_assoc(:followers)
|> Enum.map(& &1.followers)
CollectionStatement.add(following_collections, follow.object)
CollectionStatement.add(followers_collections, follow.actor)