Add admin interface to manage instances subscriptions

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-12-03 11:29:51 +01:00
parent 0a96d70348
commit 334d66bf5d
141 changed files with 4198 additions and 1923 deletions

View File

@@ -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"}

View File

@@ -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, _} ->

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View 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

View File

@@ -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}))

View File

@@ -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)

View File

@@ -9,6 +9,7 @@ defmodule MobilizonWeb.WebFingerController do
"""
use MobilizonWeb, :controller
plug(MobilizonWeb.Plugs.Federating)
alias Mobilizon.Service.WebFinger
@doc """

View File

@@ -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.

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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"}

View File

@@ -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}

View File

@@ -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} ->

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View 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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;">

View File

@@ -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) %>

View File

@@ -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) <>

View File

@@ -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

View File

@@ -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