diff --git a/config/config.exs b/config/config.exs index 2db9202..478f543 100644 --- a/config/config.exs +++ b/config/config.exs @@ -25,6 +25,10 @@ config :logger, :console, # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason +config :mime, :types, %{ + "application/activity+json" => ["activity+json"] +} + # 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" diff --git a/config/dev.exs b/config/dev.exs index 7241161..001ac20 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -16,6 +16,7 @@ config :clacks, Clacks.Repo, # watchers to your application. For example, we use it # with webpack to recompile .js and .css sources. config :clacks, ClacksWeb.Endpoint, + url: [scheme: "http", port: 4000], http: [port: 4000], debug_errors: true, code_reloader: true, diff --git a/config/prod.exs b/config/prod.exs index 82754fa..bbf0f87 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -10,7 +10,7 @@ use Mix.Config # which you should run after static files are built and # before starting your production server. config :clacks, ClacksWeb.Endpoint, - url: [host: "example.com", port: 80], + url: [scheme: "http", host: "example.com", port: 80], cache_static_manifest: "priv/static/cache_manifest.json" # Do not print debug messages in production diff --git a/lib/clacks/activity.ex b/lib/clacks/activity.ex index fadf672..b957ecf 100644 --- a/lib/clacks/activity.ex +++ b/lib/clacks/activity.ex @@ -1,6 +1,5 @@ defmodule Clacks.Activity do use Ecto.Schema - import Ecto.Changeset @type t() :: %__MODULE__{} diff --git a/lib/clacks/activitypub.ex b/lib/clacks/activitypub.ex new file mode 100644 index 0000000..a894513 --- /dev/null +++ b/lib/clacks/activitypub.ex @@ -0,0 +1,85 @@ +defmodule Clacks.ActivityPub do + @context ["https://www.w3.org/ns/activitystreams"] + @public "https://www.w3.org/ns/activitystreams#Public" + + @spec note( + actor :: String.t(), + html :: String.t(), + context :: String.t(), + id :: String.t() | nil, + published :: DateTime.t(), + to :: [String.t()], + cc :: [String.t()] + ) :: map() + def note( + actor, + html, + context, + id \\ nil, + published \\ DateTime.utc_now(), + to \\ [@public], + cc \\ [] + ) do + id = id || object_id(Ecto.UUID.generate()) + + %{ + "@context" => @context, + "id" => id, + "url" => id, + "type" => "Note", + "actor" => actor, + "attributedTo" => actor, + "to" => to, + "cc" => cc, + "content" => html, + "conversation" => context, + "context" => context, + "published" => published |> DateTime.to_iso8601() + } + end + + @spec create( + object :: map(), + id :: String.t() | nil, + actor :: String.t() | nil, + to :: [String.t()] | nil, + cc :: [String.t()] | nil + ) :: map() + def create(object, id \\ nil, actor \\ nil, to \\ nil, cc \\ nil) do + %{ + "@context" => @context, + "id" => id || activity_id(Ecto.UUID.generate()), + "actor" => actor || object["actor"], + "type" => "Create", + "object" => object, + "to" => to || object["to"], + "cc" => cc || object["cc"] + } + end + + @spec object_id(id :: String.t()) :: String.t() + def object_id(id) do + url = Application.get_env(:clacks, ClacksWeb.Endpoint)[:url] + + %URI{ + scheme: url[:scheme], + host: url[:host], + port: url[:port], + path: Path.join("/objects", id) + } + |> URI.to_string() + end + + @spec activity_id(id :: String.t()) :: String.t() + def activity_id(id) do + url = Application.get_env(:clacks, ClacksWeb.Endpoint)[:url] + + %URI{ + scheme: url[:scheme], + host: url[:host], + port: url[:port], + path: Path.join("/activities", id) + } + |> URI.to_string() + end +end diff --git a/lib/clacks/object.ex b/lib/clacks/object.ex index 609b932..73e5fc1 100644 --- a/lib/clacks/object.ex +++ b/lib/clacks/object.ex @@ -1,5 +1,6 @@ defmodule Clacks.Object do use Ecto.Schema + import Ecto.Changeset @type t() :: %__MODULE__{} @@ -8,4 +9,10 @@ defmodule Clacks.Object do timestamps() end + + def changeset(%__MODULE__{} = schema, attrs) do + schema + |> cast(attrs, [:data]) + |> validate_required([:data]) + end end diff --git a/lib/clacks_web/controllers/objects_controller.ex b/lib/clacks_web/controllers/objects_controller.ex new file mode 100644 index 0000000..72b4a69 --- /dev/null +++ b/lib/clacks_web/controllers/objects_controller.ex @@ -0,0 +1,21 @@ +defmodule ClacksWeb.ObjectsController do + use ClacksWeb, :controller + alias Clacks.{Repo, Object, ActivityPub} + import Ecto.Query + + def get(conn, %{"id" => id}) do + object_id = current_url(conn) + query = from(o in Object, where: fragment("?->>'id'", o.data) == ^object_id) + + case Repo.one(query) do + nil -> + conn + |> put_status(404) + + object -> + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(object.data) + end + end +end diff --git a/lib/clacks_web/router.ex b/lib/clacks_web/router.ex index a1b70b3..d9c9a17 100644 --- a/lib/clacks_web/router.ex +++ b/lib/clacks_web/router.ex @@ -9,8 +9,8 @@ defmodule ClacksWeb.Router do plug :put_secure_browser_headers end - pipeline :api do - plug :accepts, ["json"] + pipeline :activitypub do + plug :accepts, ["activity+json", "html"] end scope "/", ClacksWeb do @@ -19,6 +19,12 @@ defmodule ClacksWeb.Router do get "/", PageController, :index end + scope "/", ClacksWeb do + pipe_through :activitypub + + get "/objects/:id", ObjectsController, :get + end + # Other scopes may use custom stacks. # scope "/api", ClacksWeb do # pipe_through :api