diff options
Diffstat (limited to 'lib')
55 files changed, 2085 insertions, 1029 deletions
diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex index 12d64c2fe..cd523cf7d 100644 --- a/lib/pleroma/bbs/handler.ex +++ b/lib/pleroma/bbs/handler.ex @@ -92,10 +92,10 @@ defmodule Pleroma.BBS.Handler do params = %{} - |> Map.put("type", ["Create"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) activities = [user.ap_id | Pleroma.User.following(user)] diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 06174f624..13eeaa96b 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -24,6 +24,6 @@ defmodule Pleroma.Constants do const(static_only_files, do: - ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc) + ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css) ) end diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 51bb1bda9..ce7bd2396 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -163,8 +163,8 @@ defmodule Pleroma.Conversation.Participation do |> Enum.map(fn participation -> activity_id = ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ - "user" => user, - "blocking_user" => user + user: user, + blocking_user: user }) %{ diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 3a3082e72..093b1f405 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -141,6 +141,12 @@ defmodule Pleroma.FollowingRelationship do |> where([r], r.state == ^:follow_accept) end + def outgoing_pending_follow_requests_query(%User{} = follower) do + __MODULE__ + |> where([r], r.follower_id == ^follower.id) + |> where([r], r.state == ^:follow_pending) + end + def following(%User{} = user) do following = following_query(user) diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex index 69d8c8fe0..6d205a636 100644 --- a/lib/pleroma/helpers/uri_helper.ex +++ b/lib/pleroma/helpers/uri_helper.ex @@ -17,14 +17,6 @@ defmodule Pleroma.Helpers.UriHelper do |> URI.to_string() end - def append_param_if_present(%{} = params, param_name, param_value) do - if param_value do - Map.put(params, param_name, param_value) - else - params - end - end - def maybe_add_base("/" <> uri, base), do: Path.join([base, uri]) def maybe_add_base(uri, _base), do: uri end diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex new file mode 100644 index 000000000..ab2e32e2f --- /dev/null +++ b/lib/pleroma/maps.ex @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Maps do + def put_if_present(map, key, value, value_function \\ &{:ok, &1}) when is_map(map) do + with false <- is_nil(key), + false <- is_nil(value), + {:ok, new_value} <- value_function.(value) do + Map.put(map, key, new_value) + else + _ -> map + end + end +end diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex index d43a96cd2..1b99e44f9 100644 --- a/lib/pleroma/pagination.ex +++ b/lib/pleroma/pagination.ex @@ -23,12 +23,12 @@ defmodule Pleroma.Pagination do @spec fetch_paginated(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()] def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil) - def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do + def fetch_paginated(query, %{total: true} = params, :keyset, table_binding) do total = Repo.aggregate(query, :count, :id) %{ total: total, - items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset, table_binding) + items: fetch_paginated(query, Map.drop(params, [:total]), :keyset, table_binding) } end @@ -41,7 +41,7 @@ defmodule Pleroma.Pagination do |> enforce_order(options) end - def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do + def fetch_paginated(query, %{total: true} = params, :offset, table_binding) do total = query |> Ecto.Query.exclude(:left_join) @@ -49,7 +49,7 @@ defmodule Pleroma.Pagination do %{ total: total, - items: fetch_paginated(query, Map.drop(params, ["total"]), :offset, table_binding) + items: fetch_paginated(query, Map.drop(params, [:total]), :offset, table_binding) } end @@ -90,12 +90,6 @@ defmodule Pleroma.Pagination do skip_order: :boolean } - params = - Enum.reduce(params, %{}, fn - {key, _value}, acc when is_atom(key) -> Map.drop(acc, [key]) - {key, value}, acc -> Map.put(acc, key, value) - end) - changeset = cast({%{}, param_types}, params, Map.keys(param_types)) changeset.changes end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 72ee2d58e..c5c74d132 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1489,6 +1489,8 @@ defmodule Pleroma.User do delete_user_activities(user) + delete_outgoing_pending_follow_requests(user) + delete_or_deactivate(user) end @@ -1611,6 +1613,12 @@ defmodule Pleroma.User do defp delete_activity(_activity, _user), do: "Doing nothing" + defp delete_outgoing_pending_follow_requests(user) do + user + |> FollowingRelationship.outgoing_pending_follow_requests_query() + |> Repo.delete_all() + end + def html_filter_policy(%User{no_rich_text: true}) do Pleroma.HTML.Scrubber.TwitterText end diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 293bbc082..66ffe9090 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -45,7 +45,7 @@ defmodule Pleroma.User.Query do is_admin: boolean(), is_moderator: boolean(), super_users: boolean(), - exclude_service_users: boolean(), + invisible: boolean(), followers: User.t(), friends: User.t(), recipients_from_activity: [String.t()], @@ -89,8 +89,8 @@ defmodule Pleroma.User.Query do where(query, [u], ilike(field(u, ^key), ^"%#{value}%")) end - defp compose_query({:exclude_service_users, _}, query) do - where(query, [u], not like(u.ap_id, "%/relay") and not like(u.ap_id, "%/internal/fetch")) + defp compose_query({:invisible, bool}, query) when is_boolean(bool) do + where(query, [u], u.invisible == ^bool) end defp compose_query({key, value}, query) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 7f6b59cc6..5b519033e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Constants alias Pleroma.Conversation alias Pleroma.Conversation.Participation + alias Pleroma.Maps alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Containment @@ -20,7 +21,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.User alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Streamer alias Pleroma.Web.WebFinger alias Pleroma.Workers.BackgroundWorker @@ -68,16 +68,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do {recipients, to, cc} end - defp check_actor_is_active(actor) do - if not is_nil(actor) do - with user <- User.get_cached_by_ap_id(actor), - false <- user.deactivated do - true - else - _e -> false - end - else - true + defp check_actor_is_active(nil), do: true + + defp check_actor_is_active(actor) when is_binary(actor) do + case User.get_cached_by_ap_id(actor) do + %User{deactivated: deactivated} -> not deactivated + _ -> false end end @@ -88,7 +84,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp check_remote_limit(_), do: true - def increase_note_count_if_public(actor, object) do + defp increase_note_count_if_public(actor, object) do if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor} end @@ -96,36 +92,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor} end - def increase_replies_count_if_reply(%{ - "object" => %{"inReplyTo" => reply_ap_id} = object, - "type" => "Create" - }) do + defp increase_replies_count_if_reply(%{ + "object" => %{"inReplyTo" => reply_ap_id} = object, + "type" => "Create" + }) do if is_public?(object) do Object.increase_replies_count(reply_ap_id) end end - def increase_replies_count_if_reply(_create_data), do: :noop + defp increase_replies_count_if_reply(_create_data), do: :noop - def decrease_replies_count_if_reply(%Object{ - data: %{"inReplyTo" => reply_ap_id} = object - }) do - if is_public?(object) do - Object.decrease_replies_count(reply_ap_id) - end - end - - def decrease_replies_count_if_reply(_object), do: :noop - - def increase_poll_votes_if_vote(%{ - "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, - "type" => "Create", - "actor" => actor - }) do + defp increase_poll_votes_if_vote(%{ + "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, + "type" => "Create", + "actor" => actor + }) do Object.increase_vote_count(reply_ap_id, name, actor) end - def increase_poll_votes_if_vote(_create_data), do: :noop + defp increase_poll_votes_if_vote(_create_data), do: :noop @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} def persist(object, meta) do @@ -164,12 +150,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> maybe_create_activity_expiration() # Splice in the child object if we have one. - activity = - if not is_nil(object) do - Map.put(activity, :object, object) - else - activity - end + activity = Maps.put_if_present(activity, :object, object) BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id}) @@ -214,8 +195,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp create_or_bump_conversation(activity, actor) do with {:ok, conversation} <- Conversation.create_or_bump_for(activity), - %User{} = user <- User.get_cached_by_ap_id(actor), - Participation.mark_as_read(user, conversation) do + %User{} = user <- User.get_cached_by_ap_id(actor) do + Participation.mark_as_read(user, conversation) {:ok, conversation} end end @@ -237,13 +218,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def stream_out_participations(%Object{data: %{"context" => context}}, user) do - with %Conversation{} = conversation <- Conversation.get_for_ap_id(context), - conversation = Repo.preload(conversation, :participations), - last_activity_id = - fetch_latest_activity_id_for_context(conversation.ap_id, %{ - "user" => user, - "blocking_user" => user - }) do + with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do + conversation = Repo.preload(conversation, :participations) + + last_activity_id = + fetch_latest_activity_id_for_context(conversation.ap_id, %{ + user: user, + blocking_user: user + }) + if last_activity_id do stream_out_participations(conversation.participations) end @@ -277,12 +260,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do published = params[:published] quick_insert? = Config.get([:env]) == :benchmark - with create_data <- - make_create_data( - %{to: to, actor: actor, published: published, context: context, object: object}, - additional - ), - {:ok, activity} <- insert(create_data, local, fake), + create_data = + make_create_data( + %{to: to, actor: actor, published: published, context: context, object: object}, + additional + ) + + with {:ok, activity} <- insert(create_data, local, fake), {:fake, false, activity} <- {:fake, fake, activity}, _ <- increase_replies_count_if_reply(create_data), _ <- increase_poll_votes_if_vote(create_data), @@ -310,12 +294,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do local = !(params[:local] == false) published = params[:published] - with listen_data <- - make_listen_data( - %{to: to, actor: actor, published: published, context: context, object: object}, - additional - ), - {:ok, activity} <- insert(listen_data, local), + listen_data = + make_listen_data( + %{to: to, actor: actor, published: published, context: context, object: object}, + additional + ) + + with {:ok, activity} <- insert(listen_data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -333,14 +318,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end @spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()} - def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do + defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do local = Map.get(params, :local, true) activity_id = Map.get(params, :activity_id, nil) - with data <- - %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} - |> Utils.maybe_put("id", activity_id), - {:ok, activity} <- insert(data, local), + data = + %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} + |> Maps.put_if_present("id", activity_id) + + with {:ok, activity} <- insert(data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -352,15 +338,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do local = !(params[:local] == false) activity_id = params[:activity_id] - with data <- %{ - "to" => to, - "cc" => cc, - "type" => "Update", - "actor" => actor, - "object" => object - }, - data <- Utils.maybe_put(data, "id", activity_id), - {:ok, activity} <- insert(data, local), + data = + %{ + "to" => to, + "cc" => cc, + "type" => "Update", + "actor" => actor, + "object" => object + } + |> Maps.put_if_present("id", activity_id) + + with {:ok, activity} <- insert(data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -377,8 +365,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end defp do_follow(follower, followed, activity_id, local) do - with data <- make_follow_data(follower, followed, activity_id), - {:ok, activity} <- insert(data, local), + data = make_follow_data(follower, followed, activity_id) + + with {:ok, activity} <- insert(data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -422,13 +411,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp do_block(blocker, blocked, activity_id, local) do unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) - if unfollow_blocked do - follow_activity = fetch_latest_follow(blocker, blocked) - if follow_activity, do: unfollow(blocker, blocked, nil, local) + if unfollow_blocked and fetch_latest_follow(blocker, blocked) do + unfollow(blocker, blocked, nil, local) end - with block_data <- make_block_data(blocker, blocked, activity_id), - {:ok, activity} <- insert(block_data, local), + block_data = make_block_data(blocker, blocked, activity_id) + + with {:ok, activity} <- insert(block_data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -507,8 +496,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do public = [Constants.as_public()] recipients = - if opts["user"], - do: [opts["user"].ap_id | User.following(opts["user"])] ++ public, + if opts[:user], + do: [opts[:user].ap_id | User.following(opts[:user])] ++ public, else: public from(activity in Activity) @@ -516,7 +505,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> maybe_preload_bookmarks(opts) |> maybe_set_thread_muted_field(opts) |> restrict_blocked(opts) - |> restrict_recipients(recipients, opts["user"]) + |> restrict_recipients(recipients, opts[:user]) |> where( [activity], fragment( @@ -543,7 +532,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do FlakeId.Ecto.CompatType.t() | nil def fetch_latest_activity_id_for_context(context, opts \\ %{}) do context - |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts)) + |> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts)) |> limit(1) |> select([a], a.id) |> Repo.one() @@ -551,24 +540,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do @spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()] def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do - opts = Map.drop(opts, ["user"]) + opts = Map.delete(opts, :user) - query = fetch_activities_query([Constants.as_public()], opts) - - query = - if opts["restrict_unlisted"] do - restrict_unlisted(query) - else - query - end - - Pagination.fetch_paginated(query, opts, pagination) + [Constants.as_public()] + |> fetch_activities_query(opts) + |> restrict_unlisted(opts) + |> Pagination.fetch_paginated(opts, pagination) end @spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()] def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do opts - |> Map.put("restrict_unlisted", true) + |> Map.put(:restrict_unlisted, true) |> fetch_public_or_unlisted_activities(pagination) end @@ -577,20 +560,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_visibility(query, %{visibility: visibility}) when is_list(visibility) do if Enum.all?(visibility, &(&1 in @valid_visibilities)) do - query = - from( - a in query, - where: - fragment( - "activity_visibility(?, ?, ?) = ANY (?)", - a.actor, - a.recipients, - a.data, - ^visibility - ) - ) - - query + from( + a in query, + where: + fragment( + "activity_visibility(?, ?, ?) = ANY (?)", + a.actor, + a.recipients, + a.data, + ^visibility + ) + ) else Logger.error("Could not restrict visibility to #{visibility}") end @@ -612,7 +592,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_visibility(query, _visibility), do: query - defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + defp exclude_visibility(query, %{exclude_visibilities: visibility}) when is_list(visibility) do if Enum.all?(visibility, &(&1 in @valid_visibilities)) do from( @@ -632,7 +612,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end - defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + defp exclude_visibility(query, %{exclude_visibilities: visibility}) when visibility in @valid_visibilities do from( a in query, @@ -647,7 +627,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do ) end - defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + defp exclude_visibility(query, %{exclude_visibilities: visibility}) when visibility not in [nil | @valid_visibilities] do Logger.error("Could not exclude visibility to #{visibility}") query @@ -658,14 +638,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _), do: query - defp restrict_thread_visibility( - query, - %{"user" => %User{skip_thread_containment: true}}, - _ - ), - do: query + defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: true}}, _), + do: query - defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do + defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do from( a in query, where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data) @@ -677,87 +653,79 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do params = params - |> Map.put("user", reading_user) - |> Map.put("actor_id", user.ap_id) - - recipients = - user_activities_recipients(%{ - "godmode" => params["godmode"], - "reading_user" => reading_user - }) + |> Map.put(:user, reading_user) + |> Map.put(:actor_id, user.ap_id) - fetch_activities(recipients, params) + %{ + godmode: params[:godmode], + reading_user: reading_user + } + |> user_activities_recipients() + |> fetch_activities(params) |> Enum.reverse() end def fetch_user_activities(user, reading_user, params \\ %{}) do params = params - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("user", reading_user) - |> Map.put("actor_id", user.ap_id) - |> Map.put("pinned_activity_ids", user.pinned_activities) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:user, reading_user) + |> Map.put(:actor_id, user.ap_id) + |> Map.put(:pinned_activity_ids, user.pinned_activities) params = if User.blocks?(reading_user, user) do params else params - |> Map.put("blocking_user", reading_user) - |> Map.put("muting_user", reading_user) + |> Map.put(:blocking_user, reading_user) + |> Map.put(:muting_user, reading_user) end - recipients = - user_activities_recipients(%{ - "godmode" => params["godmode"], - "reading_user" => reading_user - }) - - fetch_activities(recipients, params) + %{ + godmode: params[:godmode], + reading_user: reading_user + } + |> user_activities_recipients() + |> fetch_activities(params) |> Enum.reverse() end def fetch_statuses(reading_user, params) do - params = - params - |> Map.put("type", ["Create", "Announce"]) + params = Map.put(params, :type, ["Create", "Announce"]) - recipients = - user_activities_recipients(%{ - "godmode" => params["godmode"], - "reading_user" => reading_user - }) - - fetch_activities(recipients, params, :offset) + %{ + godmode: params[:godmode], + reading_user: reading_user + } + |> user_activities_recipients() + |> fetch_activities(params, :offset) |> Enum.reverse() end - defp user_activities_recipients(%{"godmode" => true}) do - [] - end + defp user_activities_recipients(%{godmode: true}), do: [] - defp user_activities_recipients(%{"reading_user" => reading_user}) do + defp user_activities_recipients(%{reading_user: reading_user}) do if reading_user do - [Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)] + [Constants.as_public(), reading_user.ap_id | User.following(reading_user)] else [Constants.as_public()] end end - defp restrict_since(query, %{"since_id" => ""}), do: query + defp restrict_since(query, %{since_id: ""}), do: query - defp restrict_since(query, %{"since_id" => since_id}) do + defp restrict_since(query, %{since_id: since_id}) do from(activity in query, where: activity.id > ^since_id) end defp restrict_since(query, _), do: query - defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do + defp restrict_tag_reject(_query, %{tag_reject: _tag_reject, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_tag_reject(query, %{"tag_reject" => tag_reject}) - when is_list(tag_reject) and tag_reject != [] do + defp restrict_tag_reject(query, %{tag_reject: [_ | _] = tag_reject}) do from( [_activity, object] in query, where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject) @@ -766,12 +734,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_tag_reject(query, _), do: query - defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do + defp restrict_tag_all(_query, %{tag_all: _tag_all, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_tag_all(query, %{"tag_all" => tag_all}) - when is_list(tag_all) and tag_all != [] do + defp restrict_tag_all(query, %{tag_all: [_ | _] = tag_all}) do from( [_activity, object] in query, where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all) @@ -780,18 +747,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_tag_all(query, _), do: query - defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do + defp restrict_tag(_query, %{tag: _tag, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do + defp restrict_tag(query, %{tag: tag}) when is_list(tag) do from( [_activity, object] in query, where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag) ) end - defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do + defp restrict_tag(query, %{tag: tag}) when is_binary(tag) do from( [_activity, object] in query, where: fragment("(?)->'tag' \\? (?)", object.data, ^tag) @@ -814,35 +781,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do ) end - defp restrict_local(query, %{"local_only" => true}) do + defp restrict_local(query, %{local_only: true}) do from(activity in query, where: activity.local == true) end defp restrict_local(query, _), do: query - defp restrict_actor(query, %{"actor_id" => actor_id}) do + defp restrict_actor(query, %{actor_id: actor_id}) do from(activity in query, where: activity.actor == ^actor_id) end defp restrict_actor(query, _), do: query - defp restrict_type(query, %{"type" => type}) when is_binary(type) do + defp restrict_type(query, %{type: type}) when is_binary(type) do from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type)) end - defp restrict_type(query, %{"type" => type}) do + defp restrict_type(query, %{type: type}) do from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type)) end defp restrict_type(query, _), do: query - defp restrict_state(query, %{"state" => state}) do + defp restrict_state(query, %{state: state}) do from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state)) end defp restrict_state(query, _), do: query - defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do + defp restrict_favorited_by(query, %{favorited_by: ap_id}) do from( [_activity, object] in query, where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id) @@ -851,11 +818,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_favorited_by(query, _), do: query - defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do + defp restrict_media(_query, %{only_media: _val, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do + defp restrict_media(query, %{only_media: true}) do from( [_activity, object] in query, where: fragment("not (?)->'attachment' = (?)", object.data, ^[]) @@ -864,7 +831,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_media(query, _), do: query - defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do + defp restrict_replies(query, %{exclude_replies: true}) do from( [_activity, object] in query, where: fragment("?->>'inReplyTo' is null", object.data) @@ -872,8 +839,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end defp restrict_replies(query, %{ - "reply_filtering_user" => user, - "reply_visibility" => "self" + reply_filtering_user: user, + reply_visibility: "self" }) do from( [activity, object] in query, @@ -888,8 +855,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end defp restrict_replies(query, %{ - "reply_filtering_user" => user, - "reply_visibility" => "following" + reply_filtering_user: user, + reply_visibility: "following" }) do from( [activity, object] in query, @@ -908,16 +875,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_replies(query, _), do: query - defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do + defp restrict_reblogs(query, %{exclude_reblogs: true}) do from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data)) end defp restrict_reblogs(query, _), do: query - defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query + defp restrict_muted(query, %{with_muted: true}), do: query - defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do - mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user) + defp restrict_muted(query, %{muting_user: %User{} = user} = opts) do + mutes = opts[:muted_users_ap_ids] || User.muted_users_ap_ids(user) query = from([activity] in query, @@ -925,7 +892,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes) ) - unless opts["skip_preload"] do + unless opts[:skip_preload] do from([thread_mute: tm] in query, where: is_nil(tm.user_id)) else query @@ -934,8 +901,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_muted(query, _), do: query - defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do - blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user) + defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do + blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user) domain_blocks = user.domain_blocks || [] following_ap_ids = User.get_friends_ap_ids(user) @@ -949,6 +916,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids), where: fragment( + "recipients_contain_blocked_domains(?, ?) = false", + activity.recipients, + ^domain_blocks + ), + where: + fragment( "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", activity.data, activity.data, @@ -975,7 +948,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_blocked(query, _), do: query - defp restrict_unlisted(query) do + defp restrict_unlisted(query, %{restrict_unlisted: true}) do from( activity in query, where: @@ -987,19 +960,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do ) end - # TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only, - # the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2' - # and `restrict_muted/2` + defp restrict_unlisted(query, _), do: query - defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids}) - when pinned in [true, "true", "1"] do + defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do from(activity in query, where: activity.id in ^ids) end defp restrict_pinned(query, _), do: query - defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do - muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user) + defp restrict_muted_reblogs(query, %{muting_user: %User{} = user} = opts) do + muted_reblogs = opts[:reblog_muted_users_ap_ids] || User.reblog_muted_users_ap_ids(user) from( activity in query, @@ -1015,7 +985,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_muted_reblogs(query, _), do: query - defp restrict_instance(query, %{"instance" => instance}) do + defp restrict_instance(query, %{instance: instance}) do users = from( u in User, @@ -1029,7 +999,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_instance(query, _), do: query - defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query + defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query defp exclude_poll_votes(query, _) do if has_named_binding?(query, :object) do @@ -1041,38 +1011,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end - defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do + defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query + + defp exclude_invisible_actors(query, _opts) do + invisible_ap_ids = + User.Query.build(%{invisible: true, select: [:ap_id]}) + |> Repo.all() + |> Enum.map(fn %{ap_id: ap_id} -> ap_id end) + + from([activity] in query, where: activity.actor not in ^invisible_ap_ids) + end + + defp exclude_id(query, %{exclude_id: id}) when is_binary(id) do from(activity in query, where: activity.id != ^id) end defp exclude_id(query, _), do: query - defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query + defp maybe_preload_objects(query, %{skip_preload: true}), do: query defp maybe_preload_objects(query, _) do query |> Activity.with_preloaded_object() end - defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query + defp maybe_preload_bookmarks(query, %{skip_preload: true}), do: query defp maybe_preload_bookmarks(query, opts) do query - |> Activity.with_preloaded_bookmark(opts["user"]) + |> Activity.with_preloaded_bookmark(opts[:user]) end - defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do + defp maybe_preload_report_notes(query, %{preload_report_notes: true}) do query |> Activity.with_preloaded_report_notes() end defp maybe_preload_report_notes(query, _), do: query - defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query + defp maybe_set_thread_muted_field(query, %{skip_preload: true}), do: query defp maybe_set_thread_muted_field(query, opts) do query - |> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"]) + |> Activity.with_set_thread_muted_field(opts[:muting_user] || opts[:user]) end defp maybe_order(query, %{order: :desc}) do @@ -1088,24 +1069,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp maybe_order(query, _), do: query defp fetch_activities_query_ap_ids_ops(opts) do - source_user = opts["muting_user"] + source_user = opts[:muting_user] ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: [] ap_id_relationships = - ap_id_relationships ++ - if opts["blocking_user"] && opts["blocking_user"] == source_user do - [:block] - else - [] - end + if opts[:blocking_user] && opts[:blocking_user] == source_user do + [:block | ap_id_relationships] + else + ap_id_relationships + end preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships) - restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts) - restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts) + restrict_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts) + restrict_muted_opts = Map.merge(%{muted_users_ap_ids: preloaded_ap_ids[:mute]}, opts) restrict_muted_reblogs_opts = - Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts) + Map.merge(%{reblog_muted_users_ap_ids: preloaded_ap_ids[:reblog_mute]}, opts) {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} end @@ -1124,7 +1104,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> maybe_preload_report_notes(opts) |> maybe_set_thread_muted_field(opts) |> maybe_order(opts) - |> restrict_recipients(recipients, opts["user"]) + |> restrict_recipients(recipients, opts[:user]) |> restrict_replies(opts) |> restrict_tag(opts) |> restrict_tag_reject(opts) @@ -1146,16 +1126,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_instance(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) + |> exclude_invisible_actors(opts) |> exclude_visibility(opts) end def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do - list_memberships = Pleroma.List.memberships(opts["user"]) + list_memberships = Pleroma.List.memberships(opts[:user]) fetch_activities_query(recipients ++ list_memberships, opts) |> Pagination.fetch_paginated(opts, pagination) |> Enum.reverse() - |> maybe_update_cc(list_memberships, opts["user"]) + |> maybe_update_cc(list_memberships, opts[:user]) end @doc """ @@ -1171,16 +1152,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> select([_like, object, activity], %{activity | object: object}) |> order_by([like, _, _], desc_nulls_last: like.id) |> Pagination.fetch_paginated( - Map.merge(params, %{"skip_order" => true}), + Map.merge(params, %{skip_order: true}), pagination, :object_activity ) end - defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) - when is_list(list_memberships) and length(list_memberships) > 0 do + defp maybe_update_cc(activities, [_ | _] = list_memberships, %User{ap_id: user_ap_id}) do Enum.map(activities, fn - %{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 -> + %{data: %{"bcc" => [_ | _] = bcc}} = activity -> if Enum.any?(bcc, &(&1 in list_memberships)) do update_in(activity.data["cc"], &[user_ap_id | &1]) else @@ -1194,7 +1174,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp maybe_update_cc(activities, _, _), do: activities - def fetch_activities_bounded_query(query, recipients, recipients_with_public) do + defp fetch_activities_bounded_query(query, recipients, recipients_with_public) do from(activity in query, where: fragment("? && ?", activity.recipients, ^recipients) or @@ -1218,12 +1198,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()} def upload(file, opts \\ []) do with {:ok, data} <- Upload.store(file, opts) do - obj_data = - if opts[:actor] do - Map.put(data, "actor", opts[:actor]) - else - data - end + obj_data = Maps.put_if_present(data, "actor", opts[:actor]) Repo.insert(%Object{data: obj_data}) end @@ -1269,8 +1244,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do %{"type" => "Emoji"} -> true _ -> false end) - |> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc -> - Map.put(acc, String.trim(name, ":"), url) + |> Map.new(fn %{"icon" => %{"url" => url}, "name" => name} -> + {String.trim(name, ":"), url} end) locked = data["manuallyApprovesFollowers"] || false @@ -1316,18 +1291,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do } # nickname can be nil because of virtual actors - user_data = - if data["preferredUsername"] do - Map.put( - user_data, - :nickname, - "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}" - ) - else - Map.put(user_data, :nickname, nil) - end - - {:ok, user_data} + if data["preferredUsername"] do + Map.put( + user_data, + :nickname, + "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}" + ) + else + Map.put(user_data, :nickname, nil) + end end def fetch_follow_information_for_user(user) do @@ -1402,9 +1374,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp collection_private(_data), do: {:ok, true} def user_data_from_user_object(data) do - with {:ok, data} <- MRF.filter(data), - {:ok, data} <- object_to_user_data(data) do - {:ok, data} + with {:ok, data} <- MRF.filter(data) do + {:ok, object_to_user_data(data)} else e -> {:error, e} end @@ -1412,15 +1383,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def fetch_and_prepare_user_from_ap_id(ap_id) do with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id), - {:ok, data} <- user_data_from_user_object(data), - data <- maybe_update_follow_information(data) do - {:ok, data} + {:ok, data} <- user_data_from_user_object(data) do + {:ok, maybe_update_follow_information(data)} else - {:error, "Object has been deleted"} = e -> + {:error, "Object has been deleted" = e} -> Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}") {:error, e} - e -> + {:error, e} -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") {:error, e} end @@ -1443,8 +1413,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> Repo.insert() |> User.set_cache() end - else - e -> {:error, e} end end end @@ -1458,7 +1426,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end # filter out broken threads - def contain_broken_threads(%Activity{} = activity, %User{} = user) do + defp contain_broken_threads(%Activity{} = activity, %User{} = user) do entire_thread_visible_for_user?(activity, user) end @@ -1469,7 +1437,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def fetch_direct_messages_query do Activity - |> restrict_type(%{"type" => "Create"}) + |> restrict_type(%{type: "Create"}) |> restrict_visibility(%{visibility: "direct"}) |> order_by([activity], asc: activity.id) end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 28727d619..f0b5c6e93 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.ControllerHelper alias Pleroma.Web.Endpoint alias Pleroma.Web.FederatingPlug alias Pleroma.Web.Federator @@ -230,27 +231,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do when page? in [true, "true"] do with %User{} = user <- User.get_cached_by_nickname(nickname), {:ok, user} <- User.ensure_keys_present(user) do - activities = - if params["max_id"] do - ActivityPub.fetch_user_activities(user, for_user, %{ - "max_id" => params["max_id"], - # This is a hack because postgres generates inefficient queries when filtering by - # 'Answer', poll votes will be hidden by the visibility filter in this case anyway - "include_poll_votes" => true, - "limit" => 10 - }) - else - ActivityPub.fetch_user_activities(user, for_user, %{ - "limit" => 10, - "include_poll_votes" => true - }) - end + # "include_poll_votes" is a hack because postgres generates inefficient + # queries when filtering by 'Answer', poll votes will be hidden by the + # visibility filter in this case anyway + params = + params + |> Map.drop(["nickname", "page"]) + |> Map.put("include_poll_votes", true) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) + + activities = ActivityPub.fetch_user_activities(user, for_user, params) conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) |> render("activity_collection_page.json", %{ activities: activities, + pagination: ControllerHelper.get_pagination_fields(conn, activities), iri: "#{user.ap_id}/outbox" }) end @@ -353,21 +350,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do %{"nickname" => nickname, "page" => page?} = params ) when page? in [true, "true"] do + params = + params + |> Map.drop(["nickname", "page"]) + |> Map.put("blocking_user", user) + |> Map.put("user", user) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) + activities = - if params["max_id"] do - ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{ - "max_id" => params["max_id"], - "limit" => 10 - }) - else - ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10}) - end + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + |> Enum.reverse() conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) |> render("activity_collection_page.json", %{ activities: activities, + pagination: ControllerHelper.get_pagination_fields(conn, activities), iri: "#{user.ap_id}/inbox" }) end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 8443c284c..fda1c71df 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Activity alias Pleroma.EarmarkRenderer alias Pleroma.FollowingRelationship + alias Pleroma.Maps alias Pleroma.Object alias Pleroma.Object.Containment alias Pleroma.Repo @@ -208,12 +209,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> Map.put("conversation", context) end - defp add_if_present(map, _key, nil), do: map - - defp add_if_present(map, key, value) do - Map.put(map, key, value) - end - def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do attachments = Enum.map(attachment, fn data -> @@ -241,13 +236,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do attachment_url = %{"href" => href} - |> add_if_present("mediaType", media_type) - |> add_if_present("type", Map.get(url || %{}, "type")) + |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("type", Map.get(url || %{}, "type")) %{"url" => [attachment_url]} - |> add_if_present("mediaType", media_type) - |> add_if_present("type", data["type"]) - |> add_if_present("name", data["name"]) + |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("type", data["type"]) + |> Maps.put_if_present("name", data["name"]) end) Map.put(object, "attachment", attachments) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index f2375bcc4..dfae602df 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do alias Ecto.UUID alias Pleroma.Activity alias Pleroma.Config + alias Pleroma.Maps alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -244,7 +245,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do Inserts a full object if it is contained in an activity. """ def insert_full_object(%{"object" => %{"type" => type} = object_data} = map) - when is_map(object_data) and type in @supported_object_types do + when type in @supported_object_types do with {:ok, object} <- Object.create(object_data) do map = Map.put(map, "object", object.data["id"]) @@ -307,7 +308,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => cc, "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def make_emoji_reaction_data(user, object, emoji, activity_id) do @@ -477,7 +478,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "object" => followed_id, "state" => "pending" } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do @@ -546,7 +547,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => [], "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def make_announce_data( @@ -563,7 +564,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => [Pleroma.Constants.as_public()], "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def make_undo_data( @@ -582,7 +583,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => [Pleroma.Constants.as_public()], "context" => context } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end @spec add_announce_to_object(Activity.t(), Object.t()) :: @@ -627,7 +628,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "to" => [followed.ap_id], "object" => follow_activity.data } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end #### Block-related helpers @@ -650,7 +651,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "to" => [blocked.ap_id], "object" => blocked.ap_id } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end #### Create-related helpers @@ -740,12 +741,12 @@ defmodule Pleroma.Web.ActivityPub.Utils do def get_reports(params, page, page_size) do params = params - |> Map.put("type", "Flag") - |> Map.put("skip_preload", true) - |> Map.put("preload_report_notes", true) - |> Map.put("total", true) - |> Map.put("limit", page_size) - |> Map.put("offset", (page - 1) * page_size) + |> Map.put(:type, "Flag") + |> Map.put(:skip_preload, true) + |> Map.put(:preload_report_notes, true) + |> Map.put(:total, true) + |> Map.put(:limit, page_size) + |> Map.put(:offset, (page - 1) * page_size) ActivityPub.fetch_activities([], params, :offset) end @@ -870,7 +871,4 @@ defmodule Pleroma.Web.ActivityPub.Utils do |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) |> Repo.all() end - - def maybe_put(map, _key, nil), do: map - def maybe_put(map, key, value), do: Map.put(map, key, value) end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 34590b16d..4a02b09a1 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -213,34 +213,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do |> Map.merge(Utils.make_json_ld_header()) end - def render("activity_collection_page.json", %{activities: activities, iri: iri}) do - # this is sorted chronologically, so first activity is the newest (max) - {max_id, min_id, collection} = - if length(activities) > 0 do - { - Enum.at(activities, 0).id, - Enum.at(Enum.reverse(activities), 0).id, - Enum.map(activities, fn act -> - {:ok, data} = Transmogrifier.prepare_outgoing(act.data) - data - end) - } - else - { - 0, - 0, - [] - } - end + def render("activity_collection_page.json", %{ + activities: activities, + iri: iri, + pagination: pagination + }) do + collection = + Enum.map(activities, fn activity -> + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + data + end) %{ - "id" => "#{iri}?max_id=#{max_id}&page=true", "type" => "OrderedCollectionPage", "partOf" => iri, - "orderedItems" => collection, - "next" => "#{iri}?max_id=#{min_id}&page=true" + "orderedItems" => collection } |> Map.merge(Utils.make_json_ld_header()) + |> Map.merge(pagination) end defp maybe_put_total_items(map, false, _total), do: map diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 783203c07..5cbf0dd4f 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -7,38 +7,24 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] - alias Pleroma.Activity alias Pleroma.Config - alias Pleroma.ConfigDB alias Pleroma.MFA alias Pleroma.ModerationLog alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.ReportNote alias Pleroma.Stats alias Pleroma.User - alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline - alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ModerationLogView - alias Pleroma.Web.AdminAPI.Report - alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.Search - alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint - alias Pleroma.Web.MastodonAPI - alias Pleroma.Web.MastodonAPI.AppView - alias Pleroma.Web.OAuth.App alias Pleroma.Web.Router require Logger - @descriptions Pleroma.Docs.JSON.compile() @users_page_size 50 plug( @@ -69,30 +55,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do ] ) - plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites) - - plug( - OAuthScopesPlug, - %{scopes: ["write:invites"], admin: true} - when action in [:create_invite_token, :revoke_invite, :email_invite] - ) - plug( OAuthScopesPlug, %{scopes: ["write:follows"], admin: true} - when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["read:reports"], admin: true} - when action in [:list_reports, :report_show] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:reports"], admin: true} - when action in [:reports_update, :report_notes_create, :report_notes_delete] + when action in [:user_follow, :user_unfollow] ) plug( @@ -105,11 +71,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do OAuthScopesPlug, %{scopes: ["read"], admin: true} when action in [ - :config_show, :list_log, :stats, - :relay_list, - :config_descriptions, :need_reboot ] ) @@ -119,13 +82,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do %{scopes: ["write"], admin: true} when action in [ :restart, - :config_update, :resend_confirmation_email, :confirm_email, - :oauth_app_create, - :oauth_app_list, - :oauth_app_update, - :oauth_app_delete, :reload_emoji ] ) @@ -268,10 +226,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do activities = ActivityPub.fetch_statuses(nil, %{ - "instance" => instance, - "limit" => page_size, - "offset" => (page - 1) * page_size, - "exclude_reblogs" => !with_reblogs && "true" + instance: instance, + limit: page_size, + offset: (page - 1) * page_size, + exclude_reblogs: not with_reblogs }) conn @@ -288,13 +246,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do activities = ActivityPub.fetch_user_activities(user, nil, %{ - "limit" => page_size, - "godmode" => godmode, - "exclude_reblogs" => !with_reblogs && "true" + limit: page_size, + godmode: godmode, + exclude_reblogs: not with_reblogs }) conn - |> put_view(MastodonAPI.StatusView) + |> put_view(AdminAPI.StatusView) |> render("index.json", %{activities: activities, as: :activity}) else _ -> {:error, :not_found} @@ -531,113 +489,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do render_error(conn, :forbidden, "You can't revoke your own admin status.") end - def relay_list(conn, _params) do - with {:ok, list} <- Relay.list() do - json(conn, %{relays: list}) - else - _ -> - conn - |> put_status(500) - end - end - - def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do - with {:ok, _message} <- Relay.follow(target) do - ModerationLog.insert_log(%{ - action: "relay_follow", - actor: admin, - target: target - }) - - json(conn, target) - else - _ -> - conn - |> put_status(500) - |> json(target) - end - end - - def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do - with {:ok, _message} <- Relay.unfollow(target) do - ModerationLog.insert_log(%{ - action: "relay_unfollow", - actor: admin, - target: target - }) - - json(conn, target) - else - _ -> - conn - |> put_status(500) - |> json(target) - end - end - - @doc "Sends registration invite via email" - def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do - with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, - {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])}, - {:ok, invite_token} <- UserInviteToken.create_invite(), - email <- - Pleroma.Emails.UserEmail.user_invitation_email( - user, - invite_token, - email, - params["name"] - ), - {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do - json_response(conn, :no_content, "") - else - {:registrations_open, _} -> - {:error, "To send invites you need to set the `registrations_open` option to false."} - - {:invites_enabled, _} -> - {:error, "To send invites you need to set the `invites_enabled` option to true."} - end - end - - @doc "Create an account registration invite token" - def create_invite_token(conn, params) do - opts = %{} - - opts = - if params["max_use"], - do: Map.put(opts, :max_use, params["max_use"]), - else: opts - - opts = - if params["expires_at"], - do: Map.put(opts, :expires_at, params["expires_at"]), - else: opts - - {:ok, invite} = UserInviteToken.create_invite(opts) - - json(conn, AccountView.render("invite.json", %{invite: invite})) - end - - @doc "Get list of created invites" - def invites(conn, _params) do - invites = UserInviteToken.list_invites() - - conn - |> put_view(AccountView) - |> render("invites.json", %{invites: invites}) - end - - @doc "Revokes invite by token" - def revoke_invite(conn, %{"token" => token}) do - with {:ok, invite} <- UserInviteToken.find_by_token(token), - {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do - conn - |> put_view(AccountView) - |> render("invite.json", %{invite: updated_invite}) - else - nil -> {:error, :not_found} - end - end - @doc "Get a password reset token (base64 string) for given nickname" def get_password_reset(conn, %{"nickname" => nickname}) do (%User{local: true} = user) = User.get_cached_by_nickname(nickname) @@ -724,85 +575,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def list_reports(conn, params) do - {page, page_size} = page_params(params) - - reports = Utils.get_reports(params, page, page_size) - - conn - |> put_view(ReportView) - |> render("index.json", %{reports: reports}) - end - - def report_show(conn, %{"id" => id}) do - with %Activity{} = report <- Activity.get_by_id(id) do - conn - |> put_view(ReportView) - |> render("show.json", Report.extract_report_info(report)) - else - _ -> {:error, :not_found} - end - end - - def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do - result = - reports - |> Enum.map(fn report -> - with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do - ModerationLog.insert_log(%{ - action: "report_update", - actor: admin, - subject: activity - }) - - activity - else - {:error, message} -> %{id: report["id"], error: message} - end - end) - - case Enum.any?(result, &Map.has_key?(&1, :error)) do - true -> json_response(conn, :bad_request, result) - false -> json_response(conn, :no_content, "") - end - end - - def report_notes_create(%{assigns: %{user: user}} = conn, %{ - "id" => report_id, - "content" => content - }) do - with {:ok, _} <- ReportNote.create(user.id, report_id, content) do - ModerationLog.insert_log(%{ - action: "report_note", - actor: user, - subject: Activity.get_by_id(report_id), - text: content - }) - - json_response(conn, :no_content, "") - else - _ -> json_response(conn, :bad_request, "") - end - end - - def report_notes_delete(%{assigns: %{user: user}} = conn, %{ - "id" => note_id, - "report_id" => report_id - }) do - with {:ok, note} <- ReportNote.destroy(note_id) do - ModerationLog.insert_log(%{ - action: "report_note_delete", - actor: user, - subject: Activity.get_by_id(report_id), - text: note.content - }) - - json_response(conn, :no_content, "") - else - _ -> json_response(conn, :bad_request, "") - end - end - def list_log(conn, params) do {page, page_size} = page_params(params) @@ -821,105 +593,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> render("index.json", %{log: log}) end - def config_descriptions(conn, _params) do - descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) - - json(conn, descriptions) - end - - def config_show(conn, %{"only_db" => true}) do - with :ok <- configurable_from_database() do - configs = Pleroma.Repo.all(ConfigDB) - - conn - |> put_view(ConfigView) - |> render("index.json", %{configs: configs}) - end - end - - def config_show(conn, _params) do - with :ok <- configurable_from_database() do - configs = ConfigDB.get_all_as_keyword() - - merged = - Config.Holder.default_config() - |> ConfigDB.merge(configs) - |> Enum.map(fn {group, values} -> - Enum.map(values, fn {key, value} -> - db = - if configs[group][key] do - ConfigDB.get_db_keys(configs[group][key], key) - end - - db_value = configs[group][key] - - merged_value = - if !is_nil(db_value) and Keyword.keyword?(db_value) and - ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do - ConfigDB.merge_group(group, key, value, db_value) - else - value - end - - setting = %{ - group: ConfigDB.convert(group), - key: ConfigDB.convert(key), - value: ConfigDB.convert(merged_value) - } - - if db, do: Map.put(setting, :db, db), else: setting - end) - end) - |> List.flatten() - - json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) - end - end - - def config_update(conn, %{"configs" => configs}) do - with :ok <- configurable_from_database() do - {_errors, results} = - configs - |> Enum.filter(&whitelisted_config?/1) - |> Enum.map(fn - %{"group" => group, "key" => key, "delete" => true} = params -> - ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]}) - - %{"group" => group, "key" => key, "value" => value} -> - ConfigDB.update_or_create(%{group: group, key: key, value: value}) - end) - |> Enum.split_with(fn result -> elem(result, 0) == :error end) - - {deleted, updated} = - results - |> Enum.map(fn {:ok, config} -> - Map.put(config, :db, ConfigDB.get_db_keys(config)) - end) - |> Enum.split_with(fn config -> - Ecto.get_meta(config, :state) == :deleted - end) - - Config.TransferTask.load_and_update_env(deleted, false) - - if !Restarter.Pleroma.need_reboot?() do - changed_reboot_settings? = - (updated ++ deleted) - |> Enum.any?(fn config -> - group = ConfigDB.from_string(config.group) - key = ConfigDB.from_string(config.key) - value = ConfigDB.from_binary(config.value) - Config.TransferTask.pleroma_need_restart?(group, key, value) - end) - - if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() - end - - conn - |> put_view(ConfigView) - |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()}) - end - end - def restart(conn, _params) do with :ok <- configurable_from_database() do Restarter.Pleroma.restart(Config.get(:env), 50) @@ -940,28 +613,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - defp whitelisted_config?(group, key) do - if whitelisted_configs = Config.get(:database_config_whitelist) do - Enum.any?(whitelisted_configs, fn - {whitelisted_group} -> - group == inspect(whitelisted_group) - - {whitelisted_group, whitelisted_key} -> - group == inspect(whitelisted_group) && key == inspect(whitelisted_key) - end) - else - true - end - end - - defp whitelisted_config?(%{"group" => group, "key" => key}) do - whitelisted_config?(group, key) - end - - defp whitelisted_config?(%{:group => group} = config) do - whitelisted_config?(group, config[:key]) - end - def reload_emoji(conn, _params) do Pleroma.Emoji.reload() @@ -996,83 +647,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do conn |> json("") end - def oauth_app_create(conn, params) do - params = - if params["name"] do - Map.put(params, "client_name", params["name"]) - else - params - end - - result = - case App.create(params) do - {:ok, app} -> - AppView.render("show.json", %{app: app, admin: true}) - - {:error, changeset} -> - App.errors(changeset) - end - - json(conn, result) - end - - def oauth_app_update(conn, params) do - params = - if params["name"] do - Map.put(params, "client_name", params["name"]) - else - params - end - - with {:ok, app} <- App.update(params) do - json(conn, AppView.render("show.json", %{app: app, admin: true})) - else - {:error, changeset} -> - json(conn, App.errors(changeset)) - - nil -> - json_response(conn, :bad_request, "") - end - end - - def oauth_app_list(conn, params) do - {page, page_size} = page_params(params) - - search_params = %{ - client_name: params["name"], - client_id: params["client_id"], - page: page, - page_size: page_size - } - - search_params = - if Map.has_key?(params, "trusted") do - Map.put(search_params, :trusted, params["trusted"]) - else - search_params - end - - with {:ok, apps, count} <- App.search(search_params) do - json( - conn, - AppView.render("index.json", - apps: apps, - count: count, - page_size: page_size, - admin: true - ) - ) - end - end - - def oauth_app_delete(conn, params) do - with {:ok, _app} <- App.destroy(params["id"]) do - json_response(conn, :no_content, "") - else - _ -> json_response(conn, :bad_request, "") - end - end - def stats(conn, _) do count = Stats.get_status_visibility_count() diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex new file mode 100644 index 000000000..d6e2019bc --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -0,0 +1,152 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ConfigController do + use Pleroma.Web, :controller + + alias Pleroma.Config + alias Pleroma.ConfigDB + alias Pleroma.Plugs.OAuthScopesPlug + + @descriptions Pleroma.Docs.JSON.compile() + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) + + plug( + OAuthScopesPlug, + %{scopes: ["read"], admin: true} + when action in [:show, :descriptions] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation + + def descriptions(conn, _params) do + descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) + + json(conn, descriptions) + end + + def show(conn, %{only_db: true}) do + with :ok <- configurable_from_database() do + configs = Pleroma.Repo.all(ConfigDB) + render(conn, "index.json", %{configs: configs}) + end + end + + def show(conn, _params) do + with :ok <- configurable_from_database() do + configs = ConfigDB.get_all_as_keyword() + + merged = + Config.Holder.default_config() + |> ConfigDB.merge(configs) + |> Enum.map(fn {group, values} -> + Enum.map(values, fn {key, value} -> + db = + if configs[group][key] do + ConfigDB.get_db_keys(configs[group][key], key) + end + + db_value = configs[group][key] + + merged_value = + if not is_nil(db_value) and Keyword.keyword?(db_value) and + ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do + ConfigDB.merge_group(group, key, value, db_value) + else + value + end + + %{ + group: ConfigDB.convert(group), + key: ConfigDB.convert(key), + value: ConfigDB.convert(merged_value) + } + |> Pleroma.Maps.put_if_present(:db, db) + end) + end) + |> List.flatten() + + json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) + end + end + + def update(%{body_params: %{configs: configs}} = conn, _) do + with :ok <- configurable_from_database() do + results = + configs + |> Enum.filter(&whitelisted_config?/1) + |> Enum.map(fn + %{group: group, key: key, delete: true} = params -> + ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]}) + + %{group: group, key: key, value: value} -> + ConfigDB.update_or_create(%{group: group, key: key, value: value}) + end) + |> Enum.reject(fn {result, _} -> result == :error end) + + {deleted, updated} = + results + |> Enum.map(fn {:ok, config} -> + Map.put(config, :db, ConfigDB.get_db_keys(config)) + end) + |> Enum.split_with(fn config -> + Ecto.get_meta(config, :state) == :deleted + end) + + Config.TransferTask.load_and_update_env(deleted, false) + + if not Restarter.Pleroma.need_reboot?() do + changed_reboot_settings? = + (updated ++ deleted) + |> Enum.any?(fn config -> + group = ConfigDB.from_string(config.group) + key = ConfigDB.from_string(config.key) + value = ConfigDB.from_binary(config.value) + Config.TransferTask.pleroma_need_restart?(group, key, value) + end) + + if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() + end + + render(conn, "index.json", %{ + configs: updated, + need_reboot: Restarter.Pleroma.need_reboot?() + }) + end + end + + defp configurable_from_database do + if Config.get(:configurable_from_database) do + :ok + else + {:error, "To use this endpoint you need to enable configuration from database."} + end + end + + defp whitelisted_config?(group, key) do + if whitelisted_configs = Config.get(:database_config_whitelist) do + Enum.any?(whitelisted_configs, fn + {whitelisted_group} -> + group == inspect(whitelisted_group) + + {whitelisted_group, whitelisted_key} -> + group == inspect(whitelisted_group) && key == inspect(whitelisted_key) + end) + else + true + end + end + + defp whitelisted_config?(%{group: group, key: key}) do + whitelisted_config?(group, key) + end + + defp whitelisted_config?(%{group: group} = config) do + whitelisted_config?(group, config[:key]) + end +end diff --git a/lib/pleroma/web/admin_api/controllers/invite_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_controller.ex new file mode 100644 index 000000000..7d169b8d2 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/invite_controller.ex @@ -0,0 +1,78 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InviteController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.Config + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.UserInviteToken + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :index) + + plug( + OAuthScopesPlug, + %{scopes: ["write:invites"], admin: true} when action in [:create, :revoke, :email] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InviteOperation + + @doc "Get list of created invites" + def index(conn, _params) do + invites = UserInviteToken.list_invites() + + render(conn, "index.json", invites: invites) + end + + @doc "Create an account registration invite token" + def create(%{body_params: params} = conn, _) do + {:ok, invite} = UserInviteToken.create_invite(params) + + render(conn, "show.json", invite: invite) + end + + @doc "Revokes invite by token" + def revoke(%{body_params: %{token: token}} = conn, _) do + with {:ok, invite} <- UserInviteToken.find_by_token(token), + {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do + render(conn, "show.json", invite: updated_invite) + else + nil -> {:error, :not_found} + error -> error + end + end + + @doc "Sends registration invite via email" + def email(%{assigns: %{user: user}, body_params: %{email: email} = params} = conn, _) do + with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, + {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])}, + {:ok, invite_token} <- UserInviteToken.create_invite(), + {:ok, _} <- + user + |> Pleroma.Emails.UserEmail.user_invitation_email( + invite_token, + email, + params[:name] + ) + |> Pleroma.Emails.Mailer.deliver() do + json_response(conn, :no_content, "") + else + {:registrations_open, _} -> + {:error, "To send invites you need to set the `registrations_open` option to false."} + + {:invites_enabled, _} -> + {:error, "To send invites you need to set the `invites_enabled` option to true."} + + {:error, error} -> + {:error, error} + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex new file mode 100644 index 000000000..dca23ea73 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.OAuth.App + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(:put_view, Pleroma.Web.MastodonAPI.AppView) + + plug( + OAuthScopesPlug, + %{scopes: ["write"], admin: true} + when action in [:create, :index, :update, :delete] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation + + def index(conn, params) do + search_params = + params + |> Map.take([:client_id, :page, :page_size, :trusted]) + |> Map.put(:client_name, params[:name]) + + with {:ok, apps, count} <- App.search(search_params) do + render(conn, "index.json", + apps: apps, + count: count, + page_size: params.page_size, + admin: true + ) + end + end + + def create(%{body_params: params} = conn, _) do + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) + + case App.create(params) do + {:ok, app} -> + render(conn, "show.json", app: app, admin: true) + + {:error, changeset} -> + json(conn, App.errors(changeset)) + end + end + + def update(%{body_params: params} = conn, %{id: id}) do + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) + + with {:ok, app} <- App.update(id, params) do + render(conn, "show.json", app: app, admin: true) + else + {:error, changeset} -> + json(conn, App.errors(changeset)) + + nil -> + json_response(conn, :bad_request, "") + end + end + + def delete(conn, params) do + with {:ok, _app} <- App.destroy(params.id) do + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex new file mode 100644 index 000000000..cf9f3a14b --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.RelayController do + use Pleroma.Web, :controller + + alias Pleroma.ModerationLog + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.ActivityPub.Relay + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + OAuthScopesPlug, + %{scopes: ["write:follows"], admin: true} + when action in [:follow, :unfollow] + ) + + plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :index) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RelayOperation + + def index(conn, _params) do + with {:ok, list} <- Relay.list() do + json(conn, %{relays: list}) + end + end + + def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do + with {:ok, _message} <- Relay.follow(target) do + ModerationLog.insert_log(%{ + action: "relay_follow", + actor: admin, + target: target + }) + + json(conn, target) + else + _ -> + conn + |> put_status(500) + |> json(target) + end + end + + def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do + with {:ok, _message} <- Relay.unfollow(target) do + ModerationLog.insert_log(%{ + action: "relay_unfollow", + actor: admin, + target: target + }) + + json(conn, target) + else + _ -> + conn + |> put_status(500) + |> json(target) + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex new file mode 100644 index 000000000..4c011e174 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex @@ -0,0 +1,107 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ReportController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.Activity + alias Pleroma.ModerationLog + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.ReportNote + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.AdminAPI + alias Pleroma.Web.AdminAPI.Report + alias Pleroma.Web.CommonAPI + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(OAuthScopesPlug, %{scopes: ["read:reports"], admin: true} when action in [:index, :show]) + + plug( + OAuthScopesPlug, + %{scopes: ["write:reports"], admin: true} + when action in [:update, :notes_create, :notes_delete] + ) + + action_fallback(AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ReportOperation + + def index(conn, params) do + reports = Utils.get_reports(params, params.page, params.page_size) + + render(conn, "index.json", reports: reports) + end + + def show(conn, %{id: id}) do + with %Activity{} = report <- Activity.get_by_id(id) do + render(conn, "show.json", Report.extract_report_info(report)) + else + _ -> {:error, :not_found} + end + end + + def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn, _) do + result = + Enum.map(reports, fn report -> + case CommonAPI.update_report_state(report.id, report.state) do + {:ok, activity} -> + ModerationLog.insert_log(%{ + action: "report_update", + actor: admin, + subject: activity + }) + + activity + + {:error, message} -> + %{id: report.id, error: message} + end + end) + + if Enum.any?(result, &Map.has_key?(&1, :error)) do + json_response(conn, :bad_request, result) + else + json_response(conn, :no_content, "") + end + end + + def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = conn, %{ + id: report_id + }) do + with {:ok, _} <- ReportNote.create(user.id, report_id, content) do + ModerationLog.insert_log(%{ + action: "report_note", + actor: user, + subject: Activity.get_by_id(report_id), + text: content + }) + + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end + + def notes_delete(%{assigns: %{user: user}} = conn, %{ + id: note_id, + report_id: report_id + }) do + with {:ok, note} <- ReportNote.destroy(note_id) do + ModerationLog.insert_log(%{ + action: "report_note_delete", + actor: user, + subject: Activity.get_by_id(report_id), + text: note.content + }) + + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex index 08cb9c10b..bc48cc527 100644 --- a/lib/pleroma/web/admin_api/controllers/status_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex @@ -29,11 +29,11 @@ defmodule Pleroma.Web.AdminAPI.StatusController do def index(%{assigns: %{user: _admin}} = conn, params) do activities = ActivityPub.fetch_statuses(nil, %{ - "godmode" => params.godmode, - "local_only" => params.local_only, - "limit" => params.page_size, - "offset" => (params.page - 1) * params.page_size, - "exclude_reblogs" => not params.with_reblogs + godmode: params.godmode, + local_only: params.local_only, + limit: params.page_size, + offset: (params.page - 1) * params.page_size, + exclude_reblogs: not params.with_reblogs }) render(conn, "index.json", activities: activities, as: :activity) @@ -41,9 +41,7 @@ defmodule Pleroma.Web.AdminAPI.StatusController do def show(conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id(id) do - conn - |> put_view(MastodonAPI.StatusView) - |> render("show.json", %{activity: activity}) + render(conn, "show.json", %{activity: activity}) else nil -> {:error, :not_found} end diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex index c28efadd5..0bfb8f022 100644 --- a/lib/pleroma/web/admin_api/search.ex +++ b/lib/pleroma/web/admin_api/search.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web.AdminAPI.Search do query = params |> Map.drop([:page, :page_size]) - |> Map.put(:exclude_service_users, true) + |> Map.put(:invisible, false) |> User.Query.build() |> order_by([u], u.nickname) @@ -31,7 +31,6 @@ defmodule Pleroma.Web.AdminAPI.Search do count = Repo.aggregate(query, :count, :id) results = Repo.all(paginated_query) - {:ok, results, count} end end diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 46dadb5ee..120159527 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -80,24 +80,6 @@ defmodule Pleroma.Web.AdminAPI.AccountView do } end - def render("invite.json", %{invite: invite}) do - %{ - "id" => invite.id, - "token" => invite.token, - "used" => invite.used, - "expires_at" => invite.expires_at, - "uses" => invite.uses, - "max_use" => invite.max_use, - "invite_type" => invite.invite_type - } - end - - def render("invites.json", %{invites: invites}) do - %{ - invites: render_many(invites, AccountView, "invite.json", as: :invite) - } - end - def render("created.json", %{user: user}) do %{ type: "success", diff --git a/lib/pleroma/web/admin_api/views/invite_view.ex b/lib/pleroma/web/admin_api/views/invite_view.ex new file mode 100644 index 000000000..f93cb6916 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/invite_view.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InviteView do + use Pleroma.Web, :view + + def render("index.json", %{invites: invites}) do + %{ + invites: render_many(invites, __MODULE__, "show.json", as: :invite) + } + end + + def render("show.json", %{invite: invite}) do + %{ + "id" => invite.id, + "token" => invite.token, + "used" => invite.used, + "expires_at" => invite.expires_at, + "uses" => invite.uses, + "max_use" => invite.max_use, + "invite_type" => invite.invite_type + } + end +end diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index f432b8c2c..773f798fe 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do %{ reports: reports[:items] - |> Enum.map(&Report.extract_report_info(&1)) + |> Enum.map(&Report.extract_report_info/1) |> Enum.map(&render(__MODULE__, "show.json", &1)) |> Enum.reverse(), total: reports[:total] diff --git a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex new file mode 100644 index 000000000..7b38a2ef4 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex @@ -0,0 +1,142 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def show_operation do + %Operation{ + tags: ["Admin", "Config"], + summary: "Get list of merged default settings with saved in database", + operationId: "AdminAPI.ConfigController.show", + parameters: [ + Operation.parameter( + :only_db, + :query, + %Schema{type: :boolean, default: false}, + "Get only saved in database settings" + ) + ], + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => Operation.response("Config", "application/json", config_response()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "Config"], + summary: "Update config settings", + operationId: "AdminAPI.ConfigController.update", + security: [%{"oAuth" => ["write"]}], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + configs: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + group: %Schema{type: :string}, + key: %Schema{type: :string}, + value: any(), + delete: %Schema{type: :boolean}, + subkeys: %Schema{type: :array, items: %Schema{type: :string}} + } + } + } + } + }), + responses: %{ + 200 => Operation.response("Config", "application/json", config_response()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def descriptions_operation do + %Operation{ + tags: ["Admin", "Config"], + summary: "Get JSON with config descriptions.", + operationId: "AdminAPI.ConfigController.descriptions", + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => + Operation.response("Config Descriptions", "application/json", %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + group: %Schema{type: :string}, + key: %Schema{type: :string}, + type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]}, + description: %Schema{type: :string}, + children: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + key: %Schema{type: :string}, + type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]}, + description: %Schema{type: :string}, + suggestions: %Schema{type: :array} + } + } + } + } + } + }), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + defp any do + %Schema{ + oneOf: [ + %Schema{type: :array}, + %Schema{type: :object}, + %Schema{type: :string}, + %Schema{type: :integer}, + %Schema{type: :boolean} + ] + } + end + + defp config_response do + %Schema{ + type: :object, + properties: %{ + configs: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + group: %Schema{type: :string}, + key: %Schema{type: :string}, + value: any() + } + } + }, + need_reboot: %Schema{ + type: :boolean, + description: + "If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect" + } + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex new file mode 100644 index 000000000..d3af9db49 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex @@ -0,0 +1,148 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Admin", "Invites"], + summary: "Get a list of generated invites", + operationId: "AdminAPI.InviteController.index", + security: [%{"oAuth" => ["read:invites"]}], + responses: %{ + 200 => + Operation.response("Invites", "application/json", %Schema{ + type: :object, + properties: %{ + invites: %Schema{type: :array, items: invite()} + }, + example: %{ + "invites" => [ + %{ + "id" => 123, + "token" => "kSQtDj_GNy2NZsL9AQDFIsHN5qdbguB6qRg3WHw6K1U=", + "used" => true, + "expires_at" => nil, + "uses" => 0, + "max_use" => nil, + "invite_type" => "one_time" + } + ] + } + }) + } + } + end + + def create_operation do + %Operation{ + tags: ["Admin", "Invites"], + summary: "Create an account registration invite token", + operationId: "AdminAPI.InviteController.create", + security: [%{"oAuth" => ["write:invites"]}], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + max_use: %Schema{type: :integer}, + expires_at: %Schema{type: :string, format: :date, example: "2020-04-20"} + } + }), + responses: %{ + 200 => Operation.response("Invite", "application/json", invite()) + } + } + end + + def revoke_operation do + %Operation{ + tags: ["Admin", "Invites"], + summary: "Revoke invite by token", + operationId: "AdminAPI.InviteController.revoke", + security: [%{"oAuth" => ["write:invites"]}], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + required: [:token], + properties: %{ + token: %Schema{type: :string} + } + }, + required: true + ), + responses: %{ + 200 => Operation.response("Invite", "application/json", invite()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def email_operation do + %Operation{ + tags: ["Admin", "Invites"], + summary: "Sends registration invite via email", + operationId: "AdminAPI.InviteController.email", + security: [%{"oAuth" => ["write:invites"]}], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + required: [:email], + properties: %{ + email: %Schema{type: :string, format: :email}, + name: %Schema{type: :string} + } + }, + required: true + ), + responses: %{ + 204 => no_content_response(), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + defp invite do + %Schema{ + title: "Invite", + type: :object, + properties: %{ + id: %Schema{type: :integer}, + token: %Schema{type: :string}, + used: %Schema{type: :boolean}, + expires_at: %Schema{type: :string, format: :date, nullable: true}, + uses: %Schema{type: :integer}, + max_use: %Schema{type: :integer, nullable: true}, + invite_type: %Schema{ + type: :string, + enum: ["one_time", "reusable", "date_limited", "reusable_date_limited"] + } + }, + example: %{ + "id" => 123, + "token" => "kSQtDj_GNy2NZsL9AQDFIsHN5qdbguB6qRg3WHw6K1U=", + "used" => true, + "expires_at" => nil, + "uses" => 0, + "max_use" => nil, + "invite_type" => "one_time" + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex new file mode 100644 index 000000000..fbc9f80d7 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex @@ -0,0 +1,215 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + summary: "List OAuth apps", + tags: ["Admin", "oAuth Apps"], + operationId: "AdminAPI.OAuthAppController.index", + security: [%{"oAuth" => ["write"]}], + parameters: [ + Operation.parameter(:name, :query, %Schema{type: :string}, "App name"), + Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"), + Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"), + Operation.parameter( + :trusted, + :query, + %Schema{type: :boolean, default: false}, + "Trusted apps" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number of apps to return" + ) + ], + responses: %{ + 200 => + Operation.response("List of apps", "application/json", %Schema{ + type: :object, + properties: %{ + apps: %Schema{type: :array, items: oauth_app()}, + count: %Schema{type: :integer}, + page_size: %Schema{type: :integer} + }, + example: %{ + "apps" => [ + %{ + "id" => 1, + "name" => "App name", + "client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk", + "client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY", + "redirect_uri" => "https://example.com/oauth-callback", + "website" => "https://example.com", + "trusted" => true + } + ], + "count" => 1, + "page_size" => 50 + } + }) + } + } + end + + def create_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Create OAuth App", + operationId: "AdminAPI.OAuthAppController.create", + requestBody: request_body("Parameters", create_request()), + security: [%{"oAuth" => ["write"]}], + responses: %{ + 200 => Operation.response("App", "application/json", oauth_app()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Update OAuth App", + operationId: "AdminAPI.OAuthAppController.update", + parameters: [id_param()], + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_request()), + responses: %{ + 200 => Operation.response("App", "application/json", oauth_app()), + 400 => + Operation.response("Bad Request", "application/json", %Schema{ + oneOf: [ApiError, %Schema{type: :string}] + }) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Delete OAuth App", + operationId: "AdminAPI.OAuthAppController.delete", + parameters: [id_param()], + security: [%{"oAuth" => ["write"]}], + responses: %{ + 204 => no_content_response(), + 400 => no_content_response() + } + } + end + + defp create_request do + %Schema{ + title: "oAuthAppCreateRequest", + type: :object, + required: [:name, :redirect_uris], + properties: %{ + name: %Schema{type: :string, description: "Application Name"}, + scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, + redirect_uris: %Schema{ + type: :string, + description: + "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." + }, + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of the app" + }, + trusted: %Schema{ + type: :boolean, + nullable: true, + default: false, + description: "Is the app trusted?" + } + }, + example: %{ + "name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/", + "scopes" => ["read", "write"], + "trusted" => true + } + } + end + + defp update_request do + %Schema{ + title: "oAuthAppUpdateRequest", + type: :object, + properties: %{ + name: %Schema{type: :string, description: "Application Name"}, + scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, + redirect_uris: %Schema{ + type: :string, + description: + "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." + }, + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of the app" + }, + trusted: %Schema{ + type: :boolean, + nullable: true, + default: false, + description: "Is the app trusted?" + } + }, + example: %{ + "name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/", + "scopes" => ["read", "write"], + "trusted" => true + } + } + end + + defp oauth_app do + %Schema{ + title: "oAuthApp", + type: :object, + properties: %{ + id: %Schema{type: :integer}, + name: %Schema{type: :string}, + client_id: %Schema{type: :string}, + client_secret: %Schema{type: :string}, + redirect_uri: %Schema{type: :string}, + website: %Schema{type: :string, nullable: true}, + trusted: %Schema{type: :boolean} + }, + example: %{ + "id" => 123, + "name" => "My App", + "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", + "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", + "redirect_uri" => "https://myapp.com/oauth-callback", + "website" => "https://myapp.com/", + "trusted" => false + } + } + end + + def id_param do + Operation.parameter(:id, :path, :integer, "App ID", + example: 1337, + required: true + ) + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex new file mode 100644 index 000000000..7672cb467 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Admin", "Relays"], + summary: "List Relays", + operationId: "AdminAPI.RelayController.index", + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :object, + properties: %{ + relays: %Schema{ + type: :array, + items: %Schema{type: :string}, + example: ["lain.com", "mstdn.io"] + } + } + }) + } + } + end + + def follow_operation do + %Operation{ + tags: ["Admin", "Relays"], + summary: "Follow a Relay", + operationId: "AdminAPI.RelayController.follow", + security: [%{"oAuth" => ["write:follows"]}], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + relay_url: %Schema{type: :string, format: :uri} + } + }), + responses: %{ + 200 => + Operation.response("Status", "application/json", %Schema{ + type: :string, + example: "http://mastodon.example.org/users/admin" + }) + } + } + end + + def unfollow_operation do + %Operation{ + tags: ["Admin", "Relays"], + summary: "Unfollow a Relay", + operationId: "AdminAPI.RelayController.unfollow", + security: [%{"oAuth" => ["write:follows"]}], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + relay_url: %Schema{type: :string, format: :uri} + } + }), + responses: %{ + 200 => + Operation.response("Status", "application/json", %Schema{ + type: :string, + example: "http://mastodon.example.org/users/admin" + }) + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex new file mode 100644 index 000000000..15e78bfaf --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex @@ -0,0 +1,237 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.Status + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Admin", "Reports"], + summary: "Get a list of reports", + operationId: "AdminAPI.ReportController.index", + security: [%{"oAuth" => ["read:reports"]}], + parameters: [ + Operation.parameter( + :state, + :query, + report_state(), + "Filter by report state" + ), + Operation.parameter( + :limit, + :query, + %Schema{type: :integer}, + "The number of records to retrieve" + ), + Operation.parameter( + :page, + :query, + %Schema{type: :integer, default: 1}, + "Page number" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number number of log entries per page" + ) + ], + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :object, + properties: %{ + total: %Schema{type: :integer}, + reports: %Schema{ + type: :array, + items: report() + } + } + }), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def show_operation do + %Operation{ + tags: ["Admin", "Reports"], + summary: "Get an individual report", + operationId: "AdminAPI.ReportController.show", + parameters: [id_param()], + security: [%{"oAuth" => ["read:reports"]}], + responses: %{ + 200 => Operation.response("Report", "application/json", report()), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "Reports"], + summary: "Change the state of one or multiple reports", + operationId: "AdminAPI.ReportController.update", + security: [%{"oAuth" => ["write:reports"]}], + requestBody: request_body("Parameters", update_request(), required: true), + responses: %{ + 204 => no_content_response(), + 400 => Operation.response("Bad Request", "application/json", update_400_response()), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def notes_create_operation do + %Operation{ + tags: ["Admin", "Reports"], + summary: "Create report note", + operationId: "AdminAPI.ReportController.notes_create", + parameters: [id_param()], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + content: %Schema{type: :string, description: "The message"} + } + }), + security: [%{"oAuth" => ["write:reports"]}], + responses: %{ + 204 => no_content_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def notes_delete_operation do + %Operation{ + tags: ["Admin", "Reports"], + summary: "Delete report note", + operationId: "AdminAPI.ReportController.notes_delete", + parameters: [ + Operation.parameter(:report_id, :path, :string, "Report ID"), + Operation.parameter(:id, :path, :string, "Note ID") + ], + security: [%{"oAuth" => ["write:reports"]}], + responses: %{ + 204 => no_content_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp report_state do + %Schema{type: :string, enum: ["open", "closed", "resolved"]} + end + + defp id_param do + Operation.parameter(:id, :path, FlakeID, "Report ID", + example: "9umDrYheeY451cQnEe", + required: true + ) + end + + defp report do + %Schema{ + type: :object, + properties: %{ + id: FlakeID, + state: report_state(), + account: account_admin(), + actor: account_admin(), + content: %Schema{type: :string}, + created_at: %Schema{type: :string, format: :"date-time"}, + statuses: %Schema{type: :array, items: Status}, + notes: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :integer}, + user_id: FlakeID, + content: %Schema{type: :string}, + inserted_at: %Schema{type: :string, format: :"date-time"} + } + } + } + } + } + end + + defp account_admin do + %Schema{ + title: "Account", + description: "Account view for admins", + type: :object, + properties: + Map.merge(Account.schema().properties, %{ + nickname: %Schema{type: :string}, + deactivated: %Schema{type: :boolean}, + local: %Schema{type: :boolean}, + roles: %Schema{ + type: :object, + properties: %{ + admin: %Schema{type: :boolean}, + moderator: %Schema{type: :boolean} + } + }, + confirmation_pending: %Schema{type: :boolean} + }) + } + end + + defp update_request do + %Schema{ + type: :object, + required: [:reports], + properties: %{ + reports: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{allOf: [FlakeID], description: "Required, report ID"}, + state: %Schema{ + type: :string, + description: + "Required, the new state. Valid values are `open`, `closed` and `resolved`" + } + } + }, + example: %{ + "reports" => [ + %{"id" => "123", "state" => "closed"}, + %{"id" => "1337", "state" => "resolved"} + ] + } + } + } + } + end + + defp update_400_response do + %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{allOf: [FlakeID], description: "Report ID"}, + error: %Schema{type: :string, description: "Error message"} + } + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex index 0b138dc79..745399b4b 100644 --- a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex @@ -74,7 +74,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do parameters: [id_param()], security: [%{"oAuth" => ["read:statuses"]}], responses: %{ - 200 => Operation.response("Status", "application/json", Status), + 200 => Operation.response("Status", "application/json", status()), 404 => Operation.response("Not Found", "application/json", ApiError) } } @@ -123,7 +123,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do } end - defp admin_account do + def admin_account do %Schema{ type: :object, properties: %{ diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index d5c335d0c..bf39ae643 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -137,7 +137,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do "background_upload_limit" => 4_000_000, "background_image" => "/static/image.png", "banner_upload_limit" => 4_000_000, - "description" => "A Pleroma instance, an alternative fediverse server", + "description" => "Pleroma: An efficient and flexible fediverse server", "email" => "lain@lain.com", "languages" => ["en"], "max_toot_chars" => 5000, diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 5a1316a5f..5d67d75b5 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -5,6 +5,8 @@ defmodule Pleroma.Web.ControllerHelper do use Pleroma.Web, :controller + alias Pleroma.Pagination + # As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"] @@ -46,43 +48,52 @@ defmodule Pleroma.Web.ControllerHelper do do: conn def add_link_headers(conn, activities, extra_params) do + case get_pagination_fields(conn, activities, extra_params) do + %{"next" => next_url, "prev" => prev_url} -> + put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") + + _ -> + conn + end + end + + def get_pagination_fields(conn, activities, extra_params \\ %{}) do case List.last(activities) do %{id: max_id} -> params = conn.params |> Map.drop(Map.keys(conn.path_params)) - |> Map.drop(["since_id", "max_id", "min_id"]) |> Map.merge(extra_params) - - limit = - params - |> Map.get("limit", "20") - |> String.to_integer() + |> Map.drop(Pagination.page_keys() -- ["limit", "order"]) min_id = - if length(activities) <= limit do - activities - |> List.first() - |> Map.get(:id) - else - activities - |> Enum.at(limit * -1) - |> Map.get(:id) - end - - next_url = current_url(conn, Map.merge(params, %{max_id: max_id})) - prev_url = current_url(conn, Map.merge(params, %{min_id: min_id})) - - put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") + activities + |> List.first() + |> Map.get(:id) + + fields = %{ + "next" => current_url(conn, Map.put(params, :max_id, max_id)), + "prev" => current_url(conn, Map.put(params, :min_id, min_id)) + } + + # Generating an `id` without already present pagination keys would + # need a query-restriction with an `q.id >= ^id` or `q.id <= ^id` + # instead of the `q.id > ^min_id` and `q.id < ^max_id`. + # This is because we only have ids present inside of the page, while + # `min_id`, `since_id` and `max_id` requires to know one outside of it. + if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do + Map.put(fields, "id", current_url(conn, conn.params)) + else + fields + end _ -> - conn + %{} end end def assign_account_by_id(conn, _) do - # TODO: use `conn.params[:id]` only after moving to OpenAPI - case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do + case Pleroma.User.get_cached_by_id(conn.params.id) do %Pleroma.User{} = account -> assign(conn, :account, account) nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() end @@ -99,11 +110,6 @@ defmodule Pleroma.Web.ControllerHelper do render_error(conn, :not_implemented, "Can't display this activity") end - @spec put_if_exist(map(), atom() | String.t(), any) :: map() - def put_if_exist(map, _key, nil), do: map - - def put_if_exist(map, key, value), do: Map.put(map, key, value) - @doc """ Returns true if request specifies to include embedded relationships in account objects. May only be used in selected account-related endpoints; has no effect for status- or diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex new file mode 100644 index 000000000..f6b8a5ee1 --- /dev/null +++ b/lib/pleroma/web/embed_controller.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.EmbedController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.User + + alias Pleroma.Web.ActivityPub.Visibility + + plug(:put_layout, :embed) + + def show(conn, %{"id" => id}) do + with %Activity{local: true} = activity <- + Activity.get_by_id_with_object(id), + true <- Visibility.is_public?(activity.object) do + {:ok, author} = User.get_or_fetch(activity.object.data["actor"]) + + conn + |> delete_resp_header("x-frame-options") + |> delete_resp_header("content-security-policy") + |> render("show.html", + activity: activity, + author: User.sanitize_html(author), + counts: get_counts(activity) + ) + end + end + + defp get_counts(%Activity{} = activity) do + %Object{data: data} = Object.normalize(activity) + + %{ + likes: Map.get(data, "like_count", 0), + replies: Map.get(data, "repliesCount", 0), + announces: Map.get(data, "announcement_count", 0) + } + end +end diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index 8133f8480..39b2a766a 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -9,14 +9,12 @@ defmodule Pleroma.Web.Feed.TagController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Feed.FeedView - import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3] - def feed(conn, %{"tag" => raw_tag} = params) do {format, tag} = parse_tag(raw_tag) activities = - %{"type" => ["Create"], "tag" => tag} - |> put_if_exist("max_id", params["max_id"]) + %{type: ["Create"], tag: tag} + |> Pleroma.Maps.put_if_present(:max_id, params["max_id"]) |> ActivityPub.fetch_public_activities() conn diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 5a6fc9de0..d56f43818 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -11,8 +11,6 @@ defmodule Pleroma.Web.Feed.UserController do alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.Feed.FeedView - import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3] - plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) action_fallback(:errors) @@ -52,10 +50,10 @@ defmodule Pleroma.Web.Feed.UserController do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do activities = %{ - "type" => ["Create"], - "actor_id" => user.ap_id + type: ["Create"], + actor_id: user.ap_id } - |> put_if_exist("max_id", params["max_id"]) + |> Pleroma.Maps.put_if_present(:max_id, params["max_id"]) |> ActivityPub.fetch_public_or_unlisted_activities() conn diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 47649d41d..7cdd8f458 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do json_response: 3 ] + alias Pleroma.Maps alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter @@ -139,9 +140,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end @doc "PATCH /api/v1/accounts/update_credentials" - def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do - user = original_user - + def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do params = params |> Enum.filter(fn {_, value} -> not is_nil(value) end) @@ -162,41 +161,49 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do :discoverable ] |> Enum.reduce(%{}, fn key, acc -> - add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)}) + Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)}) end) - |> add_if_present(params, :display_name, :name) - |> add_if_present(params, :note, :bio) - |> add_if_present(params, :avatar, :avatar) - |> add_if_present(params, :header, :banner) - |> add_if_present(params, :pleroma_background_image, :background) - |> add_if_present( - params, - :fields_attributes, + |> Maps.put_if_present(:name, params[:display_name]) + |> Maps.put_if_present(:bio, params[:note]) + |> Maps.put_if_present(:avatar, params[:avatar]) + |> Maps.put_if_present(:banner, params[:header]) + |> Maps.put_if_present(:background, params[:pleroma_background_image]) + |> Maps.put_if_present( :raw_fields, + params[:fields_attributes], &{:ok, normalize_fields_attributes(&1)} ) - |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store) - |> add_if_present(params, :default_scope, :default_scope) - |> add_if_present(params["source"], "privacy", :default_scope) - |> add_if_present(params, :actor_type, :actor_type) + |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store]) + |> Maps.put_if_present(:default_scope, params[:default_scope]) + |> Maps.put_if_present(:default_scope, params["source"]["privacy"]) + |> Maps.put_if_present(:actor_type, params[:actor_type]) changeset = User.update_changeset(user, user_params) with {:ok, user} <- User.update_and_set_cache(changeset) do + user + |> build_update_activity_params() + |> ActivityPub.update() + render(conn, "show.json", user: user, for: user, with_pleroma_settings: true) else _e -> render_error(conn, :forbidden, "Invalid request") end end - defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do - with true <- is_map(params), - true <- Map.has_key?(params, params_field), - {:ok, new_value} <- value_function.(Map.get(params, params_field)) do - Map.put(map, map_field, new_value) - else - _ -> map - end + # Hotfix, handling will be redone with the pipeline + defp build_update_activity_params(user) do + object = + Pleroma.Web.ActivityPub.UserView.render("user.json", user: user) + |> Map.delete("@context") + + %{ + local: true, + to: [user.follower_address], + cc: [], + object: object, + actor: user.ap_id + } end defp normalize_fields_attributes(fields) do @@ -237,9 +244,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do params = params |> Map.delete(:tagged) - |> Enum.filter(&(not is_nil(&1))) - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("tag", params[:tagged]) + |> Map.put(:tag, params[:tagged]) activities = ActivityPub.fetch_user_activities(user, reading_user, params) diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex index 69f0e3846..f35ec3596 100644 --- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@ -21,7 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do @doc "GET /api/v1/conversations" def index(%{assigns: %{user: user}} = conn, params) do - params = stringify_pagination_params(params) participations = Participation.for_user_with_last_activity_id(user, params) conn @@ -37,20 +36,4 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do render(conn, "participation.json", participation: participation, for: user) end end - - defp stringify_pagination_params(params) do - atom_keys = - Pleroma.Pagination.page_keys() - |> Enum.map(&String.to_atom(&1)) - - str_keys = - params - |> Map.take(atom_keys) - |> Enum.map(fn {key, value} -> {to_string(key), value} end) - |> Enum.into(%{}) - - params - |> Map.delete(atom_keys) - |> Map.merge(str_keys) - end end diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 77e2224e4..8840fc19c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -113,22 +113,44 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do query |> prepare_tags() |> Enum.map(fn tag -> - tag = String.trim_leading(tag, "#") %{name: tag, url: tags_path <> tag} end) end defp resource_search(:v1, "hashtags", query, _options) do - query - |> prepare_tags() - |> Enum.map(fn tag -> String.trim_leading(tag, "#") end) + prepare_tags(query) end - defp prepare_tags(query) do - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) + defp prepare_tags(query, add_joined_tag \\ true) do + tags = + query + |> String.split(~r/[^#\w]+/u, trim: true) + |> Enum.uniq_by(&String.downcase/1) + + explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end) + + tags = + if Enum.any?(explicit_tags) do + explicit_tags + else + tags + end + + tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end) + + if Enum.empty?(explicit_tags) && add_joined_tag do + tags + |> Kernel.++([joined_tag(tags)]) + |> Enum.uniq_by(&String.downcase/1) + else + tags + end + end + + defp joined_tag(tags) do + tags + |> Enum.map(fn tag -> String.capitalize(tag) end) + |> Enum.join() end defp with_fallback(f, fallback \\ []) do diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index f20157a5f..468b44b67 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -359,9 +359,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do with %Activity{} = activity <- Activity.get_by_id(id) do activities = ActivityPub.fetch_activities_for_context(activity.data["context"], %{ - "blocking_user" => user, - "user" => user, - "exclude_id" => activity.id + blocking_user: user, + user: user, + exclude_id: activity.id }) render(conn, "context.json", activity: activity, activities: activities, user: user) @@ -370,11 +370,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do @doc "GET /api/v1/favourites" def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do - params = - params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.take(Pleroma.Pagination.page_keys()) - activities = ActivityPub.fetch_favourites(user, params) conn diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 958567510..9270ca267 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -44,17 +44,14 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do def home(%{assigns: %{user: user}} = conn, params) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_filtering_user", user) - |> Map.put("user", user) - - recipients = [user.ap_id | User.following(user)] + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:user, user) activities = - recipients + [user.ap_id | User.following(user)] |> ActivityPub.fetch_activities(params) |> Enum.reverse() @@ -71,10 +68,9 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do def direct(%{assigns: %{user: user}} = conn, params) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", "Create") - |> Map.put("blocking_user", user) - |> Map.put("user", user) + |> Map.put(:type, "Create") + |> Map.put(:blocking_user, user) + |> Map.put(:user, user) |> Map.put(:visibility, "direct") activities = @@ -93,9 +89,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do # GET /api/v1/timelines/public def public(%{assigns: %{user: user}} = conn, params) do - params = Map.new(params, fn {key, value} -> {to_string(key), value} end) - - local_only = params["local"] + local_only = params[:local] cfg_key = if local_only do @@ -111,11 +105,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do else activities = params - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create"]) + |> Map.put(:local_only, local_only) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() conn @@ -130,39 +124,38 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do defp hashtag_fetching(params, user, local_only) do tags = - [params["tag"], params["any"]] + [params[:tag], params[:any]] |> List.flatten() |> Enum.uniq() - |> Enum.filter(& &1) - |> Enum.map(&String.downcase(&1)) + |> Enum.reject(&is_nil/1) + |> Enum.map(&String.downcase/1) tag_all = params - |> Map.get("all", []) - |> Enum.map(&String.downcase(&1)) + |> Map.get(:all, []) + |> Enum.map(&String.downcase/1) tag_reject = params - |> Map.get("none", []) - |> Enum.map(&String.downcase(&1)) + |> Map.get(:none, []) + |> Enum.map(&String.downcase/1) _activities = params - |> Map.put("type", "Create") - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("tag", tags) - |> Map.put("tag_all", tag_all) - |> Map.put("tag_reject", tag_reject) + |> Map.put(:type, "Create") + |> Map.put(:local_only, local_only) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:tag, tags) + |> Map.put(:tag_all, tag_all) + |> Map.put(:tag_reject, tag_reject) |> ActivityPub.fetch_public_activities() end # GET /api/v1/timelines/tag/:tag def hashtag(%{assigns: %{user: user}} = conn, params) do - params = Map.new(params, fn {key, value} -> {to_string(key), value} end) - local_only = params["local"] + local_only = params[:local] activities = hashtag_fetching(params, user, local_only) conn diff --git a/lib/pleroma/web/mastodon_api/views/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex index 36071cd25..e44272c6f 100644 --- a/lib/pleroma/web/mastodon_api/views/app_view.ex +++ b/lib/pleroma/web/mastodon_api/views/app_view.ex @@ -45,10 +45,6 @@ defmodule Pleroma.Web.MastodonAPI.AppView do defp with_vapid_key(data) do vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key] - if vapid_key do - Map.put(data, "vapid_key", vapid_key) - else - data - end + Pleroma.Maps.put_if_present(data, "vapid_key", vapid_key) end end diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 2b6f84c72..fbe618377 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -24,8 +24,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do last_activity_id = with nil <- participation.last_activity_id do ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ - "user" => user, - "blocking_user" => user + user: user, + blocking_user: user }) end diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex index 458f6bc78..5b896bf3b 100644 --- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex +++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do defp with_media_attachments(data, _), do: data defp status_params(params) do - data = %{ + %{ text: params["status"], sensitive: params["sensitive"], spoiler_text: params["spoiler_text"], @@ -39,10 +39,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do poll: params["poll"], in_reply_to_id: params["in_reply_to_id"] } - - case params["media_ids"] do - nil -> data - media_ids -> Map.put(data, :media_ids, media_ids) - end + |> Pleroma.Maps.put_if_present(:media_ids, params["media_ids"]) end end diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex index 6a6d5f2e2..df99472e1 100644 --- a/lib/pleroma/web/oauth/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -25,12 +25,12 @@ defmodule Pleroma.Web.OAuth.App do timestamps() end - @spec changeset(App.t(), map()) :: Ecto.Changeset.t() + @spec changeset(t(), map()) :: Ecto.Changeset.t() def changeset(struct, params) do cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) end - @spec register_changeset(App.t(), map()) :: Ecto.Changeset.t() + @spec register_changeset(t(), map()) :: Ecto.Changeset.t() def register_changeset(struct, params \\ %{}) do changeset = struct @@ -52,18 +52,19 @@ defmodule Pleroma.Web.OAuth.App do end end - @spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} + @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} def create(params) do - with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do - Repo.insert(changeset) - end + %__MODULE__{} + |> register_changeset(params) + |> Repo.insert() end - @spec update(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} - def update(params) do - with %__MODULE__{} = app <- Repo.get(__MODULE__, params["id"]), - changeset <- changeset(app, params) do - Repo.update(changeset) + @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def update(id, params) do + with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do + app + |> changeset(params) + |> Repo.update() end end @@ -71,7 +72,7 @@ defmodule Pleroma.Web.OAuth.App do Gets app by attrs or create new with attrs. And updates the scopes if need. """ - @spec get_or_make(map(), list(String.t())) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} + @spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()} def get_or_make(attrs, scopes) do with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do update_scopes(app, scopes) @@ -92,7 +93,7 @@ defmodule Pleroma.Web.OAuth.App do |> Repo.update() end - @spec search(map()) :: {:ok, [App.t()], non_neg_integer()} + @spec search(map()) :: {:ok, [t()], non_neg_integer()} def search(params) do query = from(a in __MODULE__) @@ -128,7 +129,7 @@ defmodule Pleroma.Web.OAuth.App do {:ok, Repo.all(query), count} end - @spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} + @spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} def destroy(id) do with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do Repo.delete(app) diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 7c804233c..c557778ca 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do use Pleroma.Web, :controller alias Pleroma.Helpers.UriHelper + alias Pleroma.Maps alias Pleroma.MFA alias Pleroma.Plugs.RateLimiter alias Pleroma.Registration @@ -108,7 +109,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do if redirect_uri in String.split(app.redirect_uris) do redirect_uri = redirect_uri(conn, redirect_uri) url_params = %{access_token: token.token} - url_params = UriHelper.append_param_if_present(url_params, :state, params["state"]) + url_params = Maps.put_if_present(url_params, :state, params["state"]) url = UriHelper.append_uri_params(redirect_uri, url_params) redirect(conn, external: url) else @@ -147,7 +148,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do if redirect_uri in String.split(app.redirect_uris) do redirect_uri = redirect_uri(conn, redirect_uri) url_params = %{code: auth.token} - url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"]) + url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) url = UriHelper.append_uri_params(redirect_uri, url_params) redirect(conn, external: url) else diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 0a3f45620..f3554d919 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -126,10 +126,9 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", "Create") - |> Map.put("favorited_by", user.ap_id) - |> Map.put("blocking_user", for_user) + |> Map.put(:type, "Create") + |> Map.put(:favorited_by, user.ap_id) + |> Map.put(:blocking_user, for_user) recipients = if for_user do diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex index 21d5eb8d5..3d007f324 100644 --- a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex @@ -42,15 +42,14 @@ defmodule Pleroma.Web.PleromaAPI.ConversationController do Participation.get(participation_id, preload: [:conversation]) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) activities = participation.conversation.ap_id |> ActivityPub.fetch_activities_for_context_query(params) - |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false)) + |> Pleroma.Pagination.fetch_paginated(Map.put(params, :total, false)) |> Enum.reverse() conn diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index 8665ca56c..e9a4fba92 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -36,10 +36,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do - params = - params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", ["Listen"]) + params = Map.put(params, :type, ["Listen"]) activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e493a4153..aa272540d 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -160,14 +160,14 @@ defmodule Pleroma.Web.Router do :right_delete_multiple ) - get("/relay", AdminAPIController, :relay_list) - post("/relay", AdminAPIController, :relay_follow) - delete("/relay", AdminAPIController, :relay_unfollow) + get("/relay", RelayController, :index) + post("/relay", RelayController, :follow) + delete("/relay", RelayController, :unfollow) - post("/users/invite_token", AdminAPIController, :create_invite_token) - get("/users/invites", AdminAPIController, :invites) - post("/users/revoke_invite", AdminAPIController, :revoke_invite) - post("/users/email_invite", AdminAPIController, :email_invite) + post("/users/invite_token", InviteController, :create) + get("/users/invites", InviteController, :index) + post("/users/revoke_invite", InviteController, :revoke) + post("/users/email_invite", InviteController, :email) get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) patch("/users/force_password_reset", AdminAPIController, :force_password_reset) @@ -183,20 +183,20 @@ defmodule Pleroma.Web.Router do patch("/users/confirm_email", AdminAPIController, :confirm_email) patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) - get("/reports", AdminAPIController, :list_reports) - get("/reports/:id", AdminAPIController, :report_show) - patch("/reports", AdminAPIController, :reports_update) - post("/reports/:id/notes", AdminAPIController, :report_notes_create) - delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete) + get("/reports", ReportController, :index) + get("/reports/:id", ReportController, :show) + patch("/reports", ReportController, :update) + post("/reports/:id/notes", ReportController, :notes_create) + delete("/reports/:report_id/notes/:id", ReportController, :notes_delete) get("/statuses/:id", StatusController, :show) put("/statuses/:id", StatusController, :update) delete("/statuses/:id", StatusController, :delete) get("/statuses", StatusController, :index) - get("/config", AdminAPIController, :config_show) - post("/config", AdminAPIController, :config_update) - get("/config/descriptions", AdminAPIController, :config_descriptions) + get("/config", ConfigController, :show) + post("/config", ConfigController, :update) + get("/config/descriptions", ConfigController, :descriptions) get("/need_reboot", AdminAPIController, :need_reboot) get("/restart", AdminAPIController, :restart) @@ -205,10 +205,10 @@ defmodule Pleroma.Web.Router do post("/reload_emoji", AdminAPIController, :reload_emoji) get("/stats", AdminAPIController, :stats) - get("/oauth_app", AdminAPIController, :oauth_app_list) - post("/oauth_app", AdminAPIController, :oauth_app_create) - patch("/oauth_app/:id", AdminAPIController, :oauth_app_update) - delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete) + get("/oauth_app", OAuthAppController, :index) + post("/oauth_app", OAuthAppController, :create) + patch("/oauth_app/:id", OAuthAppController, :update) + delete("/oauth_app/:id", OAuthAppController, :delete) end scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do @@ -571,13 +571,6 @@ defmodule Pleroma.Web.Router do get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) end - scope "/", Pleroma.Web.ActivityPub do - # XXX: not really ostatus - pipe_through(:ostatus) - - get("/users/:nickname/outbox", ActivityPubController, :outbox) - end - pipeline :ap_service_actor do plug(:accepts, ["activity+json", "json"]) end @@ -602,6 +595,7 @@ defmodule Pleroma.Web.Router do get("/api/ap/whoami", ActivityPubController, :whoami) get("/users/:nickname/inbox", ActivityPubController, :read_inbox) + get("/users/:nickname/outbox", ActivityPubController, :outbox) post("/users/:nickname/outbox", ActivityPubController, :update_outbox) post("/api/ap/upload_media", ActivityPubController, :upload_media) @@ -664,6 +658,8 @@ defmodule Pleroma.Web.Router do post("/auth/password", MastodonAPI.AuthController, :password_reset) get("/web/*path", MastoFEController, :index) + + get("/embed/:id", EmbedController, :show) end scope "/proxy/", Pleroma.Web.MediaProxy do diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index c3efb6651..a7a891b13 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -111,8 +111,14 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do %User{} = user -> meta = Metadata.build_tags(%{user: user}) + params = + params + |> Map.take(@page_keys) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) + timeline = - ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys)) + user + |> ActivityPub.fetch_user_activities(nil, params) |> Enum.map(&represent/1) prev_page_id = diff --git a/lib/pleroma/web/templates/embed/_attachment.html.eex b/lib/pleroma/web/templates/embed/_attachment.html.eex new file mode 100644 index 000000000..7e04e9550 --- /dev/null +++ b/lib/pleroma/web/templates/embed/_attachment.html.eex @@ -0,0 +1,8 @@ +<%= case @mediaType do %> +<% "audio" -> %> +<audio src="<%= @url %>" controls="controls"></audio> +<% "video" -> %> +<video src="<%= @url %>" controls="controls"></video> +<% _ -> %> +<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>"> +<% end %> diff --git a/lib/pleroma/web/templates/embed/show.html.eex b/lib/pleroma/web/templates/embed/show.html.eex new file mode 100644 index 000000000..05a3f0ee3 --- /dev/null +++ b/lib/pleroma/web/templates/embed/show.html.eex @@ -0,0 +1,76 @@ +<div> + <div class="p-author h-card"> + <a class="u-url" rel="author noopener" href="<%= @author.ap_id %>"> + <div class="avatar"> + <img src="<%= User.avatar_url(@author) |> MediaProxy.url %>" width="48" height="48" alt=""> + </div> + <span class="display-name" style="padding-left: 0.5em;"> + <bdi><%= raw (@author.name |> Formatter.emojify(@author.emoji)) %></bdi> + <span class="nickname"><%= full_nickname(@author) %></span> + </span> + </a> + </div> + + <div class="activity-content" > + <%= if status_title(@activity) != "" do %> + <details <%= if open_content?() do %>open<% end %>> + <summary><%= raw status_title(@activity) %></summary> + <div><%= activity_content(@activity) %></div> + </details> + <% else %> + <div><%= activity_content(@activity) %></div> + <% end %> + <%= for %{"name" => name, "url" => [url | _]} <- attachments(@activity) do %> + <div class="attachment"> + <%= if sensitive?(@activity) do %> + <details class="nsfw"> + <summary onClick="updateHeight()"><%= Gettext.gettext("sensitive media") %></summary> + <div class="nsfw-content"> + <%= render("_attachment.html", %{name: name, url: url["href"], + mediaType: fetch_media_type(url)}) %> + </div> + </details> + <% else %> + <%= render("_attachment.html", %{name: name, url: url["href"], + mediaType: fetch_media_type(url)}) %> + <% end %> + </div> + <% end %> + </div> + + <dl class="counts pull-right"> + <dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd> + <dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd> + <dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd> + </dl> + + <p class="date pull-left"> + <%= link published(@activity), to: activity_url(@author, @activity) %> + </p> +</div> + +<script> +function updateHeight() { + window.requestAnimationFrame(function(){ + var height = document.getElementsByTagName('html')[0].scrollHeight; + + window.parent.postMessage({ + type: 'setHeightPleromaEmbed', + id: window.parentId, + height: height, + }, '*'); + }) +} + +window.addEventListener('message', function(e){ + var data = e.data || {}; + + if (!window.parent || data.type !== 'setHeightPleromaEmbed') { + return; + } + + window.parentId = data.id + + updateHeight() +}); +</script> diff --git a/lib/pleroma/web/templates/layout/embed.html.eex b/lib/pleroma/web/templates/layout/embed.html.eex new file mode 100644 index 000000000..8b905f070 --- /dev/null +++ b/lib/pleroma/web/templates/layout/embed.html.eex @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" /> + <title><%= Pleroma.Config.get([:instance, :name]) %></title> + <meta content='noindex' name='robots'> + <%= Phoenix.HTML.raw(assigns[:meta] || "") %> + <link rel="stylesheet" href="/embed.css"> + <base target="_parent"> + </head> + <body> + <%= render @view_module, @view_template, assigns %> + </body> +</html> diff --git a/lib/pleroma/web/views/embed_view.ex b/lib/pleroma/web/views/embed_view.ex new file mode 100644 index 000000000..5f50bd155 --- /dev/null +++ b/lib/pleroma/web/views/embed_view.ex @@ -0,0 +1,74 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.EmbedView do + use Pleroma.Web, :view + + alias Calendar.Strftime + alias Pleroma.Activity + alias Pleroma.Emoji.Formatter + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.Gettext + alias Pleroma.Web.MediaProxy + alias Pleroma.Web.Metadata.Utils + alias Pleroma.Web.Router.Helpers + + use Phoenix.HTML + + @media_types ["image", "audio", "video"] + + defp fetch_media_type(%{"mediaType" => mediaType}) do + Utils.fetch_media_type(@media_types, mediaType) + end + + defp open_content? do + Pleroma.Config.get( + [:frontend_configurations, :collapse_message_with_subjects], + true + ) + end + + defp full_nickname(user) do + %{host: host} = URI.parse(user.ap_id) + "@" <> user.nickname <> "@" <> host + end + + defp status_title(%Activity{object: %Object{data: %{"name" => name}}}) when is_binary(name), + do: name + + defp status_title(%Activity{object: %Object{data: %{"summary" => summary}}}) + when is_binary(summary), + do: summary + + defp status_title(_), do: nil + + defp activity_content(%Activity{object: %Object{data: %{"content" => content}}}) do + content |> Pleroma.HTML.filter_tags() |> raw() + end + + defp activity_content(_), do: nil + + defp activity_url(%User{local: true}, activity) do + Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) + end + + defp activity_url(%User{local: false}, %Activity{object: %Object{data: data}}) do + data["url"] || data["external_url"] || data["id"] + end + + defp attachments(%Activity{object: %Object{data: %{"attachment" => attachments}}}) do + attachments + end + + defp sensitive?(%Activity{object: %Object{data: %{"sensitive" => sensitive}}}) do + sensitive + end + + defp published(%Activity{object: %Object{data: %{"published" => published}}}) do + published + |> NaiveDateTime.from_iso8601!() + |> Strftime.strftime!("%B %d, %Y, %l:%M %p") + end +end |