Improve Federation boundaries

This commit is contained in:
rustra
2020-01-23 21:59:50 +01:00
parent 8ca5c0b320
commit 3577fe42e1
67 changed files with 314 additions and 227 deletions

View File

@@ -3,12 +3,10 @@ defmodule MobilizonWeb.API.Reports do
API for Reports.
"""
import Mobilizon.Service.Admin.ActionLog
alias Mobilizon.Actors.Actor
alias Mobilizon.{Admin, Users}
alias Mobilizon.Reports, as: ReportsAction
alias Mobilizon.Reports.{Note, Report, ReportStatus}
alias Mobilizon.Users
alias Mobilizon.Users.User
alias Mobilizon.Federation.ActivityPub
@@ -34,7 +32,7 @@ defmodule MobilizonWeb.API.Reports do
with {:valid_state, true} <-
{:valid_state, ReportStatus.valid_value?(state)},
{:ok, report} <- ReportsAction.update_report(report, %{"status" => state}),
{:ok, _} <- log_action(actor, "update", report) do
{:ok, _} <- Admin.log_action(actor, "update", report) do
{:ok, report}
else
{:valid_state, false} -> {:error, "Unsupported state"}
@@ -58,7 +56,7 @@ defmodule MobilizonWeb.API.Reports do
"moderator_id" => moderator_id,
"content" => content
}),
{:ok, _} <- log_action(moderator, "create", note) do
{:ok, _} <- Admin.log_action(moderator, "create", note) do
{:ok, note}
else
{:role, false} ->
@@ -79,7 +77,7 @@ defmodule MobilizonWeb.API.Reports do
{:role, true} <- {:role, role in [:administrator, :moderator]},
{:ok, %Note{} = note} <-
Mobilizon.Reports.delete_note(note),
{:ok, _} <- log_action(moderator, "delete", note) do
{:ok, _} <- Admin.log_action(moderator, "delete", note) do
{:ok, note}
else
{:role, false} ->

View File

@@ -1,10 +1,11 @@
defmodule MobilizonWeb.Context do
defmodule MobilizonWeb.Auth.Context do
@moduledoc """
Guardian context for MobilizonWeb
"""
@behaviour Plug
import Plug.Conn
alias Mobilizon.Users.User
def init(opts) do
@@ -17,8 +18,7 @@ defmodule MobilizonWeb.Context do
context =
case Guardian.Plug.current_resource(conn) do
%User{} = user ->
context
|> Map.put(:current_user, user)
Map.put(context, :current_user, user)
nil ->
context

View File

@@ -1,4 +1,4 @@
defmodule MobilizonWeb.AuthErrorHandler do
defmodule MobilizonWeb.Auth.ErrorHandler do
@moduledoc """
In case we have an auth error
"""

View File

@@ -1,7 +1,8 @@
defmodule MobilizonWeb.Guardian do
defmodule MobilizonWeb.Auth.Guardian do
@moduledoc """
Handles the JWT tokens encoding and decoding
"""
use Guardian,
otp_app: :mobilizon,
permissions: %{
@@ -11,6 +12,7 @@ defmodule MobilizonWeb.Guardian do
alias Mobilizon.Users
alias Mobilizon.Users.User
require Logger
def subject_for_token(%User{} = user, _claims) do

View File

@@ -1,14 +1,14 @@
defmodule MobilizonWeb.AuthPipeline do
defmodule MobilizonWeb.Auth.Pipeline do
@moduledoc """
Handles the app sessions
"""
use Guardian.Plug.Pipeline,
otp_app: :mobilizon,
module: MobilizonWeb.Guardian,
error_handler: MobilizonWeb.AuthErrorHandler
module: MobilizonWeb.Auth.Guardian,
error_handler: MobilizonWeb.Auth.ErrorHandler
plug(Guardian.Plug.VerifyHeader, realm: "Bearer")
plug(Guardian.Plug.LoadResource, allow_blank: true)
plug(MobilizonWeb.Context)
plug(MobilizonWeb.Auth.Context)
end

View File

@@ -1,6 +1,6 @@
defmodule MobilizonWeb.Cache.ActivityPub do
@moduledoc """
The ActivityPub related functions.
ActivityPub related cache.
"""
alias Mobilizon.{Actors, Events, Tombstone}
@@ -9,8 +9,8 @@ defmodule MobilizonWeb.Cache.ActivityPub do
alias Mobilizon.Federation.ActivityPub.Relay
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes
@cache :activity_pub

View File

@@ -8,7 +8,7 @@ defmodule MobilizonWeb.GraphQLSocket do
def connect(%{"token" => token}, socket) do
with {:ok, authed_socket} <-
Guardian.Phoenix.Socket.authenticate(socket, MobilizonWeb.Guardian, token),
Guardian.Phoenix.Socket.authenticate(socket, MobilizonWeb.Auth.Guardian, token),
%User{} = user <- Guardian.Phoenix.Socket.current_resource(authed_socket) do
authed_socket =
Absinthe.Phoenix.Socket.put_options(socket,

View File

@@ -0,0 +1,140 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/activity_pub_controller.ex
defmodule MobilizonWeb.ActivityPubController do
use MobilizonWeb, :controller
alias Mobilizon.{Actors, Config}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Federator
alias MobilizonWeb.ActivityPub.ActorView
alias MobilizonWeb.Cache
require Logger
action_fallback(:errors)
plug(MobilizonWeb.Plugs.Federating when action in [:inbox, :relay])
plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do
if Config.get([:instance, :allow_relay]) do
conn
else
conn
|> put_status(404)
|> json("Not found")
|> halt()
end
end
def following(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page),
%Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("following.json", %{actor: actor, page: page}))
end
end
def following(conn, %{"name" => name}) do
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("following.json", %{actor: actor}))
end
end
def followers(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page),
%Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("followers.json", %{actor: actor, page: page}))
end
end
def followers(conn, %{"name" => name}) do
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("followers.json", %{actor: actor}))
end
end
def outbox(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page),
%Actor{} = actor <- Actors.get_local_actor_by_name(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("outbox.json", %{actor: actor, page: page}))
end
end
def outbox(conn, %{"name" => name}) do
with %Actor{} = actor <- Actors.get_local_actor_by_name(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("outbox.json", %{actor: actor}))
end
end
# TODO: Ensure that this inbox is a recipient of the message
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
Logger.debug("Got something with valid signature inside inbox")
Federator.enqueue(:incoming_ap_doc, params)
json(conn, "ok")
end
# only accept relayed Creates
def inbox(conn, %{"type" => "Create"} = params) do
Logger.info(
"Signature missing or not from author, relayed Create message, fetching object from source"
)
ActivityPub.fetch_object_from_url(params["object"]["id"])
json(conn, "ok")
end
def inbox(conn, params) do
headers = Enum.into(conn.req_headers, %{})
if String.contains?(headers["signature"], params["actor"]) do
Logger.error(
"Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
)
Logger.debug(inspect(conn.req_headers))
end
json(conn, "error")
end
def relay(conn, _params) do
with {status, %Actor{} = actor} when status in [:commit, :ok] <- Cache.get_relay() do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("actor.json", %{actor: actor}))
end
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)
|> json("Not found")
end
def errors(conn, e) do
Logger.debug(inspect(e))
conn
|> put_status(500)
|> json("Unknown Error")
end
end

View File

@@ -12,7 +12,7 @@ defmodule MobilizonWeb.WebFingerController do
alias Mobilizon.Federation.WebFinger
plug(Mobilizon.Federation.Plugs.Federating)
plug(MobilizonWeb.Plugs.Federating)
@doc """
Provides /.well-known/host-meta

View File

@@ -149,9 +149,9 @@ defmodule MobilizonWeb.Email.User do
_ ->
case Timex.before?(
Timex.shift(Map.get(user, key), hours: 1),
DateTime.utc_now() |> DateTime.truncate(:second)
) do
Timex.shift(Map.get(user, key), hours: 1),
DateTime.utc_now() |> DateTime.truncate(:second)
) do
true ->
:ok

View File

@@ -0,0 +1,28 @@
# Portions of this file are derived from Pleroma:
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule MobilizonWeb.Plugs.Federating do
@moduledoc """
Restrict ActivityPub routes when not federating
"""
import Plug.Conn
def init(options) do
options
end
def call(conn, _opts) do
if Mobilizon.Config.get([:instance, :federating]) do
conn
else
conn
|> put_status(404)
|> Phoenix.Controller.put_view(MobilizonWeb.ErrorView)
|> Phoenix.Controller.render("404.json")
|> halt()
end
end
end

View File

@@ -0,0 +1,56 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/plugs/http_signature.ex
defmodule MobilizonWeb.Plugs.HTTPSignatures do
@moduledoc """
Plug to check HTTP Signatures on every incoming request
"""
import Plug.Conn
require Logger
def init(options) do
options
end
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
conn
end
def call(conn, _opts) do
case get_req_header(conn, "signature") do
[signature | _] ->
if signature do
# set (request-target) header to the appropriate value
# we also replace the digest header with the one we computed
conn =
conn
|> put_req_header(
"(request-target)",
String.downcase("#{conn.method}") <> " #{conn.request_path}"
)
conn =
if conn.assigns[:digest] do
conn
|> put_req_header("digest", conn.assigns[:digest])
else
conn
end
signature_valid = HTTPSignatures.validate_conn(conn)
Logger.debug("Is signature valid ? #{inspect(signature_valid)}")
assign(conn, :valid_signature, signature_valid)
else
Logger.debug("No signature header!")
conn
end
_ ->
conn
end
end
end

View File

@@ -0,0 +1,82 @@
# Portions of this file are derived from Pleroma:
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule MobilizonWeb.Plugs.MappedSignatureToIdentity do
@moduledoc """
Get actor identity from Signature when handing fetches
"""
import Plug.Conn
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Utils
alias Mobilizon.Federation.HTTPSignatures.Signature
require Logger
def init(options), do: options
@spec key_id_from_conn(Plug.Conn.t()) :: String.t() | nil
defp key_id_from_conn(conn) do
case HTTPSignatures.signature_for_conn(conn) do
%{"keyId" => key_id} ->
Signature.key_id_to_actor_url(key_id)
_ ->
nil
end
end
@spec actor_from_key_id(Plug.Conn.t()) :: Actor.t() | nil
defp actor_from_key_id(conn) do
with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(key_actor_id) do
actor
else
_ ->
nil
end
end
def call(%{assigns: %{actor: _}} = conn, _opts), do: conn
# if this has payload make sure it is signed by the same actor that made it
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
with actor_id <- Utils.get_url(actor),
{:actor, %Actor{} = actor} <- {:actor, actor_from_key_id(conn)},
{:actor_match, true} <- {:actor_match, actor.url == actor_id} do
assign(conn, :actor, actor)
else
{:actor_match, false} ->
Logger.debug("Failed to map identity from signature (payload actor mismatch)")
Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}")
assign(conn, :valid_signature, false)
# remove me once testsuite uses mapped capabilities instead of what we do now
{:actor, nil} ->
Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}")
conn
end
end
# no payload, probably a signed fetch
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
case actor_from_key_id(conn) do
%Actor{} = actor ->
assign(conn, :actor, actor)
_ ->
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
Logger.debug("key_id=#{key_id_from_conn(conn)}")
assign(conn, :valid_signature, false)
end
end
# no signature at all
def call(conn, _opts), do: conn
end

View File

@@ -3,9 +3,7 @@ defmodule MobilizonWeb.Resolvers.Comment do
Handles the comment-related GraphQL calls.
"""
import Mobilizon.Service.Admin.ActionLog
alias Mobilizon.Actors
alias Mobilizon.{Actors, Admin, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.Comment, as: CommentModel
@@ -49,7 +47,7 @@ defmodule MobilizonWeb.Resolvers.Comment do
role in [:moderator, :administrator] ->
with {:ok, res} <- do_delete_comment(comment),
%Actor{} = actor <- Actors.get_actor(actor_id) do
log_action(actor, "delete", comment)
Admin.log_action(actor, "delete", comment)
{:ok, res}
end

View File

@@ -3,11 +3,8 @@ defmodule MobilizonWeb.Resolvers.Event do
Handles the event-related GraphQL calls.
"""
import Mobilizon.Service.Admin.ActionLog
alias Mobilizon.Actors
alias Mobilizon.{Actors, Admin, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant, EventParticipantStats}
alias Mobilizon.Users.User
@@ -343,7 +340,7 @@ defmodule MobilizonWeb.Resolvers.Event do
role in [:moderator, :administrator] ->
with {:ok, res} <- do_delete_event(event, !is_local),
%Actor{} = actor <- Actors.get_actor(actor_id) do
log_action(actor, "delete", event)
Admin.log_action(actor, "delete", event)
{:ok, res}
end

View File

@@ -10,7 +10,7 @@ defmodule MobilizonWeb.Resolvers.User do
alias Mobilizon.Storage.Repo
alias Mobilizon.Users.User
alias MobilizonWeb.Email
alias MobilizonWeb.{Auth, Email}
require Logger
@@ -94,9 +94,9 @@ defmodule MobilizonWeb.Resolvers.User do
},
_context
) do
with {:ok, user, _claims} <- MobilizonWeb.Guardian.resource_from_token(refresh_token),
with {:ok, user, _claims} <- Auth.Guardian.resource_from_token(refresh_token),
{:ok, _old, {exchanged_token, _claims}} <-
MobilizonWeb.Guardian.exchange(refresh_token, ["access", "refresh"], "access"),
Auth.Guardian.exchange(refresh_token, ["access", "refresh"], "access"),
{:ok, refresh_token} <- Users.generate_refresh_token(user) do
{:ok, %{access_token: exchanged_token, refresh_token: refresh_token}}
else

View File

@@ -6,7 +6,7 @@ defmodule MobilizonWeb.Router do
pipeline :graphql do
# plug(:accepts, ["json"])
plug(MobilizonWeb.AuthPipeline)
plug(MobilizonWeb.Auth.Pipeline)
end
pipeline :well_known do
@@ -14,13 +14,13 @@ defmodule MobilizonWeb.Router do
end
pipeline :activity_pub_signature do
plug(Mobilizon.Federation.Plugs.HTTPSignatures)
plug(Mobilizon.Federation.Plugs.MappedSignatureToIdentity)
plug(MobilizonWeb.Plugs.HTTPSignatures)
plug(MobilizonWeb.Plugs.MappedSignatureToIdentity)
end
pipeline :relay do
plug(Mobilizon.Federation.Plugs.HTTPSignatures)
plug(Mobilizon.Federation.Plugs.MappedSignatureToIdentity)
plug(MobilizonWeb.Plugs.HTTPSignatures)
plug(MobilizonWeb.Plugs.MappedSignatureToIdentity)
plug(:accepts, ["activity-json", "json"])
end
@@ -58,7 +58,7 @@ defmodule MobilizonWeb.Router do
)
end
forward("/graphiql", Absinthe.Plug.GraphiQL, schema: MobilizonWeb.Schema)
## FEDERATION
scope "/.well-known", MobilizonWeb do
pipe_through(:well_known)
@@ -69,28 +69,6 @@ defmodule MobilizonWeb.Router do
get("/nodeinfo/:version", NodeInfoController, :nodeinfo)
end
scope "/", MobilizonWeb do
pipe_through(:atom_and_ical)
get("/@:name/feed/:format", FeedController, :actor)
get("/events/:uuid/export/:format", FeedController, :event)
get("/events/going/:token/:format", FeedController, :going)
end
scope "/", MobilizonWeb do
pipe_through(:browser)
# Because the "/events/:uuid" route caches all these, we need to force them
get("/events/create", PageController, :index)
get("/events/list", PageController, :index)
get("/events/me", PageController, :index)
get("/events/explore", PageController, :index)
get("/events/:uuid/edit", PageController, :index)
# This is a hack to ease link generation into emails
get("/moderation/reports/:id", PageController, :index, as: "moderation_report")
end
scope "/", MobilizonWeb do
pipe_through(:activity_pub_and_html)
pipe_through(:activity_pub_signature)
@@ -121,6 +99,34 @@ defmodule MobilizonWeb.Router do
post("/inbox", ActivityPubController, :inbox)
end
## FEED
scope "/", MobilizonWeb do
pipe_through(:atom_and_ical)
get("/@:name/feed/:format", FeedController, :actor)
get("/events/:uuid/export/:format", FeedController, :event)
get("/events/going/:token/:format", FeedController, :going)
end
## MOBILIZON
forward("/graphiql", Absinthe.Plug.GraphiQL, schema: MobilizonWeb.Schema)
scope "/", MobilizonWeb do
pipe_through(:browser)
# Because the "/events/:uuid" route caches all these, we need to force them
get("/events/create", PageController, :index)
get("/events/list", PageController, :index)
get("/events/me", PageController, :index)
get("/events/explore", PageController, :index)
get("/events/:uuid/edit", PageController, :index)
# This is a hack to ease link generation into emails
get("/moderation/reports/:id", PageController, :index, as: "moderation_report")
end
scope "/proxy/", MobilizonWeb do
pipe_through(:remote_media)

View File

@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/mime.ex
defmodule MobilizonWeb.MIME do
defmodule MobilizonWeb.Upload.MIME do
@moduledoc """
Returns the mime-type of a binary and optionally a normalized file-name.
"""

View File

@@ -27,7 +27,7 @@ defmodule MobilizonWeb.Upload do
Related behaviors:
* `MobilizonWeb.Uploaders.Uploader`
* `MobilizonWeb.Upload.Uploader`
* `MobilizonWeb.Upload.Filter`
"""
@@ -36,7 +36,7 @@ defmodule MobilizonWeb.Upload do
alias Mobilizon.Config
alias MobilizonWeb.{MIME, Upload, Uploaders}
alias MobilizonWeb.Upload.{Filter, MIME, Uploader}
require Logger
@@ -69,8 +69,8 @@ defmodule MobilizonWeb.Upload do
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Upload.Filter.filter(opts.filters, upload),
{:ok, url_spec} <- Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok, upload} <- Filter.filter(opts.filters, upload),
{:ok, url_spec} <- Uploader.put_file(opts.uploader, upload) do
{:ok,
%{
name: Map.get(opts, :description) || upload.name,
@@ -92,7 +92,7 @@ defmodule MobilizonWeb.Upload do
with opts <- get_opts(opts),
%URI{path: "/media/" <> path, host: host} <- URI.parse(url),
{:same_host, true} <- {:same_host, host == MobilizonWeb.Endpoint.host()} do
Uploaders.Uploader.remove_file(opts.uploader, path)
Uploader.remove_file(opts.uploader, path)
else
%URI{} = _uri ->
{:error, "URL doesn't match pattern"}

View File

@@ -3,12 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/uploaders/local.ex
defmodule MobilizonWeb.Uploaders.Local do
defmodule MobilizonWeb.Upload.Uploader.Local do
@moduledoc """
Local uploader for files
"""
@behaviour MobilizonWeb.Uploaders.Uploader
@behaviour MobilizonWeb.Upload.Uploader
alias Mobilizon.Config

View File

@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/uploaders/uploader.ex
defmodule MobilizonWeb.Uploaders.Uploader do
defmodule MobilizonWeb.Upload.Uploader do
@moduledoc """
Defines the contract to put and get an uploaded file to any backend.
"""

View File

@@ -0,0 +1,118 @@
defmodule MobilizonWeb.ActivityPub.ActorView do
use MobilizonWeb, :view
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
alias Mobilizon.Federation.ActivityStream.Convertible
@private_visibility_empty_collection %{elements: [], total: 0}
def render("actor.json", %{actor: actor}) do
actor
|> Convertible.model_to_as()
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{actor: actor, page: page}) do
%{total: total, elements: following} =
if Actor.is_public_visibility(actor),
do: Actors.build_followings_for_actor(actor, page),
else: @private_visibility_empty_collection
following
|> collection(actor.preferred_username, :following, page, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{actor: actor}) do
%{total: total, elements: following} =
if Actor.is_public_visibility(actor),
do: Actors.build_followings_for_actor(actor),
else: @private_visibility_empty_collection
%{
"id" => Actor.build_url(actor.preferred_username, :following),
"type" => "OrderedCollection",
"totalItems" => total,
"first" => collection(following, actor.preferred_username, :following, 1, total)
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{actor: actor, page: page}) do
%{total: total, elements: followers} =
if Actor.is_public_visibility(actor),
do: Actors.build_followers_for_actor(actor, page),
else: @private_visibility_empty_collection
followers
|> collection(actor.preferred_username, :followers, page, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{actor: actor}) do
%{total: total, elements: followers} =
if Actor.is_public_visibility(actor),
do: Actors.build_followers_for_actor(actor),
else: @private_visibility_empty_collection
%{
"id" => Actor.build_url(actor.preferred_username, :followers),
"type" => "OrderedCollection",
"totalItems" => total,
"first" => collection(followers, actor.preferred_username, :followers, 1, total)
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("outbox.json", %{actor: actor, page: page}) do
%{total: total, elements: followers} =
if Actor.is_public_visibility(actor),
do: ActivityPub.fetch_public_activities_for_actor(actor, page),
else: @private_visibility_empty_collection
followers
|> collection(actor.preferred_username, :outbox, page, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("outbox.json", %{actor: actor}) do
%{total: total, elements: followers} =
if Actor.is_public_visibility(actor),
do: ActivityPub.fetch_public_activities_for_actor(actor),
else: @private_visibility_empty_collection
%{
"id" => Actor.build_url(actor.preferred_username, :outbox),
"type" => "OrderedCollection",
"totalItems" => total,
"first" => collection(followers, actor.preferred_username, :outbox, 1, total)
}
|> Map.merge(Utils.make_json_ld_header())
end
@spec collection(list(), String.t(), atom(), integer(), integer()) :: map()
defp collection(collection, preferred_username, endpoint, page, total)
when endpoint in [:followers, :following, :outbox] do
offset = (page - 1) * 10
map = %{
"id" => Actor.build_url(preferred_username, endpoint, page: page),
"type" => "OrderedCollectionPage",
"partOf" => Actor.build_url(preferred_username, endpoint),
"orderedItems" => Enum.map(collection, &item/1)
}
if offset < total do
Map.put(map, "next", Actor.build_url(preferred_username, endpoint, page: page + 1))
end
map
end
def item(%Activity{data: %{"id" => id}}), do: id
def item(%Actor{url: url}), do: url
end

View File

@@ -0,0 +1,30 @@
defmodule MobilizonWeb.ActivityPub.ObjectView do
use MobilizonWeb, :view
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
def render("activity.json", %{activity: %Activity{local: local, data: data} = activity}) do
%{
"id" => data["id"],
"type" =>
if local do
"Create"
else
"Announce"
end,
"actor" => activity.actor,
# Not sure if needed since this is used into outbox
"published" => Timex.now(),
"to" => activity.recipients,
"object" =>
case data["type"] do
"Event" ->
render_one(data, ObjectView, "event.json", as: :event)
"Note" ->
render_one(data, ObjectView, "comment.json", as: :comment)
end
}
|> Map.merge(Utils.make_json_ld_header())
end
end