From 8371fd8ca20d7aaa16e082fd7ed39d603b9731d1 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sat, 16 Jul 2022 01:20:25 -0400 Subject: [PATCH 1/2] Implement settings api --- .../operations/pleroma_settings_operation.ex | 72 ++++++++++ .../controllers/settings_controller.ex | 79 +++++++++++ lib/pleroma/web/router.ex | 7 + .../controllers/settings_controller_test.exs | 126 ++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_settings_operation.ex create mode 100644 lib/pleroma/web/pleroma_api/controllers/settings_controller.ex create mode 100644 test/pleroma/web/pleroma_api/controllers/settings_controller_test.exs diff --git a/lib/pleroma/web/api_spec/operations/pleroma_settings_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_settings_operation.ex new file mode 100644 index 000000000..e2cef4f67 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_settings_operation.ex @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaSettingsOperation 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 show_operation do + %Operation{ + tags: ["Settings"], + summary: "Get settings for an application", + description: "Get synchronized settings for an application", + operationId: "SettingsController.show", + parameters: [app_name_param()], + security: [%{"oAuth" => ["read:accounts"]}], + responses: %{ + 200 => Operation.response("object", "application/json", object()) + } + } + end + + def update_operation do + %Operation{ + tags: ["Settings"], + summary: "Update settings for an application", + description: "Update synchronized settings for an application", + operationId: "SettingsController.update", + parameters: [app_name_param()], + security: [%{"oAuth" => ["write:accounts"]}], + requestBody: request_body("Parameters", update_request(), required: true), + responses: %{ + 200 => Operation.response("object", "application/json", object()) + } + } + end + + def app_name_param do + Operation.parameter(:app, :path, %Schema{type: :string}, "Application name", + example: "pleroma-fe", + required: true + ) + end + + def object do + %Schema{ + title: "Settings object", + description: "The object that contains settings for the application.", + type: :object + } + end + + def update_request do + %Schema{ + title: "SettingsUpdateRequest", + type: :object, + description: + "The settings object to be merged with the current settings. To remove a field, set it to null.", + example: %{ + "config1" => true, + "config2_to_unset" => nil + } + } + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/settings_controller.ex b/lib/pleroma/web/pleroma_api/controllers/settings_controller.ex new file mode 100644 index 000000000..1136575b6 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/settings_controller.ex @@ -0,0 +1,79 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.SettingsController do + use Pleroma.Web, :controller + + alias Pleroma.Web.Plugs.OAuthScopesPlug + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + OAuthScopesPlug, + %{scopes: ["write:accounts"]} when action in [:update] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["read:accounts"]} when action in [:show] + ) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaSettingsOperation + + @doc "GET /api/v1/pleroma/settings/:app" + def show(%{assigns: %{user: user}} = conn, %{app: app} = _params) do + conn + |> json(get_settings(user, app)) + end + + @doc "PATCH /api/v1/pleroma/settings/:app" + def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{app: app} = _params) do + settings = + get_settings(user, app) + |> merge_recursively(body_params) + + with changeset <- + Pleroma.User.update_changeset( + user, + %{pleroma_settings_store: %{app => settings}} + ), + {:ok, _} <- Pleroma.Repo.update(changeset) do + conn + |> json(settings) + end + end + + defp merge_recursively(old, %{} = new) do + old = ensure_object(old) + + Enum.reduce( + new, + old, + fn + {k, nil}, acc -> + Map.drop(acc, [k]) + + {k, %{} = new_child}, acc -> + Map.put(acc, k, merge_recursively(acc[k], new_child)) + + {k, v}, acc -> + Map.put(acc, k, v) + end + ) + end + + defp get_settings(user, app) do + user.pleroma_settings_store + |> Map.get(app, %{}) + |> ensure_object() + end + + defp ensure_object(%{} = object) do + object + end + + defp ensure_object(_) do + %{} + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 7bbc20275..9023b9800 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -458,6 +458,13 @@ defmodule Pleroma.Web.Router do get("/birthdays", AccountController, :birthdays) end + scope [] do + pipe_through(:authenticated_api) + + get("/settings/:app", SettingsController, :show) + patch("/settings/:app", SettingsController, :update) + end + post("/accounts/confirmation_resend", AccountController, :confirmation_resend) end diff --git a/test/pleroma/web/pleroma_api/controllers/settings_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/settings_controller_test.exs new file mode 100644 index 000000000..e3c752d53 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/settings_controller_test.exs @@ -0,0 +1,126 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.SettingsControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + describe "GET /api/v1/pleroma/settings/:app" do + setup do + oauth_access(["read:accounts"]) + end + + test "it gets empty settings", %{conn: conn} do + response = + conn + |> get("/api/v1/pleroma/settings/pleroma-fe") + |> json_response_and_validate_schema(:ok) + + assert response == %{} + end + + test "it gets settings", %{conn: conn, user: user} do + response = + conn + |> assign( + :user, + struct(user, + pleroma_settings_store: %{ + "pleroma-fe" => %{ + "foo" => "bar" + } + } + ) + ) + |> get("/api/v1/pleroma/settings/pleroma-fe") + |> json_response_and_validate_schema(:ok) + + assert %{"foo" => "bar"} == response + end + end + + describe "POST /api/v1/pleroma/settings/:app" do + setup do + settings = %{ + "foo" => "bar", + "nested" => %{ + "1" => "2" + } + } + + user = + insert( + :user, + %{ + pleroma_settings_store: %{ + "pleroma-fe" => settings + } + } + ) + + %{conn: conn} = oauth_access(["write:accounts"], user: user) + + %{conn: conn, user: user, settings: settings} + end + + test "it adds keys", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/settings/pleroma-fe", %{ + "foo" => "edited", + "bar" => "new", + "nested" => %{"3" => "4"} + }) + |> json_response_and_validate_schema(:ok) + + assert response == %{ + "foo" => "edited", + "bar" => "new", + "nested" => %{ + "1" => "2", + "3" => "4" + } + } + end + + test "it removes keys", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/settings/pleroma-fe", %{ + "foo" => nil, + "bar" => nil, + "nested" => %{ + "1" => nil, + "3" => nil + } + }) + |> json_response_and_validate_schema(:ok) + + assert response == %{ + "nested" => %{} + } + end + + test "it does not override settings for other apps", %{ + conn: conn, + user: user, + settings: settings + } do + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/settings/admin-fe", %{"foo" => "bar"}) + |> json_response_and_validate_schema(:ok) + + user = Pleroma.User.get_by_id(user.id) + + assert user.pleroma_settings_store == %{ + "pleroma-fe" => settings, + "admin-fe" => %{"foo" => "bar"} + } + end + end +end From 8113dd31ee5d4d50c2b864d46242e1c3b6e889be Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sat, 16 Jul 2022 01:27:16 -0400 Subject: [PATCH 2/2] Add api docs for settings endpoint --- docs/development/API/pleroma_api.md | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md index 0d15384b9..09ffba2e6 100644 --- a/docs/development/API/pleroma_api.md +++ b/docs/development/API/pleroma_api.md @@ -695,3 +695,42 @@ Emoji reactions work a lot like favourites do. They make it possible to react to * Authentication: required * Params: none * Response: HTTP 200 on success, 500 on error + +## `/api/v1/pleroma/settings/:app` +### Gets settings for some application +* Method `GET` +* Authentication: `read:accounts` + +* Response: JSON. The settings for that application, or empty object if there is none. +* Example response: +```json +{ + "some key": "some value" +} +``` + +### Updates settings for some application +* Method `PATCH` +* Authentication: `write:accounts` +* Request body: JSON object. The object will be merged recursively with old settings. If some field is set to null, it is removed. +* Example request: +```json +{ + "some key": "some value", + "key to remove": null, + "nested field": { + "some key": "some value", + "key to remove": null + } +} +``` +* Response: JSON. Updated (merged) settings for that application. +* Example response: +```json +{ + "some key": "some value", + "nested field": { + "some key": "some value", + } +} +```