Add admin interface to manage instances subscriptions
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -12,8 +12,8 @@ defmodule MobilizonWeb.API.Follows do
|
||||
|
||||
def follow(%Actor{} = follower, %Actor{} = followed) do
|
||||
case ActivityPub.follow(follower, followed) do
|
||||
{:ok, activity, _} ->
|
||||
{:ok, activity}
|
||||
{:ok, activity, follow} ->
|
||||
{:ok, activity, follow}
|
||||
|
||||
e ->
|
||||
Logger.warn("Error while following actor: #{inspect(e)}")
|
||||
@@ -23,8 +23,8 @@ defmodule MobilizonWeb.API.Follows do
|
||||
|
||||
def unfollow(%Actor{} = follower, %Actor{} = followed) do
|
||||
case ActivityPub.unfollow(follower, followed) do
|
||||
{:ok, activity, _} ->
|
||||
{:ok, activity}
|
||||
{:ok, activity, follow} ->
|
||||
{:ok, activity, follow}
|
||||
|
||||
e ->
|
||||
Logger.warn("Error while unfollowing actor: #{inspect(e)}")
|
||||
@@ -33,15 +33,35 @@ defmodule MobilizonWeb.API.Follows do
|
||||
end
|
||||
|
||||
def accept(%Actor{} = follower, %Actor{} = followed) do
|
||||
Logger.debug("We're trying to accept a follow")
|
||||
|
||||
with %Follower{approved: false} = follow <-
|
||||
Actors.is_following(follower, followed),
|
||||
{:ok, %Activity{} = activity, %Follower{approved: true}} <-
|
||||
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
|
||||
ActivityPub.accept(
|
||||
:follow,
|
||||
follow,
|
||||
%{approved: true}
|
||||
true
|
||||
) do
|
||||
{:ok, activity}
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
%Follower{approved: true} ->
|
||||
{:error, "Follow already accepted"}
|
||||
end
|
||||
end
|
||||
|
||||
def reject(%Actor{} = follower, %Actor{} = followed) do
|
||||
Logger.debug("We're trying to reject a follow")
|
||||
|
||||
with %Follower{} = follow <-
|
||||
Actors.is_following(follower, followed),
|
||||
{:ok, %Activity{} = activity, %Follower{} = follow} <-
|
||||
ActivityPub.reject(
|
||||
:follow,
|
||||
follow,
|
||||
true
|
||||
) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
%Follower{approved: true} ->
|
||||
{:error, "Follow already accepted"}
|
||||
|
||||
@@ -17,7 +17,8 @@ defmodule MobilizonWeb.API.Groups do
|
||||
args |> Map.get(:preferred_username) |> HtmlSanitizeEx.strip_tags() |> String.trim(),
|
||||
{:existing_group, nil} <-
|
||||
{:existing_group, Actors.get_local_group_by_title(preferred_username)},
|
||||
{:ok, %Activity{} = activity, %Actor{} = group} <- ActivityPub.create(:group, args, true) do
|
||||
{:ok, %Activity{} = activity, %Actor{} = group} <-
|
||||
ActivityPub.create(:group, args, true, %{"actor" => args.creator_actor.url}) do
|
||||
{:ok, activity, group}
|
||||
else
|
||||
{:existing_group, _} ->
|
||||
|
||||
@@ -4,7 +4,6 @@ defmodule MobilizonWeb.API.Participations do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias MobilizonWeb.Email.Participation
|
||||
@@ -36,16 +35,13 @@ defmodule MobilizonWeb.API.Participations do
|
||||
%Participant{} = participation,
|
||||
%Actor{} = moderator
|
||||
) do
|
||||
with {:ok, activity, _} <-
|
||||
with {:ok, activity, %Participant{role: :participant} = participation} <-
|
||||
ActivityPub.accept(
|
||||
:join,
|
||||
participation,
|
||||
%{role: :participant},
|
||||
true,
|
||||
%{"to" => [moderator.url]}
|
||||
%{"actor" => moderator.url}
|
||||
),
|
||||
{:ok, %Participant{role: :participant} = participation} <-
|
||||
Events.update_participant(participation, %{"role" => :participant}),
|
||||
:ok <- Participation.send_emails_to_local_user(participation) do
|
||||
{:ok, activity, participation}
|
||||
end
|
||||
@@ -55,17 +51,12 @@ defmodule MobilizonWeb.API.Participations do
|
||||
%Participant{} = participation,
|
||||
%Actor{} = moderator
|
||||
) do
|
||||
with {:ok, activity, _} <-
|
||||
with {:ok, activity, %Participant{role: :rejected} = participation} <-
|
||||
ActivityPub.reject(
|
||||
%{
|
||||
to: [participation.actor.url],
|
||||
actor: moderator.url,
|
||||
object: participation.url
|
||||
},
|
||||
"#{MobilizonWeb.Endpoint.url()}/reject/join/#{participation.id}"
|
||||
:join,
|
||||
participation,
|
||||
%{"actor" => moderator.url}
|
||||
),
|
||||
{:ok, %Participant{role: :rejected} = participation} <-
|
||||
Events.update_participant(participation, %{"role" => :rejected}),
|
||||
:ok <- Participation.send_emails_to_local_user(participation) do
|
||||
{:ok, activity, participation}
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ defmodule MobilizonWeb.API.Reports do
|
||||
Create a report/flag on an actor, and optionally on an event or on comments.
|
||||
"""
|
||||
def report(args) do
|
||||
case {:make_activity, ActivityPub.flag(args, Map.get(args, :local, false) == false)} do
|
||||
case {:make_activity, ActivityPub.flag(args, Map.get(args, :forward, false) == true)} do
|
||||
{:make_activity, {:ok, %Activity{} = activity, %Report{} = report}} ->
|
||||
{:ok, activity, report}
|
||||
|
||||
|
||||
11
lib/mobilizon_web/cache/activity_pub.ex
vendored
11
lib/mobilizon_web/cache/activity_pub.ex
vendored
@@ -3,10 +3,12 @@ defmodule MobilizonWeb.Cache.ActivityPub do
|
||||
The ActivityPub related functions.
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Events, Service}
|
||||
alias Mobilizon.{Actors, Events, Service, Tombstone}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.{Comment, Event}
|
||||
alias Service.ActivityPub
|
||||
alias MobilizonWeb.Router.Helpers, as: Routes
|
||||
alias MobilizonWeb.Endpoint
|
||||
|
||||
@cache :activity_pub
|
||||
|
||||
@@ -39,7 +41,12 @@ defmodule MobilizonWeb.Cache.ActivityPub do
|
||||
{:commit, event}
|
||||
|
||||
nil ->
|
||||
{:ignore, nil}
|
||||
with url <- Routes.page_url(Endpoint, :event, uuid),
|
||||
%Tombstone{} = tomstone <- Tombstone.find_tombstone(url) do
|
||||
tomstone
|
||||
else
|
||||
_ -> {:ignore, nil}
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
28
lib/mobilizon_web/channels/graphql_socket.ex
Normal file
28
lib/mobilizon_web/channels/graphql_socket.ex
Normal file
@@ -0,0 +1,28 @@
|
||||
defmodule MobilizonWeb.GraphQLSocket do
|
||||
use Phoenix.Socket
|
||||
|
||||
use Absinthe.Phoenix.Socket,
|
||||
schema: MobilizonWeb.Schema
|
||||
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
def connect(%{"token" => token}, socket) do
|
||||
with {:ok, authed_socket} <-
|
||||
Guardian.Phoenix.Socket.authenticate(socket, MobilizonWeb.Guardian, token),
|
||||
%User{} = user <- Guardian.Phoenix.Socket.current_resource(authed_socket) do
|
||||
authed_socket =
|
||||
Absinthe.Phoenix.Socket.put_options(socket,
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
)
|
||||
|
||||
{:ok, authed_socket}
|
||||
else
|
||||
{:error, _} ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def id(_socket), do: nil
|
||||
end
|
||||
@@ -17,6 +17,7 @@ defmodule MobilizonWeb.ActivityPubController do
|
||||
|
||||
action_fallback(:errors)
|
||||
|
||||
plug(MobilizonWeb.Plugs.Federating when action in [:inbox, :relay])
|
||||
plug(:relay_active? when action in [:relay])
|
||||
|
||||
def relay_active?(conn, _) do
|
||||
@@ -114,7 +115,7 @@ defmodule MobilizonWeb.ActivityPubController do
|
||||
end
|
||||
|
||||
def relay(conn, _params) do
|
||||
with {:commit, %Actor{} = actor} <- Cache.get_relay() 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}))
|
||||
|
||||
@@ -28,13 +28,22 @@ defmodule MobilizonWeb.PageController do
|
||||
|
||||
defp render_or_error(conn, check_fn, status, object_type, object) do
|
||||
if check_fn.(status, object) do
|
||||
render(conn, object_type, object: object)
|
||||
case object do
|
||||
%Mobilizon.Tombstone{} ->
|
||||
conn
|
||||
|> put_status(:gone)
|
||||
|> render(object_type, object: object)
|
||||
|
||||
_ ->
|
||||
render(conn, object_type, object: object)
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp is_visible?(%{visibility: v}), do: v in [:public, :unlisted]
|
||||
defp is_visible?(%Mobilizon.Tombstone{}), do: true
|
||||
|
||||
defp ok_status?(status), do: status in [:ok, :commit]
|
||||
defp ok_status?(status, _), do: ok_status?(status)
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule MobilizonWeb.WebFingerController do
|
||||
"""
|
||||
use MobilizonWeb, :controller
|
||||
|
||||
plug(MobilizonWeb.Plugs.Federating)
|
||||
alias Mobilizon.Service.WebFinger
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -3,6 +3,7 @@ defmodule MobilizonWeb.Endpoint do
|
||||
Endpoint for Mobilizon app
|
||||
"""
|
||||
use Phoenix.Endpoint, otp_app: :mobilizon
|
||||
use Absinthe.Phoenix.Endpoint
|
||||
|
||||
# For e2e tests
|
||||
if Application.get_env(:mobilizon, :sql_sandbox) do
|
||||
@@ -13,6 +14,11 @@ defmodule MobilizonWeb.Endpoint do
|
||||
)
|
||||
end
|
||||
|
||||
socket("/graphql_socket", MobilizonWeb.GraphQLSocket,
|
||||
websocket: true,
|
||||
longpoll: false
|
||||
)
|
||||
|
||||
plug(MobilizonWeb.Plugs.UploadedMedia)
|
||||
|
||||
# Serve at "/" the static files from "priv/static" directory.
|
||||
|
||||
@@ -22,30 +22,36 @@ defmodule MobilizonWeb.HTTPSignaturePlug do
|
||||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
[signature | _] = get_req_header(conn, "signature")
|
||||
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}"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
conn =
|
||||
if conn.assigns[:digest] do
|
||||
conn
|
||||
|> put_req_header("digest", conn.assigns[:digest])
|
||||
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
|
||||
|
||||
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
|
||||
else
|
||||
Logger.debug("No signature header!")
|
||||
conn
|
||||
_ ->
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
27
lib/mobilizon_web/plugs/federating.ex
Normal file
27
lib/mobilizon_web/plugs/federating.ex
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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
|
||||
79
lib/mobilizon_web/plugs/mapped_signature_to_identity.ex
Normal file
79
lib/mobilizon_web/plugs/mapped_signature_to_identity.ex
Normal file
@@ -0,0 +1,79 @@
|
||||
# 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
|
||||
"""
|
||||
alias Mobilizon.Service.HTTPSignatures.Signature
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.ActivityPub.Utils
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
|
||||
import Plug.Conn
|
||||
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
|
||||
@@ -6,11 +6,15 @@ defmodule MobilizonWeb.Resolvers.Admin do
|
||||
import Mobilizon.Users.Guards
|
||||
|
||||
alias Mobilizon.Admin.ActionLog
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Comment}
|
||||
alias Mobilizon.Reports.{Note, Report}
|
||||
alias Mobilizon.Service.Statistics
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Service.ActivityPub.Relay
|
||||
|
||||
def list_action_logs(
|
||||
_parent,
|
||||
@@ -136,4 +140,76 @@ defmodule MobilizonWeb.Resolvers.Admin do
|
||||
def get_dashboard(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in and an administrator to access dashboard statistics"}
|
||||
end
|
||||
|
||||
def list_relay_followers(_parent, %{page: page, limit: limit}, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_admin(role) do
|
||||
with %Actor{} = relay_actor <- Relay.get_actor() do
|
||||
%Page{} =
|
||||
page = Actors.list_external_followers_for_actor_paginated(relay_actor, page, limit)
|
||||
|
||||
{:ok, page}
|
||||
end
|
||||
end
|
||||
|
||||
def list_relay_followings(_parent, %{page: page, limit: limit}, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_admin(role) do
|
||||
with %Actor{} = relay_actor <- Relay.get_actor() do
|
||||
%Page{} =
|
||||
page = Actors.list_external_followings_for_actor_paginated(relay_actor, page, limit)
|
||||
|
||||
{:ok, page}
|
||||
end
|
||||
end
|
||||
|
||||
def create_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
|
||||
when is_admin(role) do
|
||||
case Relay.follow(address) do
|
||||
{:ok, _activity, follow} ->
|
||||
{:ok, follow}
|
||||
|
||||
{:error, {:error, err}} when is_bitstring(err) ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
def remove_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
|
||||
when is_admin(role) do
|
||||
case Relay.unfollow(address) do
|
||||
{:ok, _activity, follow} ->
|
||||
{:ok, follow}
|
||||
|
||||
{:error, {:error, err}} when is_bitstring(err) ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
def accept_subscription(_parent, %{address: address}, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_admin(role) do
|
||||
case Relay.accept(address) do
|
||||
{:ok, _activity, follow} ->
|
||||
{:ok, follow}
|
||||
|
||||
{:error, {:error, err}} when is_bitstring(err) ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
def reject_subscription(_parent, %{address: address}, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_admin(role) do
|
||||
case Relay.reject(address) do
|
||||
{:ok, _activity, follow} ->
|
||||
{:ok, follow}
|
||||
|
||||
{:error, {:error, err}} when is_bitstring(err) ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -245,6 +245,9 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
{:error,
|
||||
"Participant #{id} can't be approved since it's already a participant (with role #{role})"}
|
||||
|
||||
{:has_participation, nil} ->
|
||||
{:error, "Participant not found"}
|
||||
|
||||
{:actor_approve_permission, _} ->
|
||||
{:error, "Provided moderator actor ID doesn't have permission on this event"}
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ defmodule MobilizonWeb.Resolvers.Group do
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with creator_actor_id <- Map.get(args, :creator_actor_id),
|
||||
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, creator_actor_id),
|
||||
args <- Map.put(args, :creator_actor, actor),
|
||||
{:is_owned, %Actor{} = creator_actor} <- User.owns_actor(user, creator_actor_id),
|
||||
args <- Map.put(args, :creator_actor, creator_actor),
|
||||
{:ok, _activity, %Actor{type: :Group} = group} <-
|
||||
API.Groups.create_group(args) do
|
||||
{:ok, group}
|
||||
|
||||
@@ -97,7 +97,7 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
{:find_actor, Actors.get_actor(id)},
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
|
||||
args <- save_attached_pictures(args),
|
||||
{:ok, actor} <- Actors.update_actor(actor, args) do
|
||||
{:ok, _activity, %Actor{} = actor} <- ActivityPub.update(:actor, actor, args, true) do
|
||||
{:ok, actor}
|
||||
else
|
||||
{:find_actor, nil} ->
|
||||
|
||||
@@ -16,6 +16,7 @@ defmodule MobilizonWeb.Router do
|
||||
pipeline :activity_pub_signature do
|
||||
plug(:accepts, ["activity-json", "html"])
|
||||
plug(MobilizonWeb.HTTPSignaturePlug)
|
||||
plug(MobilizonWeb.Plugs.MappedSignatureToIdentity)
|
||||
end
|
||||
|
||||
pipeline :relay do
|
||||
@@ -91,6 +92,8 @@ defmodule MobilizonWeb.Router do
|
||||
|
||||
scope "/", MobilizonWeb do
|
||||
pipe_through(:activity_pub_and_html)
|
||||
pipe_through(:activity_pub_signature)
|
||||
|
||||
get("/@:name", PageController, :actor)
|
||||
get("/events/:uuid", PageController, :event)
|
||||
get("/comments/:uuid", PageController, :comment)
|
||||
|
||||
@@ -20,6 +20,7 @@ defmodule MobilizonWeb.Schema do
|
||||
import_types(MobilizonWeb.Schema.ActorInterface)
|
||||
import_types(MobilizonWeb.Schema.Actors.PersonType)
|
||||
import_types(MobilizonWeb.Schema.Actors.GroupType)
|
||||
import_types(MobilizonWeb.Schema.Actors.ApplicationType)
|
||||
import_types(MobilizonWeb.Schema.CommentType)
|
||||
import_types(MobilizonWeb.Schema.SearchType)
|
||||
import_types(MobilizonWeb.Schema.ConfigType)
|
||||
@@ -140,5 +141,13 @@ defmodule MobilizonWeb.Schema do
|
||||
import_fields(:feed_token_mutations)
|
||||
import_fields(:picture_mutations)
|
||||
import_fields(:report_mutations)
|
||||
import_fields(:admin_mutations)
|
||||
end
|
||||
|
||||
@desc """
|
||||
Root subscription
|
||||
"""
|
||||
subscription do
|
||||
import_fields(:person_subscriptions)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,13 +3,10 @@ defmodule MobilizonWeb.Schema.ActorInterface do
|
||||
Schema representation for Actor
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.{Events}
|
||||
|
||||
import_types(MobilizonWeb.Schema.Actors.FollowerType)
|
||||
import_types(MobilizonWeb.Schema.EventType)
|
||||
# import_types(MobilizonWeb.Schema.PictureType)
|
||||
|
||||
@desc "An ActivityPub actor"
|
||||
interface :actor do
|
||||
@@ -21,7 +18,6 @@ defmodule MobilizonWeb.Schema.ActorInterface do
|
||||
field(:local, :boolean, description: "If the actor is from this instance")
|
||||
field(:summary, :string, description: "The actor's summary")
|
||||
field(:preferred_username, :string, description: "The actor's preferred username")
|
||||
field(:keys, :string, description: "The actors RSA Keys")
|
||||
|
||||
field(:manually_approves_followers, :boolean,
|
||||
description: "Whether the actors manually approves followers"
|
||||
@@ -38,17 +34,6 @@ defmodule MobilizonWeb.Schema.ActorInterface do
|
||||
field(:followersCount, :integer, description: "Number of followers for this actor")
|
||||
field(:followingCount, :integer, description: "Number of actors following this actor")
|
||||
|
||||
# This one should have a privacy setting
|
||||
field(:organized_events, list_of(:event),
|
||||
resolve: dataloader(Events),
|
||||
description: "A list of the events this actor has organized"
|
||||
)
|
||||
|
||||
# This one is for the person itself **only**
|
||||
# field(:feed, list_of(:event), description: "List of events the actor sees in his or her feed")
|
||||
|
||||
# field(:memberships, list_of(:member))
|
||||
|
||||
resolve_type(fn
|
||||
%Actor{type: :Person}, _ ->
|
||||
:person
|
||||
@@ -56,6 +41,9 @@ defmodule MobilizonWeb.Schema.ActorInterface do
|
||||
%Actor{type: :Group}, _ ->
|
||||
:group
|
||||
|
||||
%Actor{type: :Application}, _ ->
|
||||
:application
|
||||
|
||||
_, _ ->
|
||||
nil
|
||||
end)
|
||||
|
||||
38
lib/mobilizon_web/schema/actors/application.ex
Normal file
38
lib/mobilizon_web/schema/actors/application.ex
Normal file
@@ -0,0 +1,38 @@
|
||||
defmodule MobilizonWeb.Schema.Actors.ApplicationType do
|
||||
@moduledoc """
|
||||
Schema representation for Group.
|
||||
"""
|
||||
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
@desc """
|
||||
Represents an application
|
||||
"""
|
||||
object :application do
|
||||
interfaces([:actor])
|
||||
|
||||
field(:id, :id, description: "Internal ID for this application")
|
||||
field(:url, :string, description: "The ActivityPub actor's URL")
|
||||
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
|
||||
field(:name, :string, description: "The actor's displayed name")
|
||||
field(:domain, :string, description: "The actor's domain if (null if it's this instance)")
|
||||
field(:local, :boolean, description: "If the actor is from this instance")
|
||||
field(:summary, :string, description: "The actor's summary")
|
||||
field(:preferred_username, :string, description: "The actor's preferred username")
|
||||
|
||||
field(:manually_approves_followers, :boolean,
|
||||
description: "Whether the actors manually approves followers"
|
||||
)
|
||||
|
||||
field(:suspended, :boolean, description: "If the actor is suspended")
|
||||
|
||||
field(:avatar, :picture, description: "The actor's avatar picture")
|
||||
field(:banner, :picture, description: "The actor's banner picture")
|
||||
|
||||
# These one should have a privacy setting
|
||||
field(:following, list_of(:follower), description: "List of followings")
|
||||
field(:followers, list_of(:follower), description: "List of followers")
|
||||
field(:followersCount, :integer, description: "Number of followers for this actor")
|
||||
field(:followingCount, :integer, description: "Number of actors following this actor")
|
||||
end
|
||||
end
|
||||
@@ -14,5 +14,13 @@ defmodule MobilizonWeb.Schema.Actors.FollowerType do
|
||||
field(:approved, :boolean,
|
||||
description: "Whether the follow has been approved by the target actor"
|
||||
)
|
||||
|
||||
field(:inserted_at, :datetime, description: "When the follow was created")
|
||||
field(:updated_at, :datetime, description: "When the follow was updated")
|
||||
end
|
||||
|
||||
object :paginated_follower_list do
|
||||
field(:elements, list_of(:follower), description: "A list of followers")
|
||||
field(:total, :integer, description: "The total number of elements in the list")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,7 +27,6 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
|
||||
field(:local, :boolean, description: "If the actor is from this instance")
|
||||
field(:summary, :string, description: "The actor's summary")
|
||||
field(:preferred_username, :string, description: "The actor's preferred username")
|
||||
field(:keys, :string, description: "The actors RSA Keys")
|
||||
|
||||
field(:manually_approves_followers, :boolean,
|
||||
description: "Whether the actors manually approves followers"
|
||||
|
||||
@@ -27,7 +27,6 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
|
||||
field(:local, :boolean, description: "If the actor is from this instance")
|
||||
field(:summary, :string, description: "The actor's summary")
|
||||
field(:preferred_username, :string, description: "The actor's preferred username")
|
||||
field(:keys, :string, description: "The actors RSA Keys")
|
||||
|
||||
field(:manually_approves_followers, :boolean,
|
||||
description: "Whether the actors manually approves followers"
|
||||
@@ -160,4 +159,14 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
|
||||
resolve(handle_errors(&Person.register_person/3))
|
||||
end
|
||||
end
|
||||
|
||||
object :person_subscriptions do
|
||||
field :event_person_participation_changed, :person do
|
||||
arg(:person_id, non_null(:id))
|
||||
|
||||
config(fn args, _ ->
|
||||
{:ok, topic: args.person_id}
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -71,5 +71,49 @@ defmodule MobilizonWeb.Schema.AdminType do
|
||||
field :dashboard, type: :dashboard do
|
||||
resolve(&Admin.get_dashboard/3)
|
||||
end
|
||||
|
||||
field :relay_followers, type: :paginated_follower_list do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
resolve(&Admin.list_relay_followers/3)
|
||||
end
|
||||
|
||||
field :relay_followings, type: :paginated_follower_list do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
arg(:order_by, :string, default_value: :updated_at)
|
||||
arg(:direction, :string, default_value: :desc)
|
||||
resolve(&Admin.list_relay_followings/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :admin_mutations do
|
||||
@desc "Add a relay subscription"
|
||||
field :add_relay, type: :follower do
|
||||
arg(:address, non_null(:string))
|
||||
|
||||
resolve(&Admin.create_relay/3)
|
||||
end
|
||||
|
||||
@desc "Delete a relay subscription"
|
||||
field :remove_relay, type: :follower do
|
||||
arg(:address, non_null(:string))
|
||||
|
||||
resolve(&Admin.remove_relay/3)
|
||||
end
|
||||
|
||||
@desc "Accept a relay subscription"
|
||||
field :accept_relay, type: :follower do
|
||||
arg(:address, non_null(:string))
|
||||
|
||||
resolve(&Admin.accept_subscription/3)
|
||||
end
|
||||
|
||||
@desc "Reject a relay subscription"
|
||||
field :reject_relay, type: :follower do
|
||||
arg(:address, non_null(:string))
|
||||
|
||||
resolve(&Admin.reject_subscription/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -75,6 +75,7 @@ defmodule MobilizonWeb.Schema.ReportType do
|
||||
arg(:reported_id, non_null(:id))
|
||||
arg(:event_id, :id, default_value: nil)
|
||||
arg(:comments_ids, list_of(:id), default_value: [])
|
||||
arg(:forward, :boolean, default_value: false)
|
||||
resolve(&Report.create_report/3)
|
||||
end
|
||||
|
||||
|
||||
@@ -35,7 +35,11 @@
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0;">
|
||||
<%= gettext "%{reporter_name} (%{reporter_username}) reported the following content.", reporter_name: @report.reporter.name, reporter_username: Mobilizon.Actors.Actor.preferred_username_and_domain(@report.reporter) %>
|
||||
<%= if @report.reporter.type == :Application and @report.reporter.preferred_username == "relay" do %>
|
||||
<%= gettext "Someone on %{instance} reported the following content.", instance: @report.reporter.domain %>
|
||||
<% else %>
|
||||
<%= gettext "%{reporter_name} (%{reporter_username}) reported the following content.", reporter_name: @report.reporter.name, reporter_username: Mobilizon.Actors.Actor.preferred_username_and_domain(@report.reporter) %>
|
||||
<% end %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -59,10 +63,10 @@
|
||||
<%= if Map.has_key?(@report, :comments) && length(@report.comments) > 0 do %>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p><%= gettext "Comments" %></p>
|
||||
<h3><%= gettext "Comments" %></h3>
|
||||
<%= for comment <- @report.comments do %>
|
||||
<p style="margin: 0;">
|
||||
<%= comment.text %>
|
||||
<%= HtmlSanitizeEx.strip_tags(comment.text) %>
|
||||
</p>
|
||||
<% end %>
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="100%" style="width: 100% !important;">
|
||||
|
||||
@@ -4,20 +4,26 @@
|
||||
|
||||
<%= if Map.has_key?(@report, :event) do %>
|
||||
<%= gettext "Event" %>
|
||||
|
||||
<%= @report.event.title %>
|
||||
<% end %>
|
||||
|
||||
|
||||
<%= if Map.has_key?(@report, :comments) && length(@report.comments) > 0 do %>
|
||||
<%= gettext "Comments" %>
|
||||
|
||||
<%= for comment <- @report.comments do %>
|
||||
<%= comment.text %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
|
||||
<%= if @report.content do %>
|
||||
<%= gettext "Reason" %>
|
||||
|
||||
<%= @report.content %>
|
||||
<% end %>
|
||||
|
||||
|
||||
View the report: <%= moderation_report_url(MobilizonWeb.Endpoint, :index, @report.id) %>
|
||||
|
||||
|
||||
@@ -148,6 +148,21 @@ defmodule MobilizonWeb.Upload do
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_upload(%{body: body, name: name} = _file, opts) do
|
||||
with :ok <- check_binary_size(body, opts.size_limit),
|
||||
tmp_path <- tempfile_for_image(body),
|
||||
{:ok, content_type, name} <- MIME.file_mime_type(tmp_path, name) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
id: UUID.generate(),
|
||||
name: name,
|
||||
tempfile: tmp_path,
|
||||
content_type: content_type,
|
||||
size: byte_size(body)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do
|
||||
with {:ok, %{size: size}} <- File.stat(path),
|
||||
true <- size <= size_limit do
|
||||
@@ -160,6 +175,23 @@ defmodule MobilizonWeb.Upload do
|
||||
|
||||
defp check_file_size(_, _), do: :ok
|
||||
|
||||
defp check_binary_size(binary, size_limit)
|
||||
when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do
|
||||
{:error, :file_too_large}
|
||||
end
|
||||
|
||||
defp check_binary_size(_, _), do: :ok
|
||||
|
||||
# Creates a tempfile using the Plug.Upload Genserver which cleans them up
|
||||
# automatically.
|
||||
defp tempfile_for_image(data) do
|
||||
{:ok, tmp_path} = Plug.Upload.random_file("temp_files")
|
||||
{:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
|
||||
IO.binwrite(tmp_file, data)
|
||||
|
||||
tmp_path
|
||||
end
|
||||
|
||||
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
|
||||
path =
|
||||
URI.encode(path, &char_unescaped?/1) <>
|
||||
|
||||
@@ -4,44 +4,13 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.ActivityPub.{Activity, Utils}
|
||||
alias Mobilizon.Service.ActivityPub.{Activity, Utils, Convertible}
|
||||
|
||||
@private_visibility_empty_collection %{elements: [], total: 0}
|
||||
|
||||
def render("actor.json", %{actor: actor}) do
|
||||
public_key = Utils.pem_to_public_key_pem(actor.keys)
|
||||
|
||||
%{
|
||||
"id" => actor.url,
|
||||
"type" => to_string(actor.type),
|
||||
"following" => actor.following_url,
|
||||
"followers" => actor.followers_url,
|
||||
"inbox" => actor.inbox_url,
|
||||
"outbox" => actor.outbox_url,
|
||||
"preferredUsername" => actor.preferred_username,
|
||||
"name" => actor.name,
|
||||
"summary" => actor.summary,
|
||||
"url" => actor.url,
|
||||
"manuallyApprovesFollowers" => actor.manually_approves_followers,
|
||||
"publicKey" => %{
|
||||
"id" => "#{actor.url}#main-key",
|
||||
"owner" => actor.url,
|
||||
"publicKeyPem" => public_key
|
||||
},
|
||||
# TODO : Make have actors have an uuid
|
||||
# "uuid" => actor.uuid
|
||||
"endpoints" => %{
|
||||
"sharedInbox" => actor.shared_inbox_url
|
||||
}
|
||||
# "icon" => %{
|
||||
# "type" => "Image",
|
||||
# "url" => User.avatar_url(actor)
|
||||
# },
|
||||
# "image" => %{
|
||||
# "type" => "Image",
|
||||
# "url" => User.banner_url(actor)
|
||||
# }
|
||||
}
|
||||
actor
|
||||
|> Convertible.model_to_as()
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
||||
@@ -4,69 +4,34 @@ defmodule MobilizonWeb.PageView do
|
||||
"""
|
||||
use MobilizonWeb, :view
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.ActivityPub.{Converter, Utils}
|
||||
alias Mobilizon.Tombstone
|
||||
alias Mobilizon.Service.ActivityPub.{Convertible, Utils}
|
||||
alias Mobilizon.Service.Metadata
|
||||
alias Mobilizon.Service.MetadataUtils
|
||||
alias Mobilizon.Service.Metadata.Instance
|
||||
alias Mobilizon.Events.{Comment, Event}
|
||||
|
||||
def render("actor.activity-json", %{conn: %{assigns: %{object: actor}}}) do
|
||||
public_key = Utils.pem_to_public_key_pem(actor.keys)
|
||||
|
||||
%{
|
||||
"id" => Actor.build_url(actor.preferred_username, :page),
|
||||
"type" => "Person",
|
||||
"following" => Actor.build_url(actor.preferred_username, :following),
|
||||
"followers" => Actor.build_url(actor.preferred_username, :followers),
|
||||
"inbox" => Actor.build_url(actor.preferred_username, :inbox),
|
||||
"outbox" => Actor.build_url(actor.preferred_username, :outbox),
|
||||
"preferredUsername" => actor.preferred_username,
|
||||
"name" => actor.name,
|
||||
"summary" => actor.summary,
|
||||
"url" => actor.url,
|
||||
"manuallyApprovesFollowers" => actor.manually_approves_followers,
|
||||
"publicKey" => %{
|
||||
"id" => "#{actor.url}#main-key",
|
||||
"owner" => actor.url,
|
||||
"publicKeyPem" => public_key
|
||||
},
|
||||
# TODO : Make have actors have an uuid
|
||||
# "uuid" => actor.uuid
|
||||
"endpoints" => %{
|
||||
"sharedInbox" => actor.shared_inbox_url
|
||||
}
|
||||
# "icon" => %{
|
||||
# "type" => "Image",
|
||||
# "url" => User.avatar_url(actor)
|
||||
# },
|
||||
# "image" => %{
|
||||
# "type" => "Image",
|
||||
# "url" => User.banner_url(actor)
|
||||
# }
|
||||
}
|
||||
def render("actor.activity-json", %{conn: %{assigns: %{object: %Actor{} = actor}}}) do
|
||||
actor
|
||||
|> Convertible.model_to_as()
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def render("event.activity-json", %{conn: %{assigns: %{object: event}}}) do
|
||||
def render("event.activity-json", %{conn: %{assigns: %{object: %Event{} = event}}}) do
|
||||
event
|
||||
|> Converter.Event.model_to_as()
|
||||
|> Convertible.model_to_as()
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def render("comment.activity-json", %{conn: %{assigns: %{object: comment}}}) do
|
||||
comment = Converter.Comment.model_to_as(comment)
|
||||
def render("event.activity-json", %{conn: %{assigns: %{object: %Tombstone{} = event}}}) do
|
||||
event
|
||||
|> Convertible.model_to_as()
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
%{
|
||||
"actor" => comment["actor"],
|
||||
"uuid" => comment["uuid"],
|
||||
# The activity should have attributedTo, not the comment itself
|
||||
# "attributedTo" => comment.attributed_to,
|
||||
"type" => "Note",
|
||||
"id" => comment["id"],
|
||||
"content" => comment["content"],
|
||||
"mediaType" => "text/html"
|
||||
# "published" => Timex.format!(comment.inserted_at, "{ISO:Extended}"),
|
||||
# "updated" => Timex.format!(comment.updated_at, "{ISO:Extended}")
|
||||
}
|
||||
def render("comment.activity-json", %{conn: %{assigns: %{object: %Comment{} = comment}}}) do
|
||||
comment
|
||||
|> Convertible.model_to_as()
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
@@ -74,7 +39,9 @@ defmodule MobilizonWeb.PageView do
|
||||
when page in ["actor.html", "event.html", "comment.html"] do
|
||||
with {:ok, index_content} <- File.read(index_file_path()) do
|
||||
tags = object |> Metadata.build_tags() |> MetadataUtils.stringify_tags()
|
||||
index_content = String.replace(index_content, "<meta name=server-injected-data>", tags)
|
||||
|
||||
index_content = replace_meta(index_content, tags)
|
||||
|
||||
{:safe, index_content}
|
||||
end
|
||||
end
|
||||
@@ -82,7 +49,9 @@ defmodule MobilizonWeb.PageView do
|
||||
def render("index.html", _assigns) do
|
||||
with {:ok, index_content} <- File.read(index_file_path()) do
|
||||
tags = Instance.build_tags() |> MetadataUtils.stringify_tags()
|
||||
index_content = String.replace(index_content, "<meta name=server-injected-data>", tags)
|
||||
|
||||
index_content = replace_meta(index_content, tags)
|
||||
|
||||
{:safe, index_content}
|
||||
end
|
||||
end
|
||||
@@ -90,4 +59,11 @@ defmodule MobilizonWeb.PageView do
|
||||
defp index_file_path do
|
||||
Path.join(Application.app_dir(:mobilizon, "priv/static"), "index.html")
|
||||
end
|
||||
|
||||
# TODO: Find why it's different in dev/prod and during tests
|
||||
defp replace_meta(index_content, tags) do
|
||||
index_content
|
||||
|> String.replace("<meta name=\"server-injected-data\" />", tags)
|
||||
|> String.replace("<meta name=server-injected-data>", tags)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user