diff options
Diffstat (limited to 'lib/pleroma/web')
53 files changed, 1026 insertions, 271 deletions
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 19286fd01..35af0f7dc 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -721,7 +721,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end - defp fetch_activities_for_context_query(context, opts) do + def fetch_activities_for_context_query(context, opts) do public = [Constants.as_public()] recipients = @@ -1427,19 +1427,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) + emojis = + data + |> Map.get("tag", []) + |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) + |> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc -> + Map.put(acc, String.trim(name, ":"), url) + end) + locked = data["manuallyApprovesFollowers"] || false data = Transmogrifier.maybe_fix_user_object(data) discoverable = data["discoverable"] || false invisible = data["invisible"] || false actor_type = data["type"] || "Person" + public_key = + if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do + data["publicKey"]["publicKeyPem"] + else + nil + end + + shared_inbox = + if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do + data["endpoints"]["sharedInbox"] + else + nil + end + user_data = %{ ap_id: data["id"], uri: get_actor_url(data["url"]), ap_enabled: true, - source_data: data, banner: banner, fields: fields, + emoji: emojis, locked: locked, discoverable: discoverable, invisible: invisible, @@ -1449,7 +1471,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do following_address: data["following"], bio: data["summary"], actor_type: actor_type, - also_known_as: Map.get(data, "alsoKnownAs", []) + also_known_as: Map.get(data, "alsoKnownAs", []), + public_key: public_key, + inbox: data["inbox"], + shared_inbox: shared_inbox } # nickname can be nil because of virtual actors @@ -1551,11 +1576,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def make_user_from_ap_id(ap_id) do - if _user = User.get_cached_by_ap_id(ap_id) do + user = User.get_cached_by_ap_id(ap_id) + + if user && !User.ap_enabled?(user) do Transmogrifier.upgrade_user_from_ap_id(ap_id) else with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do - User.insert_or_update_user(data) + if user do + user + |> User.remote_user_changeset(data) + |> User.update_and_set_cache() + else + data + |> User.remote_user_changeset() + |> Repo.insert() + |> User.set_cache() + end else e -> {:error, e} end diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex index c95b622e4..462a5620a 100644 --- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex @@ -35,6 +35,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do field(:like_count, :integer, default: 0) field(:announcement_count, :integer, default: 0) field(:inRepyTo, :string) + field(:uri, Types.Uri) field(:likes, {:array, :string}, default: []) field(:announcements, {:array, :string}, default: []) diff --git a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex index ee10be0b0..f71f76370 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex +++ b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex @@ -6,28 +6,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do def cast(object) when is_binary(object) do # Host has to be present and scheme has to be an http scheme (for now) case URI.parse(object) do - %URI{host: nil} -> - :error - - %URI{scheme: scheme} when scheme in ["https", "http"] -> - {:ok, object} - - _ -> - :error + %URI{host: nil} -> :error + %URI{host: ""} -> :error + %URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, object} + _ -> :error end end def cast(%{"id" => object}), do: cast(object) - def cast(_) do - :error - end + def cast(_), do: :error - def dump(data) do - {:ok, data} - end + def dump(data), do: {:ok, data} - def load(data) do - {:ok, data} - end + def load(data), do: {:ok, data} end diff --git a/lib/pleroma/web/activity_pub/object_validators/types/uri.ex b/lib/pleroma/web/activity_pub/object_validators/types/uri.ex new file mode 100644 index 000000000..24845bcc0 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/types/uri.ex @@ -0,0 +1,20 @@ +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Uri do + use Ecto.Type + + def type, do: :string + + def cast(uri) when is_binary(uri) do + case URI.parse(uri) do + %URI{host: nil} -> :error + %URI{host: ""} -> :error + %URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, uri} + _ -> :error + end + end + + def cast(_), do: :error + + def dump(data), do: {:ok, data} + + def load(data), do: {:ok, data} +end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 6c558e7f0..b70cbd043 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -141,8 +141,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do |> Enum.map(& &1.ap_id) end - defp maybe_use_sharedinbox(%User{source_data: data}), - do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] + defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox + defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox @doc """ Determine a user inbox to use based on heuristics. These heuristics @@ -157,7 +157,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do """ def determine_inbox( %Activity{data: activity_data}, - %User{source_data: data} = user + %User{inbox: inbox} = user ) do to = activity_data["to"] || [] cc = activity_data["cc"] || [] @@ -174,7 +174,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do maybe_use_sharedinbox(user) true -> - data["inbox"] + inbox end end @@ -192,14 +192,13 @@ defmodule Pleroma.Web.ActivityPub.Publisher do inboxes = recipients |> Enum.filter(&User.ap_enabled?/1) - |> Enum.map(fn %{source_data: data} -> data["inbox"] end) + |> Enum.map(fn actor -> actor.inbox end) |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) |> Instances.filter_reachable() Repo.checkout(fn -> Enum.each(inboxes, fn {inbox, unreachable_since} -> - %User{ap_id: ap_id} = - Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end) + %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end) # Get all the recipients on the same host and add them to cc. Otherwise, a remote # instance would only accept a first message for the first recipient and ignore the rest. diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 0a8ad62ad..09119137b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -205,16 +205,46 @@ 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 -> - media_type = data["mediaType"] || data["mimeType"] - href = data["url"] || data["href"] - url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}] + url = + cond do + is_list(data["url"]) -> List.first(data["url"]) + is_map(data["url"]) -> data["url"] + true -> nil + end - data - |> Map.put("mediaType", media_type) - |> Map.put("url", url) + media_type = + cond do + is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"] + is_binary(data["mediaType"]) -> data["mediaType"] + is_binary(data["mimeType"]) -> data["mimeType"] + true -> nil + end + + href = + cond do + is_map(url) && is_binary(url["href"]) -> url["href"] + is_binary(data["url"]) -> data["url"] + is_binary(data["href"]) -> data["href"] + end + + attachment_url = + %{"href" => href} + |> add_if_present("mediaType", media_type) + |> add_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"]) end) Map.put(object, "attachment", attachments) @@ -494,7 +524,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, {_, {:ok, _}} <- {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")}, - {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do + {:ok, _relationship} <- + FollowingRelationship.update(follower, followed, :follow_accept) do ActivityPub.accept(%{ to: [follower.ap_id], actor: followed, @@ -504,7 +535,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do else {:user_blocked, true} -> {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") - {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") + {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject) ActivityPub.reject(%{ to: [follower.ap_id], @@ -515,7 +546,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:follow, {:error, _}} -> {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") - {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") + {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject) ActivityPub.reject(%{ to: [follower.ap_id], @@ -525,7 +556,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do }) {:user_locked, true} -> - {:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending") + {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending) :noop end @@ -545,7 +576,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), - {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do + {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do ActivityPub.accept(%{ to: follow_activity.data["to"], type: "Accept", @@ -568,7 +599,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), - {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), + {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject), {:ok, activity} <- ActivityPub.reject(%{ to: follow_activity.data["to"], @@ -680,7 +711,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) actor - |> User.upgrade_changeset(new_user_data, true) + |> User.remote_user_changeset(new_user_data) |> User.update_and_set_cache() ActivityPub.update(%{ @@ -1129,7 +1160,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def take_emoji_tags(%User{emoji: emoji}) do emoji - |> Enum.flat_map(&Map.to_list/1) + |> Map.to_list() |> Enum.map(&build_emoji_tag/1) end @@ -1223,12 +1254,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def upgrade_user_from_ap_id(ap_id) do with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id), {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id), - already_ap <- User.ap_enabled?(user), - {:ok, user} <- upgrade_user(user, data) do - if not already_ap do - TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id}) - end - + {:ok, user} <- update_user(user, data) do + TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id}) {:ok, user} else %User{} = user -> {:ok, user} @@ -1236,9 +1263,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - defp upgrade_user(user, data) do + defp update_user(user, data) do user - |> User.upgrade_changeset(data, true) + |> User.remote_user_changeset(data) |> User.update_and_set_cache() end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index bc21ac6c7..34590b16d 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -79,10 +79,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do emoji_tags = Transmogrifier.take_emoji_tags(user) - fields = - user - |> User.fields() - |> Enum.map(&Map.put(&1, "type", "PropertyValue")) + fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue")) %{ "id" => user.ap_id, @@ -103,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do }, "endpoints" => endpoints, "attachment" => fields, - "tag" => (user.source_data["tag"] || []) ++ emoji_tags, + "tag" => emoji_tags, "discoverable" => user.discoverable } |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index ca5439920..9c79310c0 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -27,7 +27,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint + alias Pleroma.Web.MastodonAPI.AppView alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.OAuth.App alias Pleroma.Web.Router require Logger @@ -258,7 +260,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do conn |> put_view(Pleroma.Web.AdminAPI.StatusView) - |> render("index.json", %{activities: activities, as: :activity}) + |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false}) end def list_user_statuses(conn, %{"nickname" => nickname} = params) do @@ -277,7 +279,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do conn |> put_view(StatusView) - |> render("index.json", %{activities: activities, as: :activity}) + |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false}) else _ -> {:error, :not_found} end @@ -576,9 +578,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do @doc "Sends registration invite via email" def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do - with true <- - Config.get([:instance, :invites_enabled]) && - !Config.get([:instance, :registrations_open]), + 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( @@ -589,6 +590,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do ), {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do json_response(conn, :no_content, "") + else + {:registrations_open, _} -> + errors( + conn, + {:error, "To send invites you need to set the `registrations_open` option to false."} + ) + + {:invites_enabled, _} -> + errors( + conn, + {:error, "To send invites you need to set the `invites_enabled` option to true."} + ) end end @@ -801,7 +814,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do conn |> put_view(Pleroma.Web.AdminAPI.StatusView) - |> render("index.json", %{activities: activities, as: :activity}) + |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false}) end def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do @@ -903,16 +916,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end) |> List.flatten() - response = %{configs: merged} - - response = - if Restarter.Pleroma.need_reboot?() do - Map.put(response, :need_reboot, true) - else - response - end - - json(conn, response) + json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) end end @@ -939,28 +943,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do Config.TransferTask.load_and_update_env(deleted, false) - need_reboot? = - Restarter.Pleroma.need_reboot?() || - Enum.any?(updated, fn config -> + 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) - response = %{configs: updated} - - response = - if need_reboot? do - Restarter.Pleroma.need_reboot() - Map.put(response, :need_reboot, need_reboot?) - else - response - end + if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() + end conn |> put_view(ConfigView) - |> render("index.json", response) + |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()}) end end @@ -972,6 +970,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end + def need_reboot(conn, _params) do + json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()}) + end + defp configurable_from_database(conn) do if Config.get(:configurable_from_database) do :ok @@ -1017,6 +1019,83 @@ 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/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index ca0bcebc7..d50969b2a 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -38,7 +38,12 @@ defmodule Pleroma.Web.AdminAPI.ReportView do actor: merge_account_views(user), content: content, created_at: created_at, - statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), + statuses: + StatusView.render("index.json", %{ + activities: statuses, + as: :activity, + skip_relationships: false + }), state: report.data["state"], notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}) } diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex new file mode 100644 index 000000000..3890489e3 --- /dev/null +++ b/lib/pleroma/web/api_spec.ex @@ -0,0 +1,44 @@ +# 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 do + alias OpenApiSpex.OpenApi + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Router + + @behaviour OpenApi + + @impl OpenApi + def spec do + %OpenApi{ + servers: [ + # Populate the Server info from a phoenix endpoint + OpenApiSpex.Server.from_endpoint(Endpoint) + ], + info: %OpenApiSpex.Info{ + title: "Pleroma", + description: Application.spec(:pleroma, :description) |> to_string(), + version: Application.spec(:pleroma, :vsn) |> to_string() + }, + # populate the paths from a phoenix router + paths: OpenApiSpex.Paths.from_router(Router), + components: %OpenApiSpex.Components{ + securitySchemes: %{ + "oAuth" => %OpenApiSpex.SecurityScheme{ + type: "oauth2", + flows: %OpenApiSpex.OAuthFlows{ + password: %OpenApiSpex.OAuthFlow{ + authorizationUrl: "/oauth/authorize", + tokenUrl: "/oauth/token", + scopes: %{"read" => "read", "write" => "write", "follow" => "follow"} + } + } + } + } + } + } + # discover request/response schemas from path specs + |> OpenApiSpex.resolve_schema_modules() + end +end diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex new file mode 100644 index 000000000..7348dcbee --- /dev/null +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -0,0 +1,27 @@ +# 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.Helpers do + def request_body(description, schema_ref, opts \\ []) do + media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"] + + content = + media_types + |> Enum.map(fn type -> + {type, + %OpenApiSpex.MediaType{ + schema: schema_ref, + example: opts[:example], + examples: opts[:examples] + }} + end) + |> Enum.into(%{}) + + %OpenApiSpex.RequestBody{ + description: description, + content: content, + required: opts[:required] || false + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex new file mode 100644 index 000000000..26d8dbd42 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/app_operation.ex @@ -0,0 +1,96 @@ +# 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.AppOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest + alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse + + @spec open_api_operation(atom) :: Operation.t() + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + @spec create_operation() :: Operation.t() + def create_operation do + %Operation{ + tags: ["apps"], + summary: "Create an application", + description: "Create a new application to obtain OAuth2 credentials", + operationId: "AppController.create", + requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true), + responses: %{ + 200 => Operation.response("App", "application/json", AppCreateResponse), + 422 => + Operation.response( + "Unprocessable Entity", + "application/json", + %Schema{ + type: :object, + description: + "If a required parameter is missing or improperly formatted, the request will fail.", + properties: %{ + error: %Schema{type: :string} + }, + example: %{ + "error" => "Validation failed: Redirect URI must be an absolute URI." + } + } + ) + } + } + end + + def verify_credentials_operation do + %Operation{ + tags: ["apps"], + summary: "Verify your app works", + description: "Confirm that the app's OAuth2 credentials work.", + operationId: "AppController.verify_credentials", + security: [ + %{ + "oAuth" => ["read"] + } + ], + responses: %{ + 200 => + Operation.response("App", "application/json", %Schema{ + type: :object, + description: + "If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.", + properties: %{ + name: %Schema{type: :string}, + vapid_key: %Schema{type: :string}, + website: %Schema{type: :string, nullable: true} + }, + example: %{ + "name" => "My App", + "vapid_key" => + "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=", + "website" => "https://myapp.com/" + } + }), + 422 => + Operation.response( + "Unauthorized", + "application/json", + %Schema{ + type: :object, + description: + "If the Authorization header contains an invalid token, is malformed, or is not present, an error will be returned indicating an authorization failure.", + properties: %{ + error: %Schema{type: :string} + }, + example: %{ + "error" => "The access token is invalid." + } + } + ) + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/domain_block_operation.ex b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex new file mode 100644 index 000000000..dd14837c3 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex @@ -0,0 +1,64 @@ +# 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.DomainBlockOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest + alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["domain_blocks"], + summary: "Fetch domain blocks", + description: "View domains the user has blocked.", + security: [%{"oAuth" => ["follow", "read:blocks"]}], + operationId: "DomainBlockController.index", + responses: %{ + 200 => Operation.response("Domain blocks", "application/json", DomainBlocksResponse) + } + } + end + + def create_operation do + %Operation{ + tags: ["domain_blocks"], + summary: "Block a domain", + description: """ + Block a domain to: + + - hide all public posts from it + - hide all notifications from it + - remove all followers from it + - prevent following new users from it (but does not remove existing follows) + """, + operationId: "DomainBlockController.create", + requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true), + security: [%{"oAuth" => ["follow", "write:blocks"]}], + responses: %{ + 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) + } + } + end + + def delete_operation do + %Operation{ + tags: ["domain_blocks"], + summary: "Unblock a domain", + description: "Remove a domain block, if it exists in the user's array of blocked domains.", + operationId: "DomainBlockController.delete", + requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true), + security: [%{"oAuth" => ["follow", "write:blocks"]}], + responses: %{ + 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) + } + } + end +end diff --git a/lib/pleroma/web/api_spec/schemas/app_create_request.ex b/lib/pleroma/web/api_spec/schemas/app_create_request.ex new file mode 100644 index 000000000..8a83abef3 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/app_create_request.ex @@ -0,0 +1,33 @@ +# 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.Schemas.AppCreateRequest do + alias OpenApiSpex.Schema + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "AppCreateRequest", + description: "POST body for creating an app", + type: :object, + properties: %{ + client_name: %Schema{type: :string, description: "A name for your application."}, + 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." + }, + scopes: %Schema{ + type: :string, + description: "Space separated list of scopes. If none is provided, defaults to `read`." + }, + website: %Schema{type: :string, description: "A URL to the homepage of your app"} + }, + required: [:client_name, :redirect_uris], + example: %{ + "client_name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/" + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/app_create_response.ex b/lib/pleroma/web/api_spec/schemas/app_create_response.ex new file mode 100644 index 000000000..f290fb031 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/app_create_response.ex @@ -0,0 +1,33 @@ +# 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.Schemas.AppCreateResponse do + alias OpenApiSpex.Schema + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "AppCreateResponse", + description: "Response schema for an app", + type: :object, + properties: %{ + id: %Schema{type: :string}, + name: %Schema{type: :string}, + client_id: %Schema{type: :string}, + client_secret: %Schema{type: :string}, + redirect_uri: %Schema{type: :string}, + vapid_key: %Schema{type: :string}, + website: %Schema{type: :string, nullable: true} + }, + example: %{ + "id" => "123", + "name" => "My App", + "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", + "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", + "vapid_key" => + "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=", + "website" => "https://myapp.com/" + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/domain_block_request.ex b/lib/pleroma/web/api_spec/schemas/domain_block_request.ex new file mode 100644 index 000000000..ee9238361 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/domain_block_request.ex @@ -0,0 +1,20 @@ +# 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.Schemas.DomainBlockRequest do + alias OpenApiSpex.Schema + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "DomainBlockRequest", + type: :object, + properties: %{ + domain: %Schema{type: :string} + }, + required: [:domain], + example: %{ + "domain" => "facebook.com" + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex b/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex new file mode 100644 index 000000000..d895aca4e --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex @@ -0,0 +1,16 @@ +# 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.Schemas.DomainBlocksResponse do + require OpenApiSpex + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + title: "DomainBlocksResponse", + description: "Response schema for domain blocks", + type: :array, + items: %Schema{type: :string}, + example: ["google.com", "facebook.com"] + }) +end diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index c4356f93b..c1cd15bb2 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -187,7 +187,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end defp preview?(draft) do - preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) || false + preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) %__MODULE__{draft | preview?: preview?} end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 636cf3301..f50a909aa 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -45,7 +45,7 @@ defmodule Pleroma.Web.CommonAPI do with {:ok, follower} <- User.follow(follower, followed), %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), - {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"), + {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept), {:ok, _activity} <- ActivityPub.accept(%{ to: [follower.ap_id], @@ -60,7 +60,7 @@ defmodule Pleroma.Web.CommonAPI do def reject_follow_request(follower, followed) do with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), - {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), + {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject), {:ok, _activity} <- ActivityPub.reject(%{ to: [follower.ap_id], @@ -332,26 +332,6 @@ defmodule Pleroma.Web.CommonAPI do defp maybe_create_activity_expiration(result, _), do: result - # Updates the emojis for a user based on their profile - def update(user) do - emoji = emoji_from_profile(user) - source_data = Map.put(user.source_data, "tag", emoji) - - user = - case User.update_source_data(user, source_data) do - {:ok, user} -> user - _ -> user - end - - ActivityPub.update(%{ - local: true, - to: [Pleroma.Constants.as_public(), user.follower_address], - cc: [], - actor: user.ap_id, - object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user}) - }) - end - def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do with %Activity{ actor: ^user_ap_id, diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 635e7cd38..7eec5aa09 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -10,7 +10,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Pleroma.Activity alias Pleroma.Config alias Pleroma.Conversation.Participation - alias Pleroma.Emoji alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.Plugs.AuthenticationPlug @@ -18,7 +17,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.Endpoint alias Pleroma.Web.MediaProxy require Logger @@ -175,7 +173,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do "replies" => %{"type" => "Collection", "totalItems" => 0} } - {note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))} + {note, Map.merge(emoji, Pleroma.Emoji.Formatter.get_emoji_map(option))} end) end_time = @@ -431,19 +429,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do end end - def emoji_from_profile(%User{bio: bio, name: name}) do - [bio, name] - |> Enum.map(&Emoji.Formatter.get_emoji/1) - |> Enum.concat() - |> Enum.map(fn {shortcode, %Emoji{file: path}} -> - %{ - "type" => "Emoji", - "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{path}"}, - "name" => ":#{shortcode}:" - } - end) - end - def maybe_notify_to_recipients( recipients, %Activity{data: %{"to" => to, "type" => _type}} = _activity diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index b49523ec3..4780081b2 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -5,10 +5,18 @@ defmodule Pleroma.Web.ControllerHelper do use Pleroma.Web, :controller - # As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html + alias Pleroma.Config + + # 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"] - def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil - def truthy_param?(value), do: value not in @falsy_param_values + + def explicitly_falsy_param?(value), do: value in @falsy_param_values + + # Note: `nil` and `""` are considered falsy values in Pleroma + def falsy_param?(value), + do: explicitly_falsy_param?(value) or value in [nil, ""] + + def truthy_param?(value), do: not falsy_param?(value) def json_response(conn, status, json) do conn @@ -96,4 +104,14 @@ defmodule Pleroma.Web.ControllerHelper do def put_if_exist(map, _key, nil), do: map def put_if_exist(map, key, value), do: Map.put(map, key, value) + + @doc "Whether to skip rendering `[:account][:pleroma][:relationship]`for statuses/notifications" + def skip_relationships?(params) do + if Config.get([:extensions, :output_relationships_in_statuses_by_default]) do + false + else + # BREAKING: older PleromaFE versions do not send this param but _do_ expect relationships. + not truthy_param?(params["with_relationships"]) + end + end end diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex index e18adaea8..1ae03e7e2 100644 --- a/lib/pleroma/web/feed/feed_view.ex +++ b/lib/pleroma/web/feed/feed_view.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Web.Feed.FeedView do def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}") def prepare_activity(activity, opts \\ []) do - object = activity_object(activity) + object = Object.normalize(activity) actor = if opts[:actor] do @@ -33,7 +33,6 @@ defmodule Pleroma.Web.Feed.FeedView do %{ activity: activity, data: Map.get(object, :data), - object: object, actor: actor } end @@ -68,9 +67,7 @@ defmodule Pleroma.Web.Feed.FeedView do def last_activity(activities), do: List.last(activities) - def activity_object(activity), do: Object.normalize(activity) - - def activity_title(%{data: %{"content" => content}}, opts \\ %{}) do + def activity_title(%{"content" => content}, opts \\ %{}) do content |> Pleroma.Web.Metadata.Utils.scrub_html() |> Pleroma.Emoji.Formatter.demojify() @@ -78,7 +75,7 @@ defmodule Pleroma.Web.Feed.FeedView do |> escape() end - def activity_content(%{data: %{"content" => content}}) do + def activity_content(%{"content" => content}) do content |> String.replace(~r/[\n\r]/, "") |> escape() diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index 43649ad26..557cde328 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastoFEController do when action == :index ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index) + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :manifest]) @doc "GET /web/*path" def index(%{assigns: %{user: user, token: token}} = conn, _params) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 21bc3d5a5..e8e59ac66 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -6,7 +6,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, - only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3] + only: [ + add_link_headers: 2, + truthy_param?: 1, + assign_account_by_id: 2, + json_response: 3, + skip_relationships?: 1 + ] alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter @@ -15,10 +21,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.MastodonAPI + alias Pleroma.Web.MastodonAPI.MastodonAPIController alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.OAuth.Token alias Pleroma.Web.TwitterAPI.TwitterAPI + plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs) + plug( OAuthScopesPlug, %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]} @@ -95,6 +104,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do |> Map.put("fullname", params["fullname"] || nickname) |> Map.put("bio", params["bio"] || "") |> Map.put("confirm", params["password"]) + |> Map.put("trusted_app", app.trusted) with :ok <- validate_email_param(params), {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), @@ -140,9 +150,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end @doc "PATCH /api/v1/accounts/update_credentials" - def update_credentials(%{assigns: %{user: original_user}} = conn, params) do - user = original_user - + def update_credentials(%{assigns: %{user: user}} = conn, params) do user_params = [ :no_rich_text, @@ -178,8 +186,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do changeset = User.update_changeset(user, user_params) with {:ok, user} <- User.update_and_set_cache(changeset) do - if original_user != user, do: CommonAPI.update(user) - render(conn, "show.json", user: user, for: user, with_pleroma_settings: true) else _e -> render_error(conn, :forbidden, "Invalid request") @@ -237,7 +243,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do conn |> add_link_headers(activities) |> put_view(StatusView) - |> render("index.json", activities: activities, for: reading_user, as: :activity) + |> render("index.json", + activities: activities, + for: reading_user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) else _e -> render_error(conn, :not_found, "Can't find user") end @@ -369,6 +380,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end @doc "GET /api/v1/endorsements" - def endorsements(conn, params), - do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params) + def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params) + + @doc "GET /api/v1/identity_proofs" + def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params) end diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index 5e2871f18..005c60444 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -14,17 +14,20 @@ defmodule Pleroma.Web.MastodonAPI.AppController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials) + plug(OpenApiSpex.Plug.CastAndValidate) @local_mastodon_name "Mastodon-Local" + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation + @doc "POST /api/v1/apps" - def create(conn, params) do + def create(%{body_params: params} = conn, _params) do scopes = Scopes.fetch_scopes(params, ["read"]) app_attrs = params - |> Map.drop(["scope", "scopes"]) - |> Map.put("scopes", scopes) + |> Map.take([:client_name, :redirect_uris, :website]) + |> Map.put(:scopes, scopes) with cs <- App.register_changeset(%App{}, app_attrs), false <- cs.changes[:client_name] == @local_mastodon_name, diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex index e4156cbe6..84de79413 100644 --- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -8,6 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User + plug(OpenApiSpex.Plug.CastAndValidate) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation + plug( OAuthScopesPlug, %{scopes: ["follow", "read:blocks"]} when action == :index @@ -26,13 +29,13 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do end @doc "POST /api/v1/domain_blocks" - def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do + def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do User.block_domain(blocker, domain) json(conn, %{}) end @doc "DELETE /api/v1/domain_blocks" - def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do + def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do User.unblock_domain(blocker, domain) json(conn, %{}) end diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index 14075307d..ac8c18f24 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -3,21 +3,31 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do + @moduledoc """ + Contains stubs for unimplemented Mastodon API endpoints. + + Note: instead of routing directly to this controller's action, + it's preferable to define an action in relevant (non-generic) controller, + set up OAuth rules for it and call this controller's function from it. + """ + use Pleroma.Web, :controller require Logger + plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object]) + + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) - # Stubs for unimplemented mastodon api - # def empty_array(conn, _) do - Logger.debug("Unimplemented, returning an empty array") + Logger.debug("Unimplemented, returning an empty array (list)") json(conn, []) end def empty_object(conn, _) do - Logger.debug("Unimplemented, returning an empty object") + Logger.debug("Unimplemented, returning an empty object (map)") json(conn, %{}) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 0c9218454..7fb536b09 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1] alias Pleroma.Notification alias Pleroma.Plugs.OAuthScopesPlug @@ -45,7 +45,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do conn |> add_link_headers(notifications) - |> render("index.json", notifications: notifications, for: user) + |> render("index.json", + notifications: notifications, + for: user, + skip_relationships: skip_relationships?(params) + ) end # GET /api/v1/notifications/:id @@ -66,7 +70,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do json(conn, %{}) end - # POST /api/v1/notifications/dismiss + # POST /api/v1/notifications/:id/dismiss + # POST /api/v1/notifications/dismiss (deprecated) def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do with {:ok, _notif} <- Notification.dismiss(user, id) do json(conn, %{}) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index fcab4ef63..c258742dd 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -5,13 +5,14 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do use Pleroma.Web, :controller + import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 2, skip_relationships?: 1] + alias Pleroma.Activity alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web - alias Pleroma.Web.ControllerHelper alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView @@ -66,10 +67,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do defp search_options(params, user) do [ + skip_relationships: skip_relationships?(params), resolve: params["resolve"] == "true", following: params["following"] == "true", - limit: ControllerHelper.fetch_integer_param(params, "limit"), - offset: ControllerHelper.fetch_integer_param(params, "offset"), + limit: fetch_integer_param(params, "limit"), + offset: fetch_integer_param(params, "offset"), type: params["type"], author: get_author(params), for_user: user @@ -79,12 +81,24 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do defp resource_search(_, "accounts", query, options) do accounts = with_fallback(fn -> User.search(query, options) end) - AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user) + + AccountView.render("index.json", + users: accounts, + for: options[:for_user], + as: :user, + skip_relationships: false + ) end defp resource_search(_, "statuses", query, options) do statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end) - StatusView.render("index.json", activities: statuses, for: options[:for_user], as: :activity) + + StatusView.render("index.json", + activities: statuses, + for: options[:for_user], + as: :activity, + skip_relationships: options[:skip_relationships] + ) end defp resource_search(:v2, "hashtags", query, _options) do diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index ec8f0d8a0..397dd10e3 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -5,7 +5,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2] + import Pleroma.Web.ControllerHelper, + only: [try_render: 3, add_link_headers: 2, skip_relationships?: 1] require Ecto.Query @@ -101,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do `ids` query param is required """ - def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do + def index(%{assigns: %{user: user}} = conn, %{"ids" => ids} = params) do limit = 100 activities = @@ -110,7 +111,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do |> Activity.all_by_ids_with_object() |> Enum.filter(&Visibility.visible_for_user?(&1, user)) - render(conn, "index.json", activities: activities, for: user, as: :activity) + render(conn, "index.json", + activities: activities, + for: user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) end @doc """ @@ -360,7 +366,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do conn |> add_link_headers(activities) - |> render("index.json", activities: activities, for: user, as: :activity) + |> render("index.json", + activities: activities, + for: user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) end @doc "GET /api/v1/bookmarks" @@ -378,6 +389,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do conn |> add_link_headers(bookmarks) - |> render("index.json", %{activities: activities, for: user, as: :activity}) + |> render("index.json", + activities: activities, + for: user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex index 0cdc7bd8d..c93a43969 100644 --- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex @@ -5,10 +5,13 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionController do use Pleroma.Web, :controller + alias Pleroma.Plugs.OAuthScopesPlug + require Logger + plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index) + @doc "GET /api/v1/suggestions" - def index(conn, _) do - json(conn, []) - end + def index(conn, params), + do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params) end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 91f41416d..b3c58005e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, - only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1] + only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1] alias Pleroma.Pagination alias Pleroma.Plugs.OAuthScopesPlug @@ -14,9 +14,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - # TODO: Replace with a macro when there is a Phoenix release with + # TODO: Replace with a macro when there is a Phoenix release with the following commit in it: # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e - # in it plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct) plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public) @@ -49,7 +48,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do conn |> add_link_headers(activities) - |> render("index.json", activities: activities, for: user, as: :activity) + |> render("index.json", + activities: activities, + for: user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) end # GET /api/v1/timelines/direct @@ -68,7 +72,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do conn |> add_link_headers(activities) - |> render("index.json", activities: activities, for: user, as: :activity) + |> render("index.json", + activities: activities, + for: user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) end # GET /api/v1/timelines/public @@ -95,7 +104,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do conn |> add_link_headers(activities, %{"local" => local_only}) - |> render("index.json", activities: activities, for: user, as: :activity) + |> render("index.json", + activities: activities, + for: user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) else render_error(conn, :unauthorized, "authorization required for timeline view") end @@ -140,7 +154,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do conn |> add_link_headers(activities, %{"local" => local_only}) - |> render("index.json", activities: activities, for: user, as: :activity) + |> render("index.json", + activities: activities, + for: user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) end # GET /api/v1/timelines/list/:list_id @@ -164,7 +183,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> ActivityPub.fetch_activities_bounded(following, params) |> Enum.reverse() - render(conn, "index.json", activities: activities, for: user, as: :activity) + render(conn, "index.json", + activities: activities, + for: user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) else _e -> render_error(conn, :forbidden, "Error.") end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 99e62f580..b4b61e74c 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do def render("index.json", %{users: users} = opts) do reading_user = opts[:for] + # Note: :skip_relationships option is currently intentionally not supported for accounts relationships_opt = cond do Map.has_key?(opts, :relationships) -> @@ -73,7 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do followed_by = if following_relationships do case FollowingRelationship.find(following_relationships, target, reading_user) do - %{state: "accept"} -> true + %{state: :follow_accept} -> true _ -> false end else @@ -83,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags %{ id: to_string(target.id), - following: follow_state == "accept", + following: follow_state == :follow_accept, followed_by: followed_by, blocking: UserRelationship.exists?( @@ -125,7 +126,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do reading_user, &User.subscribed_to?(&2, &1) ), - requested: follow_state == "pending", + requested: follow_state == :follow_pending, domain_blocking: User.blocks_domain?(reading_user, target), showing_reblogs: not UserRelationship.exists?( @@ -180,23 +181,25 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do bot = user.actor_type in ["Application", "Service"] emojis = - (user.source_data["tag"] || []) - |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) - |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> + Enum.map(user.emoji, fn {shortcode, url} -> %{ - "shortcode" => String.trim(name, ":"), - "url" => MediaProxy.url(url), - "static_url" => MediaProxy.url(url), + "shortcode" => shortcode, + "url" => url, + "static_url" => url, "visible_in_picker" => false } end) relationship = - render("relationship.json", %{ - user: opts[:for], - target: user, - relationships: opts[:relationships] - }) + if opts[:skip_relationships] do + %{} + else + render("relationship.json", %{ + user: opts[:for], + target: user, + relationships: opts[:relationships] + }) + end %{ id: to_string(user.id), diff --git a/lib/pleroma/web/mastodon_api/views/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex index d934e2107..36071cd25 100644 --- a/lib/pleroma/web/mastodon_api/views/app_view.ex +++ b/lib/pleroma/web/mastodon_api/views/app_view.ex @@ -7,6 +7,21 @@ defmodule Pleroma.Web.MastodonAPI.AppView do alias Pleroma.Web.OAuth.App + def render("index.json", %{apps: apps, count: count, page_size: page_size, admin: true}) do + %{ + apps: render_many(apps, Pleroma.Web.MastodonAPI.AppView, "show.json", %{admin: true}), + count: count, + page_size: page_size + } + end + + def render("show.json", %{admin: true, app: %App{} = app} = assigns) do + "show.json" + |> render(Map.delete(assigns, :admin)) + |> Map.put(:trusted, app.trusted) + |> Map.put(:id, app.id) + end + def render("show.json", %{app: %App{} = app}) do %{ id: app.id |> to_string, diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 7001fd7b9..4da1ab67f 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -51,14 +51,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do |> Enum.filter(& &1) |> Kernel.++(move_activities_targets) - UserRelationship.view_relationships_option(reading_user, actors) + UserRelationship.view_relationships_option(reading_user, actors, + source_mutes_only: opts[:skip_relationships] + ) end - opts = %{ - for: reading_user, - parent_activities: parent_activities, - relationships: relationships_opt - } + opts = + opts + |> Map.put(:parent_activities, parent_activities) + |> Map.put(:relationships, relationships_opt) safe_render_many(notifications, NotificationView, "show.json", opts) end @@ -82,12 +83,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do mastodon_type = Activity.mastodon_notification_type(activity) + render_opts = %{ + relationships: opts[:relationships], + skip_relationships: opts[:skip_relationships] + } + with %{id: _} = account <- - AccountView.render("show.json", %{ - user: actor, - for: reading_user, - relationships: opts[:relationships] - }) do + AccountView.render( + "show.json", + Map.merge(render_opts, %{user: actor, for: reading_user}) + ) do response = %{ id: to_string(notification.id), type: mastodon_type, @@ -98,8 +103,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do } } - render_opts = %{relationships: opts[:relationships]} - case mastodon_type do "mention" -> put_status(response, activity, reading_user, render_opts) @@ -111,6 +114,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do put_status(response, parent_activity_fn.(), reading_user, render_opts) "move" -> + # Note: :skip_relationships option being applied to _account_ rendering (here) put_target(response, activity, reading_user, render_opts) "pleroma:emoji_reaction" -> diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index cea76e735..b5850e1ae 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -99,7 +99,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do true -> actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) - UserRelationship.view_relationships_option(reading_user, actors) + UserRelationship.view_relationships_option(reading_user, actors, + source_mutes_only: opts[:skip_relationships] + ) end opts = @@ -153,7 +155,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do AccountView.render("show.json", %{ user: user, for: opts[:for], - relationships: opts[:relationships] + relationships: opts[:relationships], + skip_relationships: opts[:skip_relationships] }), in_reply_to_id: nil, in_reply_to_account_id: nil, @@ -301,6 +304,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do _ -> [] end + # Status muted state (would do 1 request per status unless user mutes are preloaded) muted = thread_muted? || UserRelationship.exists?( @@ -319,7 +323,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do AccountView.render("show.json", %{ user: user, for: opts[:for], - relationships: opts[:relationships] + relationships: opts[:relationships], + skip_relationships: opts[:skip_relationships] }), in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex index 01ed326f4..6a6d5f2e2 100644 --- a/lib/pleroma/web/oauth/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.OAuth.App do use Ecto.Schema import Ecto.Changeset + import Ecto.Query alias Pleroma.Repo @type t :: %__MODULE__{} @@ -16,14 +17,24 @@ defmodule Pleroma.Web.OAuth.App do field(:website, :string) field(:client_id, :string) field(:client_secret, :string) + field(:trusted, :boolean, default: false) + + has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all) + has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all) timestamps() end + @spec changeset(App.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() def register_changeset(struct, params \\ %{}) do changeset = struct - |> cast(params, [:client_name, :redirect_uris, :scopes, :website]) + |> changeset(params) |> validate_required([:client_name, :redirect_uris, :scopes]) if changeset.valid? do @@ -41,6 +52,21 @@ defmodule Pleroma.Web.OAuth.App do end end + @spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} + def create(params) do + with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do + Repo.insert(changeset) + end + 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) + end + end + @doc """ Gets app by attrs or create new with attrs. And updates the scopes if need. @@ -65,4 +91,58 @@ defmodule Pleroma.Web.OAuth.App do |> change(%{scopes: scopes}) |> Repo.update() end + + @spec search(map()) :: {:ok, [App.t()], non_neg_integer()} + def search(params) do + query = from(a in __MODULE__) + + query = + if params[:client_name] do + from(a in query, where: a.client_name == ^params[:client_name]) + else + query + end + + query = + if params[:client_id] do + from(a in query, where: a.client_id == ^params[:client_id]) + else + query + end + + query = + if Map.has_key?(params, :trusted) do + from(a in query, where: a.trusted == ^params[:trusted]) + else + query + end + + query = + from(u in query, + limit: ^params[:page_size], + offset: ^((params[:page] - 1) * params[:page_size]) + ) + + count = Repo.aggregate(__MODULE__, :count, :id) + + {:ok, Repo.all(query), count} + end + + @spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} + def destroy(id) do + with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do + Repo.delete(app) + end + end + + @spec errors(Ecto.Changeset.t()) :: map() + def errors(changeset) do + Enum.reduce(changeset.errors, %{}, fn + {:client_name, {error, _}}, acc -> + Map.put(acc, :name, error) + + {key, {error, _}}, acc -> + Map.put(acc, key, error) + end) + end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 46688db7e..0121cd661 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -27,6 +27,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do plug(:fetch_flash) plug(RateLimiter, [name: :authentication] when action == :create_authorization) + plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug) + action_fallback(Pleroma.Web.OAuth.FallbackController) @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 8ecf901f3..1023f16d4 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -15,7 +15,12 @@ defmodule Pleroma.Web.OAuth.Scopes do Note: `scopes` is used by Mastodon — supporting it but sticking to OAuth's standard `scope` wherever we control it """ - @spec fetch_scopes(map(), list()) :: list() + @spec fetch_scopes(map() | struct(), list()) :: list() + + def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do + parse_scopes(scopes, default) + end + def fetch_scopes(params, default) do parse_scopes(params["scope"] || params["scopes"], default) end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index dcba67d03..60405fbff 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -6,14 +6,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, - only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] + only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1] alias Ecto.Changeset alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.StatusView require Pleroma.Constants @@ -58,38 +57,32 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do @doc "PATCH /api/v1/pleroma/accounts/update_avatar" def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do - {:ok, user} = + {:ok, _user} = user |> Changeset.change(%{avatar: nil}) |> User.update_and_set_cache() - CommonAPI.update(user) - json(conn, %{url: nil}) end def update_avatar(%{assigns: %{user: user}} = conn, params) do {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar) - {:ok, user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache() + {:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache() %{"url" => [%{"href" => href} | _]} = data - CommonAPI.update(user) - json(conn, %{url: href}) end @doc "PATCH /api/v1/pleroma/accounts/update_banner" def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do - with {:ok, user} <- User.update_banner(user, %{}) do - CommonAPI.update(user) + with {:ok, _user} <- User.update_banner(user, %{}) do json(conn, %{url: nil}) end end def update_banner(%{assigns: %{user: user}} = conn, params) do with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), - {:ok, user} <- User.update_banner(user, object.data) do - CommonAPI.update(user) + {:ok, _user} <- User.update_banner(user, object.data) do %{"url" => [%{"href" => href} | _]} = object.data json(conn, %{url: href}) @@ -139,7 +132,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do conn |> add_link_headers(activities) |> put_view(StatusView) - |> render("index.json", activities: activities, for: for_user, as: :activity) + |> render("index.json", + activities: activities, + for: for_user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) end @doc "POST /api/v1/pleroma/accounts/:id/subscribe" diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index dae7f0f2f..fe1b97a20 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1] alias Pleroma.Activity alias Pleroma.Conversation.Participation @@ -34,7 +34,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do plug( OAuthScopesPlug, - %{scopes: ["write:conversations"]} when action == :update_conversation + %{scopes: ["write:conversations"]} when action in [:update_conversation, :read_conversations] ) plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification) @@ -110,12 +110,11 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do end def conversation_statuses( - %{assigns: %{user: user}} = conn, + %{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => participation_id} = params ) do - with %Participation{} = participation <- - Participation.get(participation_id, preload: [:conversation]), - true <- user.id == participation.user_id do + with %Participation{user_id: ^user_id} = participation <- + Participation.get(participation_id, preload: [:conversation]) do params = params |> Map.put("blocking_user", user) @@ -124,13 +123,19 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do activities = participation.conversation.ap_id - |> ActivityPub.fetch_activities_for_context(params) + |> ActivityPub.fetch_activities_for_context_query(params) + |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false)) |> Enum.reverse() conn |> add_link_headers(activities) |> put_view(StatusView) - |> render("index.json", %{activities: activities, for: user, as: :activity}) + |> render("index.json", + activities: activities, + for: user, + as: :activity, + skip_relationships: skip_relationships?(params) + ) else _error -> conn @@ -184,13 +189,17 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do end end - def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do + def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do with notifications <- Notification.set_read_up_to(user, max_id) do notifications = Enum.take(notifications, 80) conn |> put_view(NotificationView) - |> render("index.json", %{notifications: notifications, for: user}) + |> render("index.json", + notifications: notifications, + for: user, + skip_relationships: skip_relationships?(params) + ) end end end diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 0314535d2..9d3d7f978 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -64,5 +64,8 @@ defmodule Pleroma.Web.RichMedia.Helpers do def fetch_data_for_activity(_), do: %{} - def perform(:fetch, %Activity{} = activity), do: fetch_data_for_activity(activity) + def perform(:fetch, %Activity{} = activity) do + fetch_data_for_activity(activity) + :ok + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 5a0902739..7e5960949 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -29,11 +29,13 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.EnsureUserKeyPlug) plug(Pleroma.Plugs.IdempotencyPlug) + plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec) end pipeline :authenticated_api do plug(:accepts, ["json"]) plug(:fetch_session) + plug(Pleroma.Plugs.AuthExpectedPlug) plug(Pleroma.Plugs.OAuthPlug) plug(Pleroma.Plugs.BasicAuthDecoderPlug) plug(Pleroma.Plugs.UserFetcherPlug) @@ -44,6 +46,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Plugs.IdempotencyPlug) + plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec) end pipeline :admin_api do @@ -61,6 +64,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Plugs.UserIsAdminPlug) plug(Pleroma.Plugs.IdempotencyPlug) + plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec) end pipeline :mastodon_html do @@ -94,10 +98,12 @@ defmodule Pleroma.Web.Router do pipeline :config do plug(:accepts, ["json", "xml"]) + plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec) end pipeline :pleroma_api do plug(:accepts, ["html", "json"]) + plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec) end pipeline :mailbox_preview do @@ -198,12 +204,18 @@ defmodule Pleroma.Web.Router do get("/config", AdminAPIController, :config_show) post("/config", AdminAPIController, :config_update) get("/config/descriptions", AdminAPIController, :config_descriptions) + get("/need_reboot", AdminAPIController, :need_reboot) get("/restart", AdminAPIController, :restart) get("/moderation_log", AdminAPIController, :list_log) 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) end scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do @@ -333,7 +345,7 @@ defmodule Pleroma.Web.Router do get("/accounts/relationships", AccountController, :relationships) get("/accounts/:id/lists", AccountController, :lists) - get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) + get("/accounts/:id/identity_proofs", AccountController, :identity_proofs) get("/follow_requests", FollowRequestController, :index) get("/blocks", AccountController, :blocks) @@ -347,9 +359,11 @@ defmodule Pleroma.Web.Router do get("/notifications", NotificationController, :index) get("/notifications/:id", NotificationController, :show) + post("/notifications/:id/dismiss", NotificationController, :dismiss) post("/notifications/clear", NotificationController, :clear) - post("/notifications/dismiss", NotificationController, :dismiss) delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple) + # Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead + post("/notifications/dismiss", NotificationController, :dismiss) get("/scheduled_statuses", ScheduledActivityController, :index) get("/scheduled_statuses/:id", ScheduledActivityController, :show) @@ -500,6 +514,12 @@ defmodule Pleroma.Web.Router do ) end + scope "/api" do + pipe_through(:api) + + get("/openapi", OpenApiSpex.Plug.RenderSpec, []) + end + scope "/api", Pleroma.Web, as: :authenticated_twitter_api do pipe_through(:authenticated_api) @@ -658,6 +678,17 @@ defmodule Pleroma.Web.Router do end end + # Test-only routes needed to test action dispatching and plug chain execution + if Pleroma.Config.get(:env) == :test do + scope "/test/authenticated_api", Pleroma.Tests do + pipe_through(:authenticated_api) + + for action <- [:skipped_oauth, :performed_oauth, :missed_oauth] do + get("/#{action}", OAuthTestController, action) + end + end + end + scope "/", Pleroma.Web.MongooseIM do get("/user_exists", MongooseIMController, :user_exists) get("/check_password", MongooseIMController, :check_password) diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex index 66d87620c..b3d1d1ec8 100644 --- a/lib/pleroma/web/static_fe/static_fe_view.ex +++ b/lib/pleroma/web/static_fe/static_fe_view.ex @@ -18,15 +18,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEView do @media_types ["image", "audio", "video"] - def emoji_for_user(%User{} = user) do - user.source_data - |> Map.get("tag", []) - |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) - |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> - {String.trim(name, ":"), url} - end) - end - def fetch_media_type(%{"mediaType" => mediaType}) do Utils.fetch_media_type(@media_types, mediaType) end diff --git a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex index ac8a75009..78350f2aa 100644 --- a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex @@ -2,10 +2,10 @@ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> <id><%= @data["id"] %></id> - <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title> - <content type="html"><%= activity_content(@object) %></content> - <published><%= @data["published"] %></published> - <updated><%= @data["published"] %></updated> + <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> + <content type="html"><%= activity_content(@data) %></content> + <published><%= @activity.data["published"] %></published> + <updated><%= @activity.data["published"] %></updated> <ostatus:conversation ref="<%= activity_context(@activity) %>"> <%= activity_context(@activity) %> </ostatus:conversation> diff --git a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex index a4dbed638..a304a16af 100644 --- a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex @@ -2,10 +2,10 @@ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> <guid><%= @data["id"] %></guid> - <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title> - <description><%= activity_content(@object) %></description> - <pubDate><%= @data["published"] %></pubDate> - <updated><%= @data["published"] %></updated> + <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> + <description><%= activity_content(@data) %></description> + <pubDate><%= @activity.data["published"] %></pubDate> + <updated><%= @activity.data["published"] %></updated> <ostatus:conversation ref="<%= activity_context(@activity) %>"> <%= activity_context(@activity) %> </ostatus:conversation> diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex index da4fa6d6c..cf5874a91 100644 --- a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex @@ -1,12 +1,12 @@ <entry> <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> - + <%= render @view_module, "_tag_author.atom", assigns %> - + <id><%= @data["id"] %></id> - <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title> - <content type="html"><%= activity_content(@object) %></content> + <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> + <content type="html"><%= activity_content(@data) %></content> <%= if @activity.local do %> <link type="application/atom+xml" href='<%= @data["id"] %>' rel="self"/> @@ -15,8 +15,8 @@ <link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/> <% end %> - <published><%= @data["published"] %></published> - <updated><%= @data["published"] %></updated> + <published><%= @activity.data["published"] %></published> + <updated><%= @activity.data["published"] %></updated> <ostatus:conversation ref="<%= activity_context(@activity) %>"> <%= activity_context(@activity) %> @@ -26,7 +26,7 @@ <%= if @data["summary"] do %> <summary><%= @data["summary"] %></summary> <% end %> - + <%= for id <- @activity.recipients do %> <%= if id == Pleroma.Constants.as_public() do %> <link rel="mentioned" @@ -40,7 +40,7 @@ <% end %> <% end %> <% end %> - + <%= for tag <- @data["tag"] || [] do %> <category term="<%= tag %>"></category> <% end %> diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex index 295574df1..2334e24a2 100644 --- a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex +++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex @@ -1,15 +1,14 @@ <item> - <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title> - - + <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> + + <guid isPermalink="true"><%= activity_context(@activity) %></guid> <link><%= activity_context(@activity) %></link> - <pubDate><%= pub_date(@data["published"]) %></pubDate> - - <description><%= activity_content(@object) %></description> + <pubDate><%= pub_date(@activity.data["published"]) %></pubDate> + + <description><%= activity_content(@data) %></description> <%= for attachment <- @data["attachment"] || [] do %> <enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>"/> <% end %> - -</item> +</item> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex index 2a7582d45..56f3a1524 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex @@ -4,7 +4,7 @@ <img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt=""> </div> <span class="display-name"> - <bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi> + <bdi><%= raw Formatter.emojify(@user.name, @user.emoji) %></bdi> <span class="nickname"><%= @user.nickname %></span> </span> </a> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex index e7d2aecad..3191bf450 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex @@ -7,7 +7,7 @@ <input type="hidden" name="profile" value=""> <button type="submit" class="collapse">Remote follow</button> </form> - <%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> | + <%= raw Formatter.emojify(@user.name, @user.emoji) %> | <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %> </h3> <p><%= raw @user.bio %></p> diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index f9c0994da..7a1ba6936 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do def register_user(params, opts \\ []) do token = params["token"] + trusted_app? = params["trusted_app"] params = %{ nickname: params["nickname"], @@ -29,7 +30,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled]) # true if captcha is disabled or enabled and valid, false otherwise captcha_ok = - if not captcha_enabled do + if trusted_app? || not captcha_enabled do :ok else Pleroma.Captcha.validate( diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 0229aea97..31adc2817 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -15,6 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.Controller do plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) + plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) action_fallback(:errors) diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index cf3ac1287..ae7c94640 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -29,11 +29,40 @@ defmodule Pleroma.Web do import Pleroma.Web.Router.Helpers import Pleroma.Web.TranslationHelpers + alias Pleroma.Plugs.PlugHelper + plug(:set_put_layout) defp set_put_layout(conn, _) do put_layout(conn, Pleroma.Config.get(:app_layout, "app.html")) end + + # Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain + defp skip_plug(conn, plug_module) do + try do + plug_module.ensure_skippable() + rescue + UndefinedFunctionError -> + raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code." + end + + PlugHelper.append_to_skipped_plugs(conn, plug_module) + end + + # Here we can apply before-action hooks (e.g. verify whether auth checks were preformed) + defp action(conn, params) do + if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) && + not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do + conn + |> render_error( + :forbidden, + "Security violation: OAuth scopes check was neither handled nor explicitly skipped." + ) + |> halt() + else + super(conn, params) + end + end end end @@ -96,6 +125,26 @@ defmodule Pleroma.Web do end end + def plug do + quote do + alias Pleroma.Plugs.PlugHelper + + def ensure_skippable, do: :noop + + @impl Plug + @doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise." + def call(%Plug.Conn{} = conn, options) do + if PlugHelper.plug_skipped?(conn, __MODULE__) do + conn + else + conn + |> PlugHelper.append_to_called_plugs(__MODULE__) + |> perform(options) + end + end + end + end + @doc """ When used, dispatch to the appropriate controller/view/etc. """ |