From 5c028b8f92aacb296afbd59130d848883f0c3a10 Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Fri, 17 May 2019 12:20:31 +0545 Subject: user creation admin api will create multiple users --- CHANGELOG.md | 1 + lib/pleroma/web/admin_api/admin_api_controller.ex | 37 ++++++---- lib/pleroma/web/admin_api/views/account_view.ex | 46 +++++++++++++ lib/pleroma/web/router.ex | 2 +- test/web/admin_api/admin_api_controller_test.exs | 83 +++++++++++++++++++++-- 5 files changed, 149 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e849285..5ee853c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work. - Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats. - Admin API: Move the user related API to `api/pleroma/admin/users` +- Admin API: `POST /api/pleroma/admin/users` will take list of users - Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change - Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications` - Mastodon API: Add `languages` and `registrations` to `/api/v1/instance` diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index e00b33aba..6048ed35b 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -46,24 +46,31 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> json("ok") end - def user_create( - conn, - %{"nickname" => nickname, "email" => email, "password" => password} - ) do - user_data = %{ - nickname: nickname, - name: nickname, - email: email, - password: password, - password_confirmation: password, - bio: "." - } + def users_create(conn, %{"users" => users}) do + result = + Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} -> + user_data = %{ + nickname: nickname, + name: nickname, + email: email, + password: password, + password_confirmation: password, + bio: "." + } + + changeset = User.register_changeset(%User{}, user_data, need_confirmation: false) + + case User.register(changeset) do + {:ok, user} -> + AccountView.render("created.json", %{user: user}) - changeset = User.register_changeset(%User{}, user_data, need_confirmation: false) - {:ok, user} = User.register(changeset) + {:error, changeset} -> + AccountView.render("create-error.json", %{changeset: changeset}) + end + end) conn - |> json(user.nickname) + |> json(result) end def user_show(conn, %{"nickname" => nickname}) do diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 28bb667d8..e1825c5f1 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -44,4 +44,50 @@ defmodule Pleroma.Web.AdminAPI.AccountView do invites: render_many(invites, AccountView, "invite.json", as: :invite) } end + + def render("created.json", %{user: user}) do + %{ + type: "success", + code: 201, + data: %{ + nickname: user.nickname, + email: user.email + } + } + end + + def render("create-error.json", %{changeset: %Ecto.Changeset{changes: changes, errors: errors}}) do + %{ + type: "error", + code: 409, + error: parse_error(errors), + data: %{ + nickname: Map.get(changes, :nickname), + email: Map.get(changes, :email) + } + } + end + + defp parse_error([]), do: "" + + defp parse_error(errors) do + ## when nickname is duplicate ap_id constraint error is raised + nickname_error = Keyword.get(errors, :nickname) || Keyword.get(errors, :ap_id) + email_error = Keyword.get(errors, :email) + password_error = Keyword.get(errors, :password) + + cond do + nickname_error -> + "nickname #{elem(nickname_error, 0)}" + + email_error -> + "email #{elem(email_error, 0)}" + + password_error -> + "password #{elem(password_error, 0)}" + + true -> + "" + end + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 7fef82f82..bbc2fda9b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -156,7 +156,7 @@ defmodule Pleroma.Web.Router do post("/user", AdminAPIController, :user_create) delete("/users", AdminAPIController, :user_delete) - post("/users", AdminAPIController, :user_create) + post("/users", AdminAPIController, :users_create) patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) put("/users/tag", AdminAPIController, :tag_users) delete("/users/tag", AdminAPIController, :untag_users) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 6c1897b5a..a0c9fd56f 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -31,12 +31,87 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do |> assign(:user, admin) |> put_req_header("accept", "application/json") |> post("/api/pleroma/admin/users", %{ - "nickname" => "lain", - "email" => "lain@example.org", - "password" => "test" + "users" => [ + %{ + "nickname" => "lain", + "email" => "lain@example.org", + "password" => "test" + } + ] }) - assert json_response(conn, 200) == "lain" + assert json_response(conn, 200) == [ + %{ + "code" => 201, + "data" => %{ + "email" => "lain@example.org", + "nickname" => "lain" + }, + "type" => "success" + } + ] + end + + test "Cannot create user with exisiting email" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user) + + conn = + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "lain", + "email" => user.email, + "password" => "test" + } + ] + }) + + assert json_response(conn, 200) == [ + %{ + "code" => 409, + "data" => %{ + "email" => user.email, + "nickname" => "lain" + }, + "error" => "email has already been taken", + "type" => "error" + } + ] + end + + test "Cannot create user with exisiting nickname" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user) + + conn = + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => user.nickname, + "email" => "someuser@plerama.social", + "password" => "test" + } + ] + }) + + assert json_response(conn, 200) == [ + %{ + "code" => 409, + "data" => %{ + "email" => "someuser@plerama.social", + "nickname" => user.nickname + }, + "error" => "nickname has already been taken", + "type" => "error" + } + ] end end -- cgit v1.2.3 From 5534d4c67675901ab272ee47355ad43dfae99033 Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Sat, 1 Jun 2019 11:17:53 +0545 Subject: make bulk user creation from admin works as a transaction --- lib/pleroma/user.ex | 8 ++- lib/pleroma/web/admin_api/admin_api_controller.ex | 43 +++++++++++---- lib/pleroma/web/admin_api/views/account_view.ex | 2 +- test/web/admin_api/admin_api_controller_test.exs | 66 +++++++++++++++++++++-- 4 files changed, 103 insertions(+), 16 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index c6a562a61..722e8ff6b 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -276,7 +276,13 @@ defmodule Pleroma.User do @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)" def register(%Ecto.Changeset{} = changeset) do with {:ok, user} <- Repo.insert(changeset), - {:ok, user} <- autofollow_users(user), + {:ok, user} <- post_register_action(user) do + {:ok, user} + end + end + + def post_register_action(%User{} = user) do + with {:ok, user} <- autofollow_users(user), {:ok, user} <- set_cache(user), {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user), {:ok, _} <- try_send_confirmation_email(user) do diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 6048ed35b..60fd4e571 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -47,7 +47,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def users_create(conn, %{"users" => users}) do - result = + changesets = Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} -> user_data = %{ nickname: nickname, @@ -58,19 +58,40 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do bio: "." } - changeset = User.register_changeset(%User{}, user_data, need_confirmation: false) + User.register_changeset(%User{}, user_data, need_confirmation: false) + end) + |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi -> + Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset) + end) - case User.register(changeset) do - {:ok, user} -> - AccountView.render("created.json", %{user: user}) + case Pleroma.Repo.transaction(changesets) do + {:ok, users} -> + res = + users + |> Map.values() + |> Enum.map(fn user -> + {:ok, user} = User.post_register_action(user) + user + end) + |> Enum.map(&AccountView.render("created.json", %{user: &1})) - {:error, changeset} -> - AccountView.render("create-error.json", %{changeset: changeset}) - end - end) + conn + |> json(res) - conn - |> json(result) + {:error, id, changeset, _} -> + res = + Enum.map(changesets.operations, fn + {current_id, {:changeset, _current_changeset, _}} when current_id == id -> + AccountView.render("create-error.json", %{changeset: changeset}) + + {_, {:changeset, current_changeset, _}} -> + AccountView.render("create-error.json", %{changeset: current_changeset}) + end) + + conn + |> put_status(:conflict) + |> json(res) + end end def user_show(conn, %{"nickname" => nickname}) do diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index e1825c5f1..cccdeff7e 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -48,7 +48,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do def render("created.json", %{user: user}) do %{ type: "success", - code: 201, + code: 200, data: %{ nickname: user.nickname, email: user.email diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index a0c9fd56f..019905137 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -36,18 +36,31 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "nickname" => "lain", "email" => "lain@example.org", "password" => "test" + }, + %{ + "nickname" => "lain2", + "email" => "lain2@example.org", + "password" => "test" } ] }) assert json_response(conn, 200) == [ %{ - "code" => 201, + "code" => 200, "data" => %{ "email" => "lain@example.org", "nickname" => "lain" }, "type" => "success" + }, + %{ + "code" => 200, + "data" => %{ + "email" => "lain2@example.org", + "nickname" => "lain2" + }, + "type" => "success" } ] end @@ -70,7 +83,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do ] }) - assert json_response(conn, 200) == [ + assert json_response(conn, 409) == [ %{ "code" => 409, "data" => %{ @@ -101,7 +114,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do ] }) - assert json_response(conn, 200) == [ + assert json_response(conn, 409) == [ %{ "code" => 409, "data" => %{ @@ -113,6 +126,53 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do } ] end + + test "Multiple user creation works in transaction" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user) + + conn = + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "newuser", + "email" => "newuser@pleroma.social", + "password" => "test" + }, + %{ + "nickname" => "lain", + "email" => user.email, + "password" => "test" + } + ] + }) + + assert json_response(conn, 409) == [ + %{ + "code" => 409, + "data" => %{ + "email" => user.email, + "nickname" => "lain" + }, + "error" => "email has already been taken", + "type" => "error" + }, + %{ + "code" => 409, + "data" => %{ + "email" => "newuser@pleroma.social", + "nickname" => "newuser" + }, + "error" => "", + "type" => "error" + } + ] + + assert User.get_by_nickname("newuser") === nil + end end describe "/api/pleroma/admin/users/:nickname" do -- cgit v1.2.3 From e394fc2eefdd7a4c7edd5fb3c04b445215d4a86c Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Sun, 2 Jun 2019 09:48:45 +0545 Subject: fix the flaky test for users creation by admin --- test/web/admin_api/admin_api_controller_test.exs | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 9721a4034..86b160246 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -47,24 +47,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do ] }) - assert json_response(conn, 200) == [ - %{ - "code" => 200, - "data" => %{ - "email" => "lain@example.org", - "nickname" => "lain" - }, - "type" => "success" - }, - %{ - "code" => 200, - "data" => %{ - "email" => "lain2@example.org", - "nickname" => "lain2" - }, - "type" => "success" - } - ] + response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type")) + assert response == ["success", "success"] end test "Cannot create user with exisiting email" do -- cgit v1.2.3 From 8ba7a151adf77c5cc47d6e1364a6078cc4bdef98 Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Mon, 22 Jul 2019 16:45:54 +0200 Subject: Cleanup: fix a comment --- test/web/mastodon_api/mastodon_api_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index ce2e44499..b5279412f 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -405,7 +405,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"visibility" => "direct"} = status assert status["url"] != direct.data["id"] - # User should be able to see his own direct message + # User should be able to see their own direct message res_conn = build_conn() |> assign(:user, user_one) -- cgit v1.2.3 From b72940277470c67802b979e4cab44f277e8fffb3 Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Mon, 22 Jul 2019 09:10:30 +0200 Subject: Make test.exs read config in the same way as dev.exs This way, if your test.secret.exs has an error, you'll actually see it. --- config/test.exs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/config/test.exs b/config/test.exs index 92dca18bc..3f606aa81 100644 --- a/config/test.exs +++ b/config/test.exs @@ -82,11 +82,10 @@ IO.puts("RUM enabled: #{rum_enabled}") config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock -try do +if File.exists?("./config/test.secret.exs") do import_config "test.secret.exs" -rescue - _ -> - IO.puts( - "You may want to create test.secret.exs to declare custom database connection parameters." - ) +else + IO.puts( + "You may want to create test.secret.exs to declare custom database connection parameters." + ) end -- cgit v1.2.3 From 666514194a325e2463c05bae516b89d7c5f59316 Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Mon, 22 Jul 2019 14:16:20 +0200 Subject: Add activity expirations table Add a table to store activity expirations. An activity can have zero or one expirations. The expiration has a scheduled_at field which stores the time at which the activity should expire and be deleted. --- lib/pleroma/activity.ex | 3 +++ lib/pleroma/activity_expiration.ex | 31 ++++++++++++++++++++++ .../20190716100804_add_expirations_table.exs | 10 +++++++ test/activity_expiration_test.exs | 21 +++++++++++++++ test/activity_test.exs | 9 +++++++ test/support/factory.ex | 19 ++++++++++++- 6 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 lib/pleroma/activity_expiration.ex create mode 100644 priv/repo/migrations/20190716100804_add_expirations_table.exs create mode 100644 test/activity_expiration_test.exs diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 46552c7be..be4850560 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Activity do use Ecto.Schema alias Pleroma.Activity + alias Pleroma.ActivityExpiration alias Pleroma.Bookmark alias Pleroma.Notification alias Pleroma.Object @@ -59,6 +60,8 @@ defmodule Pleroma.Activity do # typical case. has_one(:object, Object, on_delete: :nothing, foreign_key: :id) + has_one(:expiration, ActivityExpiration, on_delete: :delete_all) + timestamps() end diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex new file mode 100644 index 000000000..d3d95f9e9 --- /dev/null +++ b/lib/pleroma/activity_expiration.ex @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityExpiration do + use Ecto.Schema + + alias Pleroma.Activity + alias Pleroma.ActivityExpiration + alias Pleroma.FlakeId + alias Pleroma.Repo + + import Ecto.Query + + @type t :: %__MODULE__{} + + schema "activity_expirations" do + belongs_to(:activity, Activity, type: FlakeId) + field(:scheduled_at, :naive_datetime) + end + + def due_expirations(offset \\ 0) do + naive_datetime = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(offset, :millisecond) + + ActivityExpiration + |> where([exp], exp.scheduled_at < ^naive_datetime) + |> Repo.all() + end +end diff --git a/priv/repo/migrations/20190716100804_add_expirations_table.exs b/priv/repo/migrations/20190716100804_add_expirations_table.exs new file mode 100644 index 000000000..fbde8f9d6 --- /dev/null +++ b/priv/repo/migrations/20190716100804_add_expirations_table.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.AddExpirationsTable do + use Ecto.Migration + + def change do + create_if_not_exists table(:activity_expirations) do + add(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all)) + add(:scheduled_at, :naive_datetime, null: false) + end + end +end diff --git a/test/activity_expiration_test.exs b/test/activity_expiration_test.exs new file mode 100644 index 000000000..20566a186 --- /dev/null +++ b/test/activity_expiration_test.exs @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityExpirationTest do + use Pleroma.DataCase + alias Pleroma.ActivityExpiration + import Pleroma.Factory + + test "finds activities due to be deleted only" do + activity = insert(:note_activity) + expiration_due = insert(:expiration_in_the_past, %{activity_id: activity.id}) + activity2 = insert(:note_activity) + insert(:expiration_in_the_future, %{activity_id: activity2.id}) + + expirations = ActivityExpiration.due_expirations() + + assert length(expirations) == 1 + assert hd(expirations) == expiration_due + end +end diff --git a/test/activity_test.exs b/test/activity_test.exs index b27f6fd36..785c4b3cf 100644 --- a/test/activity_test.exs +++ b/test/activity_test.exs @@ -164,4 +164,13 @@ defmodule Pleroma.ActivityTest do Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) end end + + test "add an activity with an expiration" do + activity = insert(:note_activity) + insert(:expiration_in_the_future, %{activity_id: activity.id}) + + Pleroma.ActivityExpiration + |> where([a], a.activity_id == ^activity.id) + |> Repo.one!() + end end diff --git a/test/support/factory.ex b/test/support/factory.ex index c751546ce..7b52b1328 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors +# Copyright © 2017-2019 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Factory do @@ -142,6 +142,23 @@ defmodule Pleroma.Factory do |> Map.merge(attrs) end + defp expiration_offset_by_minutes(attrs, minutes) do + %Pleroma.ActivityExpiration{} + |> Map.merge(attrs) + |> Map.put( + :scheduled_at, + NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(minutes), :millisecond) + ) + end + + def expiration_in_the_past_factory(attrs \\ %{}) do + expiration_offset_by_minutes(attrs, -60) + end + + def expiration_in_the_future_factory(attrs \\ %{}) do + expiration_offset_by_minutes(attrs, 60) + end + def article_activity_factory do article = insert(:article) -- cgit v1.2.3 From 378f5f0fbe21c2533719fed9afe8313586fda5d5 Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Mon, 22 Jul 2019 14:18:58 +0200 Subject: Add activity expiration worker This is a worker that runs every minute and deletes expired activities. It's based heavily on the scheduled activities worker. --- config/config.exs | 3 ++ docs/config.md | 4 ++ lib/pleroma/activity_expiration_worker.ex | 62 +++++++++++++++++++++++++++++++ lib/pleroma/application.ex | 4 ++ test/activity_expiration_worker_test.exs | 17 +++++++++ 5 files changed, 90 insertions(+) create mode 100644 lib/pleroma/activity_expiration_worker.ex create mode 100644 test/activity_expiration_worker_test.exs diff --git a/config/config.exs b/config/config.exs index 569411866..2887353fb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -447,6 +447,7 @@ config :pleroma, Pleroma.Web.Federator.RetryQueue, max_retries: 5 config :pleroma_job_queue, :queues, + activity_expiration: 10, federator_incoming: 50, federator_outgoing: 50, web_push: 50, @@ -536,6 +537,8 @@ config :pleroma, :rate_limit, status_id_action: {60_000, 3}, password_reset: {1_800_000, 5} +config :pleroma, Pleroma.ActivityExpiration, enabled: true + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/docs/config.md b/docs/config.md index 02f86dc16..a20ed704f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -484,6 +484,10 @@ config :auto_linker, * `total_user_limit`: the number of scheduled activities a user is allowed to create in total (Default: `300`) * `enabled`: whether scheduled activities are sent to the job queue to be executed +## Pleroma.ActivityExpiration + +# `enabled`: whether expired activities will be sent to the job queue to be deleted + ## Pleroma.Web.Auth.Authenticator * `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator diff --git a/lib/pleroma/activity_expiration_worker.ex b/lib/pleroma/activity_expiration_worker.ex new file mode 100644 index 000000000..a341f58df --- /dev/null +++ b/lib/pleroma/activity_expiration_worker.ex @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityExpirationWorker do + alias Pleroma.Activity + alias Pleroma.ActivityExpiration + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + require Logger + use GenServer + import Ecto.Query + + @schedule_interval :timer.minutes(1) + + def start_link do + GenServer.start_link(__MODULE__, nil) + end + + @impl true + def init(_) do + if Config.get([ActivityExpiration, :enabled]) do + schedule_next() + {:ok, nil} + else + :ignore + end + end + + def perform(:execute, expiration_id) do + try do + expiration = + ActivityExpiration + |> where([e], e.id == ^expiration_id) + |> Repo.one!() + + activity = Activity.get_by_id_with_object(expiration.activity_id) + user = User.get_by_ap_id(activity.object.data["actor"]) + CommonAPI.delete(activity.id, user) + rescue + error -> + Logger.error("#{__MODULE__} Couldn't delete expired activity: #{inspect(error)}") + end + end + + @impl true + def handle_info(:perform, state) do + ActivityExpiration.due_expirations(@schedule_interval) + |> Enum.each(fn expiration -> + PleromaJobQueue.enqueue(:activity_expiration, __MODULE__, [:execute, expiration.id]) + end) + + schedule_next() + {:noreply, state} + end + + defp schedule_next do + Process.send_after(self(), :perform, @schedule_interval) + end +end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 035331491..42e4a1dfa 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -115,6 +115,10 @@ defmodule Pleroma.Application do %{ id: Pleroma.ScheduledActivityWorker, start: {Pleroma.ScheduledActivityWorker, :start_link, []} + }, + %{ + id: Pleroma.ActivityExpirationWorker, + start: {Pleroma.ActivityExpirationWorker, :start_link, []} } ] ++ hackney_pool_children() ++ diff --git a/test/activity_expiration_worker_test.exs b/test/activity_expiration_worker_test.exs new file mode 100644 index 000000000..939d912f1 --- /dev/null +++ b/test/activity_expiration_worker_test.exs @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityExpirationWorkerTest do + use Pleroma.DataCase + alias Pleroma.Activity + import Pleroma.Factory + + test "deletes an activity" do + activity = insert(:note_activity) + expiration = insert(:expiration_in_the_past, %{activity_id: activity.id}) + Pleroma.ActivityExpirationWorker.perform(:execute, expiration.id) + + refute Repo.get(Activity, activity.id) + end +end -- cgit v1.2.3 From 704960b3c135d2e050308c68f5ccf5d7b7df40f8 Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Mon, 22 Jul 2019 16:46:20 +0200 Subject: Add support for activity expiration to common and Masto API The "expires_at" parameter accepts an ISO8601-formatted date which defines when the activity will expire. At this point the API will not give you any feedback about if your post will expire or not. --- docs/api/differences_in_mastoapi_responses.md | 1 + lib/pleroma/activity_expiration.ex | 19 ++++++++++++++ lib/pleroma/web/common_api/common_api.ex | 29 +++++++++++++++------- test/support/factory.ex | 10 +++++--- test/web/common_api/common_api_test.exs | 17 +++++++++++++ .../mastodon_api/mastodon_api_controller_test.exs | 19 ++++++++++++++ 6 files changed, 82 insertions(+), 13 deletions(-) diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 1907d70c8..7d5be4713 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -79,6 +79,7 @@ Additional parameters can be added to the JSON body/Form data: - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint. - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply. - `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`. +- `expires_on`: datetime (iso8601), sets when the posted activity should expire. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. ## PATCH `/api/v1/update_credentials` diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex index d3d95f9e9..a0af5255b 100644 --- a/lib/pleroma/activity_expiration.ex +++ b/lib/pleroma/activity_expiration.ex @@ -10,6 +10,7 @@ defmodule Pleroma.ActivityExpiration do alias Pleroma.FlakeId alias Pleroma.Repo + import Ecto.Changeset import Ecto.Query @type t :: %__MODULE__{} @@ -19,6 +20,24 @@ defmodule Pleroma.ActivityExpiration do field(:scheduled_at, :naive_datetime) end + def changeset(%ActivityExpiration{} = expiration, attrs) do + expiration + |> cast(attrs, [:scheduled_at]) + |> validate_required([:scheduled_at]) + end + + def get_by_activity_id(activity_id) do + ActivityExpiration + |> where([exp], exp.activity_id == ^activity_id) + |> Repo.one() + end + + def create(%Activity{} = activity, scheduled_at) do + %ActivityExpiration{activity_id: activity.id} + |> changeset(%{scheduled_at: scheduled_at}) + |> Repo.insert() + end + def due_expirations(offset \\ 0) do naive_datetime = NaiveDateTime.utc_now() diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 44af6a773..0f287af4e 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity + alias Pleroma.ActivityExpiration alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.ThreadMute @@ -218,6 +219,7 @@ defmodule Pleroma.Web.CommonAPI do context <- make_context(in_reply_to), cw <- data["spoiler_text"] || "", sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), + {:ok, expires_at} <- Ecto.Type.cast(:naive_datetime, data["expires_at"]), full_payload <- String.trim(status <> cw), :ok <- validate_character_limit(full_payload, attachments, limit), object <- @@ -243,15 +245,24 @@ defmodule Pleroma.Web.CommonAPI do preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false direct? = visibility == "direct" - %{ - to: to, - actor: user, - context: context, - object: object, - additional: %{"cc" => cc, "directMessage" => direct?} - } - |> maybe_add_list_data(user, visibility) - |> ActivityPub.create(preview?) + result = + %{ + to: to, + actor: user, + context: context, + object: object, + additional: %{"cc" => cc, "directMessage" => direct?} + } + |> maybe_add_list_data(user, visibility) + |> ActivityPub.create(preview?) + + if expires_at do + with {:ok, activity} <- result do + ActivityExpiration.create(activity, expires_at) + end + end + + result else {:private_to_public, true} -> {:error, dgettext("errors", "The message visibility must be direct")} diff --git a/test/support/factory.ex b/test/support/factory.ex index 7b52b1328..63fe3a66d 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -143,12 +143,14 @@ defmodule Pleroma.Factory do end defp expiration_offset_by_minutes(attrs, minutes) do + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(minutes), :millisecond) + |> NaiveDateTime.truncate(:second) + %Pleroma.ActivityExpiration{} |> Map.merge(attrs) - |> Map.put( - :scheduled_at, - NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(minutes), :millisecond) - ) + |> Map.put(:scheduled_at, scheduled_at) end def expiration_in_the_past_factory(attrs \\ %{}) do diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 16b3f121d..210314a4a 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -160,6 +160,23 @@ defmodule Pleroma.Web.CommonAPITest do Pleroma.Config.put([:instance, :limit], limit) end + + test "it can handle activities that expire" do + user = insert(:user) + + expires_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.truncate(:second) + |> NaiveDateTime.add(1_000_000, :second) + + expires_at_iso8601 = expires_at |> NaiveDateTime.to_iso8601() + + assert {:ok, activity} = + CommonAPI.post(user, %{"status" => "chai", "expires_at" => expires_at_iso8601}) + + assert expiration = Pleroma.ActivityExpiration.get_by_activity_id(activity.id) + assert expiration.scheduled_at == expires_at + end end describe "reactions" do diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index b5279412f..24482a4a2 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do alias Ecto.Changeset alias Pleroma.Activity + alias Pleroma.ActivityExpiration alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -151,6 +152,24 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"id" => third_id} = json_response(conn_three, 200) refute id == third_id + + # An activity that will expire: + expires_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(120), :millisecond) + |> NaiveDateTime.truncate(:second) + + conn_four = + conn + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_at" => expires_at + }) + + assert %{"id" => fourth_id} = json_response(conn_four, 200) + assert activity = Activity.get_by_id(fourth_id) + assert expiration = ActivityExpiration.get_by_activity_id(fourth_id) + assert expiration.scheduled_at == expires_at end test "replying to a status", %{conn: conn} do -- cgit v1.2.3 From 36012ef6c1dfea2489e61063e14783fa3fb52700 Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Tue, 23 Jul 2019 16:33:45 +0200 Subject: Require that ephemeral posts live for at least one hour If we didn't put some kind of lifetime requirement on these, I guess you could annoy people by sending large numbers of ephemeral posts that provoke notifications but then disappear before anyone can read them. --- lib/pleroma/activity_expiration.ex | 18 ++++++++++++++++++ lib/pleroma/web/common_api/common_api.ex | 14 ++++++++++++-- test/activity_expiration_test.exs | 6 ++++++ test/support/factory.ex | 2 +- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex index a0af5255b..bf57abca4 100644 --- a/lib/pleroma/activity_expiration.ex +++ b/lib/pleroma/activity_expiration.ex @@ -14,6 +14,7 @@ defmodule Pleroma.ActivityExpiration do import Ecto.Query @type t :: %__MODULE__{} + @min_activity_lifetime :timer.hours(1) schema "activity_expirations" do belongs_to(:activity, Activity, type: FlakeId) @@ -24,6 +25,7 @@ defmodule Pleroma.ActivityExpiration do expiration |> cast(attrs, [:scheduled_at]) |> validate_required([:scheduled_at]) + |> validate_scheduled_at() end def get_by_activity_id(activity_id) do @@ -47,4 +49,20 @@ defmodule Pleroma.ActivityExpiration do |> where([exp], exp.scheduled_at < ^naive_datetime) |> Repo.all() end + + def validate_scheduled_at(changeset) do + validate_change(changeset, :scheduled_at, fn _, scheduled_at -> + if not expires_late_enough?(scheduled_at) do + [scheduled_at: "an ephemeral activity must live for at least one hour"] + else + [] + end + end) + end + + def expires_late_enough?(scheduled_at) do + now = NaiveDateTime.utc_now() + diff = NaiveDateTime.diff(scheduled_at, now, :millisecond) + diff >= @min_activity_lifetime + end end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 0f287af4e..261d60392 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -196,6 +196,16 @@ defmodule Pleroma.Web.CommonAPI do end end + defp check_expiry_date(expiry_str) do + {:ok, expiry} = Ecto.Type.cast(:naive_datetime, expiry_str) + + if is_nil(expiry) || ActivityExpiration.expires_late_enough?(expiry) do + {:ok, expiry} + else + {:error, "Expiry date is too soon"} + end + end + def post(user, %{"status" => status} = data) do limit = Pleroma.Config.get([:instance, :limit]) @@ -219,7 +229,7 @@ defmodule Pleroma.Web.CommonAPI do context <- make_context(in_reply_to), cw <- data["spoiler_text"] || "", sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), - {:ok, expires_at} <- Ecto.Type.cast(:naive_datetime, data["expires_at"]), + {:ok, expires_at} <- check_expiry_date(data["expires_at"]), full_payload <- String.trim(status <> cw), :ok <- validate_character_limit(full_payload, attachments, limit), object <- @@ -258,7 +268,7 @@ defmodule Pleroma.Web.CommonAPI do if expires_at do with {:ok, activity} <- result do - ActivityExpiration.create(activity, expires_at) + {:ok, _} = ActivityExpiration.create(activity, expires_at) end end diff --git a/test/activity_expiration_test.exs b/test/activity_expiration_test.exs index 20566a186..4948fae16 100644 --- a/test/activity_expiration_test.exs +++ b/test/activity_expiration_test.exs @@ -18,4 +18,10 @@ defmodule Pleroma.ActivityExpirationTest do assert length(expirations) == 1 assert hd(expirations) == expiration_due end + + test "denies expirations that don't live long enough" do + activity = insert(:note_activity) + now = NaiveDateTime.utc_now() + assert {:error, _} = ActivityExpiration.create(activity, now) + end end diff --git a/test/support/factory.ex b/test/support/factory.ex index 63fe3a66d..7a2ddcada 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -158,7 +158,7 @@ defmodule Pleroma.Factory do end def expiration_in_the_future_factory(attrs \\ %{}) do - expiration_offset_by_minutes(attrs, 60) + expiration_offset_by_minutes(attrs, 61) end def article_activity_factory do -- cgit v1.2.3 From 3cb471ec0688b81c8ef37dd27f2b82e6c858431f Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Wed, 24 Jul 2019 12:43:20 +0200 Subject: Expose expires_at datetime in mastoAPI only for the activity actor In the "pleroma" section of the MastoAPI for status activities you can see an expires_at item that states when the activity will expire, or nothing if the activity will not expire. The expires_at date is only visible to the person who posted the activity. This is the conservative approach in case some attacker decides to write a logger for expiring posts. However, in the future of OCAP, signed requests, and all that stuff, this attack might not be that likely. Some other pleroma dev should remove the restriction in the code at that time, if they're satisfied with the security implications of doing so. --- docs/api/differences_in_mastoapi_responses.md | 1 + lib/pleroma/web/mastodon_api/views/status_view.ex | 13 ++++++++++++- test/web/mastodon_api/mastodon_api_controller_test.exs | 3 ++- test/web/mastodon_api/status_view_test.exs | 3 ++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 7d5be4713..168a13f4e 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -25,6 +25,7 @@ Has these additional fields under the `pleroma` object: - `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any) - `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` +- `expires_on`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire ## Attachments diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index de9425959..7264dcafb 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do use Pleroma.Web, :view alias Pleroma.Activity + alias Pleroma.ActivityExpiration alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Repo @@ -165,6 +166,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil + client_posted_this_activity = opts[:for] && user.id == opts[:for].id + + expires_at = + with true <- client_posted_this_activity, + expiration when not is_nil(expiration) <- + ActivityExpiration.get_by_activity_id(activity.id) do + expiration.scheduled_at + end + thread_muted? = case activity.thread_muted? do thread_muted? when is_boolean(thread_muted?) -> thread_muted? @@ -262,7 +272,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do conversation_id: get_context_id(activity), in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, content: %{"text/plain" => content_plaintext}, - spoiler_text: %{"text/plain" => summary_plaintext} + spoiler_text: %{"text/plain" => summary_plaintext}, + expires_at: expires_at } } end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 24482a4a2..e59908979 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -166,10 +166,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do "expires_at" => expires_at }) - assert %{"id" => fourth_id} = json_response(conn_four, 200) + assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200) assert activity = Activity.get_by_id(fourth_id) assert expiration = ActivityExpiration.get_by_activity_id(fourth_id) assert expiration.scheduled_at == expires_at + assert fourth_response["pleroma"]["expires_at"] == NaiveDateTime.to_iso8601(expires_at) end test "replying to a status", %{conn: conn} do diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index 3447c5b1f..073c69659 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -133,7 +133,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do conversation_id: convo_id, in_reply_to_account_acct: nil, content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])}, - spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])} + spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}, + expires_at: nil } } -- cgit v1.2.3 From 91d9fdc7decc664483625c11e44d4e053dd9c585 Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Wed, 24 Jul 2019 13:02:28 +0200 Subject: Update changelog to document expiring posts feature --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a5a6c21..75d236af5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Expiring/ephemeral activites. All activities can have expires_on value set, which controls when they should be deleted automatically. +- Mastodon API: in post_status, expires_at datetime parameter lets you set when an activity should expire +- Mastodon API: all status JSON responses contain a `pleroma.expires_in` item which states the number of minutes until an activity expires. The value is only shown to the user who created the activity. To everyone else it's empty. +- Configuration: `ActivityExpiration.enabled` controls whether expired activites will get deleted at the appropriate time. Enabled by default. + ### Changed - **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config - Configuration: OpenGraph and TwitterCard providers enabled by default -- cgit v1.2.3 From 2981821db834448bf9b2ba26590314e36201664c Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Wed, 24 Jul 2019 16:51:09 +0200 Subject: squash! Expose expires_at datetime in mastoAPI only for the activity actor NOTE: rewrite the commit msg --- docs/api/differences_in_mastoapi_responses.md | 2 +- lib/pleroma/web/mastodon_api/views/status_view.ex | 10 +++++++--- test/web/mastodon_api/mastodon_api_controller_test.exs | 2 +- test/web/mastodon_api/status_view_test.exs | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 168a13f4e..829468b13 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -25,7 +25,7 @@ Has these additional fields under the `pleroma` object: - `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any) - `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` -- `expires_on`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire +- `expires_in`: the number of minutes until a post will expire (be deleted automatically), or empty if the post won't expire ## Attachments diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 7264dcafb..4a3686d72 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -168,11 +168,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do client_posted_this_activity = opts[:for] && user.id == opts[:for].id - expires_at = + expires_in = with true <- client_posted_this_activity, expiration when not is_nil(expiration) <- ActivityExpiration.get_by_activity_id(activity.id) do - expiration.scheduled_at + expires_in_seconds = + expiration.scheduled_at + |> NaiveDateTime.diff(NaiveDateTime.utc_now(), :second) + + round(expires_in_seconds / 60) end thread_muted? = @@ -273,7 +277,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary_plaintext}, - expires_at: expires_at + expires_in: expires_in } } end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index e59908979..a9d38c06e 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -170,7 +170,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert activity = Activity.get_by_id(fourth_id) assert expiration = ActivityExpiration.get_by_activity_id(fourth_id) assert expiration.scheduled_at == expires_at - assert fourth_response["pleroma"]["expires_at"] == NaiveDateTime.to_iso8601(expires_at) + assert fourth_response["pleroma"]["expires_in"] > 0 end test "replying to a status", %{conn: conn} do diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index 073c69659..eb0874ab2 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -134,7 +134,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do in_reply_to_account_acct: nil, content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])}, spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}, - expires_at: nil + expires_in: nil } } -- cgit v1.2.3 From 877575d0da830724e822eac2de243391aaea7ec8 Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Wed, 24 Jul 2019 17:07:51 +0200 Subject: fixup! Update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d236af5..f64506637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added - Expiring/ephemeral activites. All activities can have expires_on value set, which controls when they should be deleted automatically. -- Mastodon API: in post_status, expires_at datetime parameter lets you set when an activity should expire -- Mastodon API: all status JSON responses contain a `pleroma.expires_in` item which states the number of minutes until an activity expires. The value is only shown to the user who created the activity. To everyone else it's empty. +- Mastodon API: in post_status, the expires_in parameter lets you set the number of minutes until an activity expires. It must be at least one hour. +- Mastodon API: all status JSON responses contain a `pleroma.expires_on` item which states when an activity will expire. The value is only shown to the user who created the activity. To everyone else it's empty. - Configuration: `ActivityExpiration.enabled` controls whether expired activites will get deleted at the appropriate time. Enabled by default. ### Changed -- cgit v1.2.3 From 2c83eb0b157b2f574f55341e9171f0b5ab7bd3b2 Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Wed, 24 Jul 2019 17:09:59 +0200 Subject: Revert "squash! Expose expires_at datetime in mastoAPI only for the activity actor" This reverts commit 2981821db834448bf9b2ba26590314e36201664c. --- docs/api/differences_in_mastoapi_responses.md | 2 +- lib/pleroma/web/mastodon_api/views/status_view.ex | 10 +++------- test/web/mastodon_api/mastodon_api_controller_test.exs | 2 +- test/web/mastodon_api/status_view_test.exs | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 829468b13..168a13f4e 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -25,7 +25,7 @@ Has these additional fields under the `pleroma` object: - `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any) - `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` -- `expires_in`: the number of minutes until a post will expire (be deleted automatically), or empty if the post won't expire +- `expires_on`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire ## Attachments diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 4a3686d72..7264dcafb 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -168,15 +168,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do client_posted_this_activity = opts[:for] && user.id == opts[:for].id - expires_in = + expires_at = with true <- client_posted_this_activity, expiration when not is_nil(expiration) <- ActivityExpiration.get_by_activity_id(activity.id) do - expires_in_seconds = - expiration.scheduled_at - |> NaiveDateTime.diff(NaiveDateTime.utc_now(), :second) - - round(expires_in_seconds / 60) + expiration.scheduled_at end thread_muted? = @@ -277,7 +273,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary_plaintext}, - expires_in: expires_in + expires_at: expires_at } } end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index a9d38c06e..e59908979 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -170,7 +170,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert activity = Activity.get_by_id(fourth_id) assert expiration = ActivityExpiration.get_by_activity_id(fourth_id) assert expiration.scheduled_at == expires_at - assert fourth_response["pleroma"]["expires_in"] > 0 + assert fourth_response["pleroma"]["expires_at"] == NaiveDateTime.to_iso8601(expires_at) end test "replying to a status", %{conn: conn} do diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index eb0874ab2..073c69659 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -134,7 +134,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do in_reply_to_account_acct: nil, content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])}, spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}, - expires_in: nil + expires_at: nil } } -- cgit v1.2.3 From 0e2b5a3e6aed7947909c2a1ff1618403546f1572 Mon Sep 17 00:00:00 2001 From: Mike Verdone Date: Wed, 24 Jul 2019 17:25:11 +0200 Subject: WIP --- test/web/mastodon_api/mastodon_api_controller_test.exs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index e59908979..fbe0ab375 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -154,23 +154,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do refute id == third_id # An activity that will expire: - expires_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(120), :millisecond) - |> NaiveDateTime.truncate(:second) + expires_in = 120 conn_four = conn |> post("api/v1/statuses", %{ "status" => "oolong", - "expires_at" => expires_at + "expires_in" => expires_in }) assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200) assert activity = Activity.get_by_id(fourth_id) assert expiration = ActivityExpiration.get_by_activity_id(fourth_id) - assert expiration.scheduled_at == expires_at - assert fourth_response["pleroma"]["expires_at"] == NaiveDateTime.to_iso8601(expires_at) + + estimated_expires_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(expires_in), :millisecond) + |> NaiveDateTime.truncate(:second) + + # This assert will fail if the test takes longer than a minute. I sure hope it never does: + assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60 + assert fourth_response["pleroma"]["expires_at"] == NaiveDateTime.to_iso8601(expiration.scheduled_at) end test "replying to a status", %{conn: conn} do -- cgit v1.2.3 From 37229af15fe6d540e7b4a0b6e89f548a100d51c7 Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Thu, 22 Aug 2019 00:15:00 +0545 Subject: remove old user create and delete routes for admin --- lib/pleroma/web/router.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index eb3ee03f3..445cf62e2 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -151,10 +151,6 @@ defmodule Pleroma.Web.Router do post("/users/follow", AdminAPIController, :user_follow) post("/users/unfollow", AdminAPIController, :user_unfollow) - # TODO: to be removed at version 1.0 - delete("/user", AdminAPIController, :user_delete) - post("/user", AdminAPIController, :user_create) - delete("/users", AdminAPIController, :user_delete) post("/users", AdminAPIController, :users_create) patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) -- cgit v1.2.3 From 64bfb41c553a45855e86737298185c1395bbb350 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Thu, 22 Aug 2019 06:57:55 +0300 Subject: fixed unfollow for relay actor --- .../web/activity_pub/activity_pub_controller.ex | 14 +++++++++++ lib/pleroma/web/activity_pub/relay.ex | 3 ++- lib/pleroma/web/router.ex | 3 +++ .../activity_pub/activity_pub_controller_test.exs | 29 ++++++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 133a726c5..e72ec5500 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -104,6 +104,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end + # GET /relay/following + def following(%{assigns: %{relay: true}} = conn, _params) do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(UserView.render("following.json", %{user: Relay.get_actor()})) + end + def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do with %User{} = user <- User.get_cached_by_nickname(nickname), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), @@ -131,6 +138,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end + # GET /relay/followers + def followers(%{assigns: %{relay: true}} = conn, _params) do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(UserView.render("followers.json", %{user: Relay.get_actor()})) + end + def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do with %User{} = user <- User.get_cached_by_nickname(nickname), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 5f18cc64a..905e85cc6 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -36,7 +36,8 @@ defmodule Pleroma.Web.ActivityPub.Relay do def unfollow(target_instance) do with %User{} = local_user <- get_actor(), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), - {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do + {:ok, activity} <- ActivityPub.unfollow(local_user, target_user), + {:ok, _, _} <- User.unfollow(local_user, target_user) do Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") {:ok, activity} else diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 1eb6f7b9d..469e46f5d 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -687,6 +687,9 @@ defmodule Pleroma.Web.Router do get("/", ActivityPubController, :relay) post("/inbox", ActivityPubController, :inbox) + + get("/following", ActivityPubController, :following, assigns: %{relay: true}) + get("/followers", ActivityPubController, :followers, assigns: %{relay: true}) end scope "/internal/fetch", Pleroma.Web.ActivityPub do diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 77f5e39fa..cf71066fd 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -12,6 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.CommonAPI setup_all do @@ -593,6 +594,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end end + describe "/relay/followers" do + test "it returns relay followers", %{conn: conn} do + relay_actor = Relay.get_actor() + user = insert(:user) + User.follow(user, relay_actor) + + result = + conn + |> assign(:relay, true) + |> get("/relay/followers") + |> json_response(200) + + assert result["first"]["orderedItems"] == [user.ap_id] + end + end + + describe "/relay/following" do + test "it returns relay following", %{conn: conn} do + result = + conn + |> assign(:relay, true) + |> get("/relay/following") + |> json_response(200) + + assert result["first"]["orderedItems"] == [] + end + end + describe "/users/:nickname/followers" do test "it returns the followers in a collection", %{conn: conn} do user = insert(:user) -- cgit v1.2.3 From 399ca9133b67725242f76093103e9909e2337e72 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Thu, 22 Aug 2019 21:32:40 +0300 Subject: fix test --- lib/pleroma/web/activity_pub/relay.ex | 4 ++-- test/web/activity_pub/activity_pub_controller_test.exs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 905e85cc6..ce3e30874 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -36,8 +36,8 @@ defmodule Pleroma.Web.ActivityPub.Relay do def unfollow(target_instance) do with %User{} = local_user <- get_actor(), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), - {:ok, activity} <- ActivityPub.unfollow(local_user, target_user), - {:ok, _, _} <- User.unfollow(local_user, target_user) do + {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do + User.unfollow(local_user, target_user) Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") {:ok, activity} else diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index cf71066fd..5192e734f 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -10,9 +10,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectView + alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.CommonAPI setup_all do -- cgit v1.2.3 From 8dc6a6b210e56ec1a175a3496466d1f8aa62f128 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Thu, 22 Aug 2019 22:39:06 +0300 Subject: fix /inbox for Relay --- lib/pleroma/object/fetcher.ex | 4 +--- lib/pleroma/signature.ex | 6 ++++++ lib/pleroma/web/activity_pub/publisher.ex | 4 +--- lib/pleroma/web/router.ex | 10 +++++++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 8d79ddb1f..c1795ae0f 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -117,9 +117,7 @@ defmodule Pleroma.Object.Fetcher do def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do Logger.info("Fetching object #{id} via AP") - date = - NaiveDateTime.utc_now() - |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") + date = Pleroma.Signature.signed_date() headers = [{:Accept, "application/activity+json"}] diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index 15bf3c317..f20aeb0d5 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -53,4 +53,10 @@ defmodule Pleroma.Signature do HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) end end + + def signed_date, do: signed_date(NaiveDateTime.utc_now()) + + def signed_date(%NaiveDateTime{} = date) do + Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") + end end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 262529b84..c97405690 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -50,9 +50,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) - date = - NaiveDateTime.utc_now() - |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") + date = Pleroma.Signature.signed_date() signature = Pleroma.Signature.sign(actor, %{ diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 469e46f5d..c2e6e8819 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -133,6 +133,10 @@ defmodule Pleroma.Web.Router do }) end + pipeline :http_signature do + plug(Pleroma.Web.Plugs.HTTPSignaturePlug) + end + scope "/api/pleroma", Pleroma.Web.TwitterAPI do pipe_through(:pleroma_api) @@ -686,7 +690,11 @@ defmodule Pleroma.Web.Router do pipe_through(:ap_service_actor) get("/", ActivityPubController, :relay) - post("/inbox", ActivityPubController, :inbox) + + scope [] do + pipe_through(:http_signature) + post("/inbox", ActivityPubController, :inbox) + end get("/following", ActivityPubController, :following, assigns: %{relay: true}) get("/followers", ActivityPubController, :followers, assigns: %{relay: true}) -- cgit v1.2.3 From 73bcbf4fa3bcac7e3ef04049ae6e5768baa6b1d8 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Fri, 23 Aug 2019 21:17:14 +0300 Subject: add tests --- test/signature_test.exs | 14 ++++++++++++++ test/tasks/relay_test.exs | 4 +++- test/web/activity_pub/relay_test.exs | 3 +++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/test/signature_test.exs b/test/signature_test.exs index 26337eaf9..d5bf63d7d 100644 --- a/test/signature_test.exs +++ b/test/signature_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.SignatureTest do import ExUnit.CaptureLog import Pleroma.Factory import Tesla.Mock + import Mock alias Pleroma.Signature @@ -114,4 +115,17 @@ defmodule Pleroma.SignatureTest do "https://example.com/users/1234" end end + + describe "signed_date" do + test "it returns formatted current date" do + with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do + assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT" + end + end + + test "it returns formatted date" do + assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) == + "Fri, 23 Aug 2019 08:11:24 GMT" + end + end end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index 0d341c8d6..7bde56606 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -50,7 +50,8 @@ defmodule Mix.Tasks.Pleroma.RelayTest do %User{ap_id: follower_id} = local_user = Relay.get_actor() target_user = User.get_cached_by_ap_id(target_instance) follow_activity = Utils.fetch_latest_follow(local_user, target_user) - + User.follow(local_user, target_user) + assert "#{target_instance}/followers" in refresh_record(local_user).following Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) @@ -67,6 +68,7 @@ defmodule Mix.Tasks.Pleroma.RelayTest do assert undo_activity.data["type"] == "Undo" assert undo_activity.data["actor"] == local_user.ap_id assert undo_activity.data["object"] == cancelled_activity.data + refute "#{target_instance}/followers" in refresh_record(local_user).following end end diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index e10b808f7..aeef91cda 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -43,12 +43,15 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do user = insert(:user) service_actor = Relay.get_actor() ActivityPub.follow(service_actor, user) + Pleroma.User.follow(service_actor, user) + assert "#{user.ap_id}/followers" in refresh_record(service_actor).following assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id) assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" assert user.ap_id in activity.recipients assert activity.data["type"] == "Undo" assert activity.data["actor"] == service_actor.ap_id assert activity.data["to"] == [user.ap_id] + refute "#{user.ap_id}/followers" in refresh_record(service_actor).following end end -- cgit v1.2.3 From 6062017493bd8c8749fcbe590121d20ef94df44f Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Sat, 24 Aug 2019 17:17:17 +0300 Subject: put_resp_header("content-type", "application/activity+json") -> put_resp_content_type("application/activity+json") --- .../web/activity_pub/activity_pub_controller.ex | 34 +++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index e72ec5500..ed801a7ae 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -41,7 +41,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do with %User{} = user <- User.get_cached_by_nickname(nickname), {:ok, user} <- User.ensure_keys_present(user) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("user.json", %{user: user})) else nil -> {:error, :not_found} @@ -53,7 +53,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do %Object{} = object <- Object.get_cached_by_ap_id(ap_id), {_, true} <- {:public?, Visibility.is_public?(object)} do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(ObjectView.render("object.json", %{object: object})) else {:public?, false} -> @@ -69,7 +69,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {page, _} = Integer.parse(page) conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(ObjectView.render("likes.json", ap_id, likes, page)) else {:public?, false} -> @@ -83,7 +83,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {_, true} <- {:public?, Visibility.is_public?(object)}, likes <- Utils.get_object_likes(object) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(ObjectView.render("likes.json", ap_id, likes)) else {:public?, false} -> @@ -96,7 +96,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do %Activity{} = activity <- Activity.normalize(ap_id), {_, true} <- {:public?, Visibility.is_public?(activity)} do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(ObjectView.render("object.json", %{object: activity})) else {:public?, false} -> @@ -107,7 +107,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do # GET /relay/following def following(%{assigns: %{relay: true}} = conn, _params) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("following.json", %{user: Relay.get_actor()})) end @@ -119,12 +119,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {page, _} = Integer.parse(page) conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("following.json", %{user: user, page: page, for: for_user})) else {:show_follows, _} -> conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> send_resp(403, "") end end @@ -133,7 +133,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do with %User{} = user <- User.get_cached_by_nickname(nickname), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("following.json", %{user: user, for: for_user})) end end @@ -141,7 +141,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do # GET /relay/followers def followers(%{assigns: %{relay: true}} = conn, _params) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("followers.json", %{user: Relay.get_actor()})) end @@ -153,12 +153,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {page, _} = Integer.parse(page) conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user})) else {:show_followers, _} -> conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> send_resp(403, "") end end @@ -167,7 +167,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do with %User{} = user <- User.get_cached_by_nickname(nickname), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("followers.json", %{user: user, for: for_user})) end end @@ -176,7 +176,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do with %User{} = user <- User.get_cached_by_nickname(nickname), {:ok, user} <- User.ensure_keys_present(user) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]})) end end @@ -224,7 +224,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do defp represent_service_actor(%User{} = user, conn) do with {:ok, user} <- User.ensure_keys_present(user) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("user.json", %{user: user})) else nil -> {:error, :not_found} @@ -245,7 +245,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("user.json", %{user: user})) end @@ -254,7 +254,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do if nickname == user.nickname do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]})) else err = -- cgit v1.2.3 From 654d291b6d151bc372bca849ce0b42f723e2bd94 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Sat, 24 Aug 2019 17:41:53 +0300 Subject: update tests --- lib/pleroma/web/activity_pub/relay.ex | 27 ++++++++++---------------- test/web/activity_pub/relay_test.exs | 36 ++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index ce3e30874..c2ac38907 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -22,13 +22,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}") {:ok, activity} else - {:error, _} = error -> - Logger.error("error: #{inspect(error)}") - error - - e -> - Logger.error("error: #{inspect(e)}") - {:error, e} + error -> format_error(error) end end @@ -41,13 +35,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") {:ok, activity} else - {:error, _} = error -> - Logger.error("error: #{inspect(error)}") - error - - e -> - Logger.error("error: #{inspect(e)}") - {:error, e} + error -> format_error(error) end end @@ -57,11 +45,16 @@ defmodule Pleroma.Web.ActivityPub.Relay do %Object{} = object <- Object.normalize(activity) do ActivityPub.announce(user, object, nil, true, false) else - e -> - Logger.error("error: #{inspect(e)}") - {:error, inspect(e)} + error -> format_error(error) end end def publish(_), do: {:error, "Not implemented"} + + defp format_error({:error, error}), do: format_error(error) + + defp format_error(error) do + Logger.error("error: #{inspect(error)}") + {:error, error} + end end diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index aeef91cda..4f7d592a6 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do alias Pleroma.Web.ActivityPub.Relay import Pleroma.Factory + import Mock test "gets an actor for the relay" do user = Relay.get_actor() @@ -56,6 +57,8 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do end describe "publish/1" do + clear_config([:instance, :federating]) + test "returns error when activity not `Create` type" do activity = insert(:like_activity) assert Relay.publish(activity) == {:error, "Not implemented"} @@ -66,13 +69,44 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do assert Relay.publish(activity) == {:error, false} end - test "returns announce activity" do + test "returns error when object is unknown" do + activity = + insert(:note_activity, + data: %{ + "type" => "Create", + "object" => "http://mastodon.example.org/eee/99541947525187367" + } + ) + + assert Relay.publish(activity) == {:error, nil} + end + + test_with_mock "returns announce activity and publish to federate", + Pleroma.Web.Federator, + [:passthrough], + [] do + Pleroma.Config.put([:instance, :federating], true) + service_actor = Relay.get_actor() + note = insert(:note_activity) + assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note) + assert activity.data["type"] == "Announce" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["object"] == obj.data["id"] + assert called(Pleroma.Web.Federator.publish(activity, 5)) + end + + test_with_mock "returns announce activity and not publish to federate", + Pleroma.Web.Federator, + [:passthrough], + [] do + Pleroma.Config.put([:instance, :federating], false) service_actor = Relay.get_actor() note = insert(:note_activity) assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note) assert activity.data["type"] == "Announce" assert activity.data["actor"] == service_actor.ap_id assert activity.data["object"] == obj.data["id"] + refute called(Pleroma.Web.Federator.publish(activity, 5)) end end end -- cgit v1.2.3 From 1692fa89458f0f83f69ffa2f85a998869b8fe454 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 24 Aug 2019 17:22:26 +0200 Subject: ActivityExpirationWorker: Fix merge issues. --- lib/pleroma/activity_expiration_worker.ex | 2 +- lib/pleroma/application.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/activity_expiration_worker.ex b/lib/pleroma/activity_expiration_worker.ex index a341f58df..0f9e715f8 100644 --- a/lib/pleroma/activity_expiration_worker.ex +++ b/lib/pleroma/activity_expiration_worker.ex @@ -15,7 +15,7 @@ defmodule Pleroma.ActivityExpirationWorker do @schedule_interval :timer.minutes(1) - def start_link do + def start_link(_) do GenServer.start_link(__MODULE__, nil) end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 1e4de272c..483ac1f39 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -36,7 +36,7 @@ defmodule Pleroma.Application do Pleroma.Captcha, Pleroma.FlakeId, Pleroma.ScheduledActivityWorker, - Pleroma.ActiviyExpirationWorker + Pleroma.ActivityExpirationWorker ] ++ cachex_children() ++ hackney_pool_children() ++ -- cgit v1.2.3 From efb8818e9ee280b53eac17699e8114e8af82b03b Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 24 Aug 2019 17:22:48 +0200 Subject: Activity Expiration: Switch to 'expires_in' system. --- lib/pleroma/web/common_api/common_api.ex | 15 +++++++++++---- test/web/common_api/common_api_test.exs | 4 +--- test/web/mastodon_api/mastodon_api_controller_test.exs | 9 ++++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 69120cc19..5faddc9f4 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -201,16 +201,23 @@ defmodule Pleroma.Web.CommonAPI do end end - defp check_expiry_date(expiry_str) do - {:ok, expiry} = Ecto.Type.cast(:naive_datetime, expiry_str) + defp check_expiry_date({:ok, nil} = res), do: res + + defp check_expiry_date({:ok, in_seconds}) do + expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds) - if is_nil(expiry) || ActivityExpiration.expires_late_enough?(expiry) do + if ActivityExpiration.expires_late_enough?(expiry) do {:ok, expiry} else {:error, "Expiry date is too soon"} end end + defp check_expiry_date(expiry_str) do + Ecto.Type.cast(:integer, expiry_str) + |> check_expiry_date() + end + def post(user, %{"status" => status} = data) do limit = Pleroma.Config.get([:instance, :limit]) @@ -237,7 +244,7 @@ defmodule Pleroma.Web.CommonAPI do context <- make_context(in_reply_to, in_reply_to_conversation), cw <- data["spoiler_text"] || "", sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), - {:ok, expires_at} <- check_expiry_date(data["expires_at"]), + {:ok, expires_at} <- check_expiry_date(data["expires_in"]), full_payload <- String.trim(status <> cw), :ok <- validate_character_limit(full_payload, attachments, limit), object <- diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 5fda91438..f28a66090 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -213,10 +213,8 @@ defmodule Pleroma.Web.CommonAPITest do |> NaiveDateTime.truncate(:second) |> NaiveDateTime.add(1_000_000, :second) - expires_at_iso8601 = expires_at |> NaiveDateTime.to_iso8601() - assert {:ok, activity} = - CommonAPI.post(user, %{"status" => "chai", "expires_at" => expires_at_iso8601}) + CommonAPI.post(user, %{"status" => "chai", "expires_in" => 1_000_000}) assert expiration = Pleroma.ActivityExpiration.get_by_activity_id(activity.id) assert expiration.scheduled_at == expires_at diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index c05c39db6..6fcdc19aa 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -153,7 +153,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do refute id == third_id # An activity that will expire: - expires_in = 120 + # 2 hours + expires_in = 120 * 60 conn_four = conn @@ -168,12 +169,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do estimated_expires_at = NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(expires_in), :millisecond) + |> NaiveDateTime.add(expires_in) |> NaiveDateTime.truncate(:second) # This assert will fail if the test takes longer than a minute. I sure hope it never does: assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60 - assert fourth_response["pleroma"]["expires_at"] == NaiveDateTime.to_iso8601(expiration.scheduled_at) + + assert fourth_response["pleroma"]["expires_at"] == + NaiveDateTime.to_iso8601(expiration.scheduled_at) end test "replying to a status", %{conn: conn} do -- cgit v1.2.3 From 24994f3e0c643abe4d74bec3edec53fa89f4ed72 Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 24 Aug 2019 17:28:19 +0200 Subject: Activity expiration: Fix docs. --- docs/api/differences_in_mastoapi_responses.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 197c465d8..f34e3dd72 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -25,7 +25,7 @@ Has these additional fields under the `pleroma` object: - `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any) - `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` -- `expires_on`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire +- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire ## Attachments @@ -87,7 +87,7 @@ Additional parameters can be added to the JSON body/Form data: - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint. - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply. - `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`. -- `expires_on`: datetime (iso8601), sets when the posted activity should expire. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. +- `expires_in`: The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour. - `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`. ## PATCH `/api/v1/update_credentials` -- cgit v1.2.3 From 1d7033d96289edf0adf2ca61a725f93b345305ec Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 24 Aug 2019 15:33:17 +0000 Subject: Update CHANGELOG.md --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 949577842..b1ec21818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,9 +50,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - ActivityPub: Deactivated user deletion ### Added -- Expiring/ephemeral activites. All activities can have expires_on value set, which controls when they should be deleted automatically. -- Mastodon API: in post_status, the expires_in parameter lets you set the number of minutes until an activity expires. It must be at least one hour. -- Mastodon API: all status JSON responses contain a `pleroma.expires_on` item which states when an activity will expire. The value is only shown to the user who created the activity. To everyone else it's empty. +- Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically. +- Mastodon API: in post_status, the expires_in parameter lets you set the number of seconds until an activity expires. It must be at least one hour. +- Mastodon API: all status JSON responses contain a `pleroma.expires_at` item which states when an activity will expire. The value is only shown to the user who created the activity. To everyone else it's empty. - Configuration: `ActivityExpiration.enabled` controls whether expired activites will get deleted at the appropriate time. Enabled by default. - Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data. - **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo. -- cgit v1.2.3 From 3549cd9754f95b17a2be2eb76d9bb6c38bdbf288 Mon Sep 17 00:00:00 2001 From: kPherox Date: Sun, 25 Aug 2019 01:28:38 +0900 Subject: Change to use attachment only when fields do not exist --- lib/pleroma/user/info.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 45a39924b..779bfbc18 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -49,7 +49,7 @@ defmodule Pleroma.User.Info do field(:mascot, :map, default: nil) field(:emoji, {:array, :map}, default: []) field(:pleroma_settings_store, :map, default: %{}) - field(:fields, {:array, :map}, default: []) + field(:fields, {:array, :map}, default: nil) field(:raw_fields, {:array, :map}, default: []) field(:notification_settings, :map, @@ -422,7 +422,7 @@ defmodule Pleroma.User.Info do # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``. # For example: [{"name": "Pronoun", "value": "she/her"}, …] - def fields(%{fields: [], source_data: %{"attachment" => attachment}}) do + def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0) attachment @@ -431,6 +431,8 @@ defmodule Pleroma.User.Info do |> Enum.take(limit) end + def fields(%{fields: nil}), do: [] + def fields(%{fields: fields}), do: fields def follow_information_update(info, params) do -- cgit v1.2.3 From 18668447d268524e39d9cc8812805053ef9c186e Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 25 Aug 2019 07:10:22 +0200 Subject: HttpRequestMock: Log mock errors as warnings --- test/support/http_request_mock.ex | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 3adb5ba3b..00f4660c1 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -17,9 +17,14 @@ defmodule HttpRequestMock do with {:ok, res} <- apply(__MODULE__, method, [url, query, body, headers]) do res else - {_, _r} = error -> - # Logger.warn(r) - error + error -> + error = error + + with {:error, message} <- error do + Logger.warn(message) + end + + {_, _r} = error end end -- cgit v1.2.3 From e22737ffb5f7e7c567802e2bcef5520e2759e734 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 25 Aug 2019 07:33:46 +0200 Subject: HttpRequestMock: Improve non-implemented error message --- test/support/http_request_mock.ex | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 00f4660c1..320244c75 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -975,7 +975,7 @@ defmodule HttpRequestMock do def get(url, query, body, headers) do {:error, - "Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{ + "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{ inspect(headers) }"} end @@ -1037,7 +1037,10 @@ defmodule HttpRequestMock do }} end - def post(url, _query, _body, _headers) do - {:error, "Not implemented the mock response for post #{inspect(url)}"} + def post(url, query, body, headers) do + {:error, + "Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{ + inspect(headers) + }"} end end -- cgit v1.2.3 From 211e1637705266bebd33735d4bb1809c0326f707 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 25 Aug 2019 08:03:25 +0200 Subject: Implement missing mocks for rel=me --- test/support/http_request_mock.ex | 16 ++++++++++++++++ test/web/rel_me_test.exs | 29 ++--------------------------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 320244c75..c308e5a36 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -973,6 +973,22 @@ defmodule HttpRequestMock do }} end + def get("http://example.com/rel_me/anchor", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")}} + end + + def get("http://example.com/rel_me/anchor_nofollow", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor_nofollow.html")}} + end + + def get("http://example.com/rel_me/link", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_link.html")}} + end + + def get("http://example.com/rel_me/null", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")}} + end + def get(url, query, body, headers) do {:error, "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{ diff --git a/test/web/rel_me_test.exs b/test/web/rel_me_test.exs index 85515c432..2251fed16 100644 --- a/test/web/rel_me_test.exs +++ b/test/web/rel_me_test.exs @@ -5,33 +5,8 @@ defmodule Pleroma.Web.RelMeTest do use ExUnit.Case, async: true - setup do - Tesla.Mock.mock(fn - %{ - method: :get, - url: "http://example.com/rel_me/anchor" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")} - - %{ - method: :get, - url: "http://example.com/rel_me/anchor_nofollow" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor_nofollow.html")} - - %{ - method: :get, - url: "http://example.com/rel_me/link" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_link.html")} - - %{ - method: :get, - url: "http://example.com/rel_me/null" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")} - end) - + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok end -- cgit v1.2.3 From f3b12662731fa8d1aa458ea16fb4bcb176b77744 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 25 Aug 2019 08:48:38 +0200 Subject: user_test.exs: fix rel=me tests --- test/user_test.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/user_test.exs b/test/user_test.exs index 661ffc0b3..2cbc1f525 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1253,18 +1253,18 @@ defmodule Pleroma.UserTest do end test "Adds rel=me on linkbacked urls" do - user = insert(:user, ap_id: "http://social.example.org/users/lain") + user = insert(:user, ap_id: "https://social.example.org/users/lain") - bio = "http://example.org/rel_me/null" + bio = "http://example.com/rel_me/null" expected_text = "#{bio}" assert expected_text == User.parse_bio(bio, user) - bio = "http://example.org/rel_me/link" - expected_text = "#{bio}" + bio = "http://example.com/rel_me/link" + expected_text = "#{bio}" assert expected_text == User.parse_bio(bio, user) - bio = "http://example.org/rel_me/anchor" - expected_text = "#{bio}" + bio = "http://example.com/rel_me/anchor" + expected_text = "#{bio}" assert expected_text == User.parse_bio(bio, user) end end -- cgit v1.2.3 From 20c3f613d8574d67b1e5a47bf41f324101183398 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 25 Aug 2019 08:55:29 +0200 Subject: HttpRequestMock: Remove useless `error = error` --- test/support/http_request_mock.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 320244c75..314b20a45 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -18,8 +18,6 @@ defmodule HttpRequestMock do res else error -> - error = error - with {:error, message} <- error do Logger.warn(message) end -- cgit v1.2.3 From d74efde94e3526b45dc9b31d9d48ffce14203ffa Mon Sep 17 00:00:00 2001 From: kPherox Date: Mon, 26 Aug 2019 02:00:41 +0900 Subject: Update test for custom profile fields --- test/web/activity_pub/transmogrifier_test.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 629c76c97..0661d5d7c 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -563,6 +563,14 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do %{"name" => "foo", "value" => "updated"}, %{"name" => "foo1", "value" => "updated"} ] + + update_data = put_in(update_data, ["object", "attachment"], []) + + {:ok, _} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert User.Info.fields(user.info) == [] end test "it works for incoming update activities which lock the account" do -- cgit v1.2.3 From 37dd3867bb0439e4a2717eb780a1837196fcef00 Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Sun, 25 Aug 2019 19:39:37 +0000 Subject: Log admin/moderator actions --- CHANGELOG.md | 1 + docs/api/admin_api.md | 24 ++ lib/pleroma/moderation_log.ex | 433 +++++++++++++++++++++ lib/pleroma/web/admin_api/admin_api_controller.ex | 183 +++++++-- .../web/admin_api/views/moderation_log_view.ex | 26 ++ lib/pleroma/web/common_api/utils.ex | 3 +- lib/pleroma/web/ostatus/ostatus_controller.ex | 3 +- lib/pleroma/web/router.ex | 2 + .../20190818124341_create_moderation_log.exs | 11 + test/moderation_log_test.exs | 301 ++++++++++++++ test/web/admin_api/admin_api_controller_test.exs | 241 +++++++++++- 11 files changed, 1187 insertions(+), 41 deletions(-) create mode 100644 lib/pleroma/moderation_log.ex create mode 100644 lib/pleroma/web/admin_api/views/moderation_log_view.ex create mode 100644 priv/repo/migrations/20190818124341_create_moderation_log.exs create mode 100644 test/moderation_log_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c3051c94..2fdcb014a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Relays: Added a task to list relay subscriptions. - Mix Tasks: `mix pleroma.database fix_likes_collections` - Federation: Remove `likes` from objects. +- Admin API: Added moderation log ### Changed - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index 7ccb90836..d79c342be 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -694,3 +694,27 @@ Compile time settings (need instance reboot): ] } ``` + +## `/api/pleroma/admin/moderation_log` +### Get moderation log +- Method `GET` +- Params: + - *optional* `page`: **integer** page number + - *optional* `page_size`: **integer** number of users per page (default is `50`) +- Response: + +```json +[ + { + "data": { + "actor": { + "id": 1, + "nickname": "lain" + }, + "action": "relay_follow" + }, + "time": 1502812026, // timestamp + "message": "[2017-08-15 15:47:06] @nick0 followed relay: https://example.org/relay" // log message + } +] +``` diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex new file mode 100644 index 000000000..1ef6fe67a --- /dev/null +++ b/lib/pleroma/moderation_log.ex @@ -0,0 +1,433 @@ +defmodule Pleroma.ModerationLog do + use Ecto.Schema + + alias Pleroma.Activity + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.User + + import Ecto.Query + + schema "moderation_log" do + field(:data, :map) + + timestamps() + end + + def get_all(page, page_size) do + from(q in __MODULE__, + order_by: [desc: q.inserted_at], + limit: ^page_size, + offset: ^((page - 1) * page_size) + ) + |> Repo.all() + end + + def insert_log(%{ + actor: %User{} = actor, + subject: %User{} = subject, + action: action, + permission: permission + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + subject: user_to_map(subject), + action: action, + permission: permission + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "report_update", + subject: %Activity{data: %{"type" => "Flag"}} = subject + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "report_update", + subject: report_to_map(subject) + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "report_response", + subject: %Activity{} = subject, + text: text + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "report_response", + subject: report_to_map(subject), + text: text + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "status_update", + subject: %Activity{} = subject, + sensitive: sensitive, + visibility: visibility + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "status_update", + subject: status_to_map(subject), + sensitive: sensitive, + visibility: visibility + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "status_delete", + subject_id: subject_id + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "status_delete", + subject_id: subject_id + } + }) + end + + @spec insert_log(%{actor: User, subject: User, action: String.t()}) :: + {:ok, ModerationLog} | {:error, any} + def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: action, + subject: user_to_map(subject) + } + }) + end + + @spec insert_log(%{actor: User, subjects: [User], action: String.t()}) :: + {:ok, ModerationLog} | {:error, any} + def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do + subjects = Enum.map(subjects, &user_to_map/1) + + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: action, + subjects: subjects + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + followed: %User{} = followed, + follower: %User{} = follower, + action: "follow" + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "follow", + followed: user_to_map(followed), + follower: user_to_map(follower) + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + followed: %User{} = followed, + follower: %User{} = follower, + action: "unfollow" + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "unfollow", + followed: user_to_map(followed), + follower: user_to_map(follower) + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + nicknames: nicknames, + tags: tags, + action: action + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + nicknames: nicknames, + tags: tags, + action: action + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: action, + target: target + }) + when action in ["relay_follow", "relay_unfollow"] do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: action, + target: target + } + }) + end + + defp user_to_map(%User{} = user) do + user + |> Map.from_struct() + |> Map.take([:id, :nickname]) + |> Map.put(:type, "user") + end + + defp report_to_map(%Activity{} = report) do + %{ + type: "report", + id: report.id, + state: report.data["state"] + } + end + + defp status_to_map(%Activity{} = status) do + %{ + type: "status", + id: status.id + } + end + + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => action, + "followed" => %{"nickname" => followed_nickname}, + "follower" => %{"nickname" => follower_nickname} + } + }) do + "@#{actor_nickname} made @#{follower_nickname} #{action} @#{followed_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "delete", + "subject" => %{"nickname" => subject_nickname, "type" => "user"} + } + }) do + "@#{actor_nickname} deleted user @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "create", + "subjects" => subjects + } + }) do + nicknames = + subjects + |> Enum.map(&"@#{&1["nickname"]}") + |> Enum.join(", ") + + "@#{actor_nickname} created users: #{nicknames}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "activate", + "subject" => %{"nickname" => subject_nickname, "type" => "user"} + } + }) do + "@#{actor_nickname} activated user @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "deactivate", + "subject" => %{"nickname" => subject_nickname, "type" => "user"} + } + }) do + "@#{actor_nickname} deactivated user @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "nicknames" => nicknames, + "tags" => tags, + "action" => "tag" + } + }) do + nicknames_string = + nicknames + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags_string = tags |> Enum.join(", ") + + "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_string}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "nicknames" => nicknames, + "tags" => tags, + "action" => "untag" + } + }) do + nicknames_string = + nicknames + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags_string = tags |> Enum.join(", ") + + "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_string}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "grant", + "subject" => %{"nickname" => subject_nickname}, + "permission" => permission + } + }) do + "@#{actor_nickname} made @#{subject_nickname} #{permission}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "revoke", + "subject" => %{"nickname" => subject_nickname}, + "permission" => permission + } + }) do + "@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "relay_follow", + "target" => target + } + }) do + "@#{actor_nickname} followed relay: #{target}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "relay_unfollow", + "target" => target + } + }) do + "@#{actor_nickname} unfollowed relay: #{target}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_update", + "subject" => %{"id" => subject_id, "state" => state, "type" => "report"} + } + }) do + "@#{actor_nickname} updated report ##{subject_id} with '#{state}' state" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_response", + "subject" => %{"id" => subject_id, "type" => "report"}, + "text" => text + } + }) do + "@#{actor_nickname} responded with '#{text}' to report ##{subject_id}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_update", + "subject" => %{"id" => subject_id, "type" => "status"}, + "sensitive" => nil, + "visibility" => visibility + } + }) do + "@#{actor_nickname} updated status ##{subject_id}, set visibility: '#{visibility}'" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_update", + "subject" => %{"id" => subject_id, "type" => "status"}, + "sensitive" => sensitive, + "visibility" => nil + } + }) do + "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}'" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_update", + "subject" => %{"id" => subject_id, "type" => "status"}, + "sensitive" => sensitive, + "visibility" => visibility + } + }) do + "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}', visibility: '#{ + visibility + }'" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_delete", + "subject_id" => subject_id + } + }) do + "@#{actor_nickname} deleted status ##{subject_id}" + end +end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 048ac8019..544b9d7d8 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do use Pleroma.Web, :controller alias Pleroma.Activity + alias Pleroma.ModerationLog alias Pleroma.User alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.ActivityPub @@ -12,6 +13,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.Config alias Pleroma.Web.AdminAPI.ConfigView + alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI @@ -25,35 +27,61 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do action_fallback(:errors) - def user_delete(conn, %{"nickname" => nickname}) do - User.get_cached_by_nickname(nickname) - |> User.delete() + def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do + user = User.get_cached_by_nickname(nickname) + User.delete(user) + + ModerationLog.insert_log(%{ + actor: admin, + subject: user, + action: "delete" + }) conn |> json(nickname) end - def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do + def user_follow(%{assigns: %{user: admin}} = conn, %{ + "follower" => follower_nick, + "followed" => followed_nick + }) do with %User{} = follower <- User.get_cached_by_nickname(follower_nick), %User{} = followed <- User.get_cached_by_nickname(followed_nick) do User.follow(follower, followed) + + ModerationLog.insert_log(%{ + actor: admin, + followed: followed, + follower: follower, + action: "follow" + }) end conn |> json("ok") end - def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do + def user_unfollow(%{assigns: %{user: admin}} = conn, %{ + "follower" => follower_nick, + "followed" => followed_nick + }) do with %User{} = follower <- User.get_cached_by_nickname(follower_nick), %User{} = followed <- User.get_cached_by_nickname(followed_nick) do User.unfollow(follower, followed) + + ModerationLog.insert_log(%{ + actor: admin, + followed: followed, + follower: follower, + action: "unfollow" + }) end conn |> json("ok") end - def users_create(conn, %{"users" => users}) do + def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do changesets = Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} -> user_data = %{ @@ -78,10 +106,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> Map.values() |> Enum.map(fn user -> {:ok, user} = User.post_register_action(user) + user end) |> Enum.map(&AccountView.render("created.json", %{user: &1})) + ModerationLog.insert_log(%{ + actor: admin, + subjects: Map.values(users), + action: "create" + }) + conn |> json(res) @@ -129,23 +164,47 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def user_toggle_activation(conn, %{"nickname" => nickname}) do + def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do user = User.get_cached_by_nickname(nickname) {:ok, updated_user} = User.deactivate(user, !user.info.deactivated) + action = if user.info.deactivated, do: "activate", else: "deactivate" + + ModerationLog.insert_log(%{ + actor: admin, + subject: user, + action: action + }) + conn |> json(AccountView.render("show.json", %{user: updated_user})) end - def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do - with {:ok, _} <- User.tag(nicknames, tags), - do: json_response(conn, :no_content, "") + def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do + with {:ok, _} <- User.tag(nicknames, tags) do + ModerationLog.insert_log(%{ + actor: admin, + nicknames: nicknames, + tags: tags, + action: "tag" + }) + + json_response(conn, :no_content, "") + end end - def untag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do - with {:ok, _} <- User.untag(nicknames, tags), - do: json_response(conn, :no_content, "") + def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do + with {:ok, _} <- User.untag(nicknames, tags) do + ModerationLog.insert_log(%{ + actor: admin, + nicknames: nicknames, + tags: tags, + action: "untag" + }) + + json_response(conn, :no_content, "") + end end def list_users(conn, params) do @@ -186,7 +245,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> Enum.into(%{}, &{&1, true}) end - def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname}) + def right_add(%{assigns: %{user: admin}} = conn, %{ + "permission_group" => permission_group, + "nickname" => nickname + }) when permission_group in ["moderator", "admin"] do user = User.get_cached_by_nickname(nickname) @@ -201,6 +263,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> Ecto.Changeset.change() |> Ecto.Changeset.put_embed(:info, info_cng) + ModerationLog.insert_log(%{ + action: "grant", + actor: admin, + subject: user, + permission: permission_group + }) + {:ok, _user} = User.update_and_set_cache(cng) json(conn, info) @@ -221,7 +290,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def right_delete( - %{assigns: %{user: %User{:nickname => admin_nickname}}} = conn, + %{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn, %{ "permission_group" => permission_group, "nickname" => nickname @@ -245,6 +314,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do {:ok, _user} = User.update_and_set_cache(cng) + ModerationLog.insert_log(%{ + action: "revoke", + actor: admin, + subject: user, + permission: permission_group + }) + json(conn, info) end end @@ -253,15 +329,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do render_error(conn, :not_found, "No such permission_group") end - def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do + def set_activation_status(%{assigns: %{user: admin}} = conn, %{ + "nickname" => nickname, + "status" => status + }) do with {:ok, status} <- Ecto.Type.cast(:boolean, status), %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, _} <- User.deactivate(user, !status), - do: json_response(conn, :no_content, "") + {:ok, _} <- User.deactivate(user, !status) do + action = if(user.info.deactivated, do: "activate", else: "deactivate") + + ModerationLog.insert_log(%{ + actor: admin, + subject: user, + action: action + }) + + json_response(conn, :no_content, "") + end end - def relay_follow(conn, %{"relay_url" => target}) do + def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do with {:ok, _message} <- Relay.follow(target) do + ModerationLog.insert_log(%{ + action: "relay_follow", + actor: admin, + target: target + }) + json(conn, target) else _ -> @@ -271,8 +365,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def relay_unfollow(conn, %{"relay_url" => target}) do + def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do with {:ok, _message} <- Relay.unfollow(target) do + ModerationLog.insert_log(%{ + action: "relay_unfollow", + actor: admin, + target: target + }) + json(conn, target) else _ -> @@ -363,8 +463,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def report_update_state(conn, %{"id" => id, "state" => state}) do + def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do with {:ok, report} <- CommonAPI.update_report_state(id, state) do + ModerationLog.insert_log(%{ + action: "report_update", + actor: admin, + subject: report + }) + conn |> put_view(ReportView) |> render("show.json", %{report: report}) @@ -381,6 +487,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do {:ok, activity} = CommonAPI.post(user, params) + ModerationLog.insert_log(%{ + action: "report_response", + actor: user, + subject: activity, + text: params["status"] + }) + conn |> put_view(StatusView) |> render("status.json", %{activity: activity}) @@ -393,8 +506,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def status_update(conn, %{"id" => id} = params) do + def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do + {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"]) + + ModerationLog.insert_log(%{ + action: "status_update", + actor: admin, + subject: activity, + sensitive: sensitive, + visibility: params["visibility"] + }) + conn |> put_view(StatusView) |> render("status.json", %{activity: activity}) @@ -403,10 +526,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + ModerationLog.insert_log(%{ + action: "status_delete", + actor: user, + subject_id: id + }) + json(conn, %{}) end end + def list_log(conn, params) do + {page, page_size} = page_params(params) + + log = ModerationLog.get_all(page, page_size) + + conn + |> put_view(ModerationLogView) + |> render("index.json", %{log: log}) + end + def migrate_to_db(conn, _params) do Mix.Tasks.Pleroma.Config.run(["migrate_to_db"]) json(conn, %{}) diff --git a/lib/pleroma/web/admin_api/views/moderation_log_view.ex b/lib/pleroma/web/admin_api/views/moderation_log_view.ex new file mode 100644 index 000000000..b3fc7cfe5 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/moderation_log_view.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ModerationLogView do + use Pleroma.Web, :view + + alias Pleroma.ModerationLog + + def render("index.json", %{log: log}) do + render_many(log, __MODULE__, "show.json", as: :log_entry) + end + + def render("show.json", %{log_entry: log_entry}) do + time = + log_entry.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + + %{ + data: log_entry.data, + time: time, + message: ModerationLog.get_log_entry_message(log_entry) + } + end +end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 61b96aba9..6958c7511 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -93,8 +93,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do Activity.t() | nil, String.t(), Participation.t() | nil - ) :: - {list(String.t()), list(String.t())} + ) :: {list(String.t()), list(String.t())} def get_to_and_cc(_, _, _, _, %Participation{} = participation) do participation = Repo.preload(participation, :recipients) diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index fdba0f77f..07e2a4c2d 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -37,8 +37,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do action_fallback(:errors) def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do - with {_, %User{} = user} <- - {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do + with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do RedirectController.redirector_with_meta(conn, %{user: user}) end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 97c5016d5..f800d16fd 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -198,6 +198,8 @@ defmodule Pleroma.Web.Router do post("/config", AdminAPIController, :config_update) get("/config/migrate_to_db", AdminAPIController, :migrate_to_db) get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) + + get("/moderation_log", AdminAPIController, :list_log) end scope "/", Pleroma.Web.TwitterAPI do diff --git a/priv/repo/migrations/20190818124341_create_moderation_log.exs b/priv/repo/migrations/20190818124341_create_moderation_log.exs new file mode 100644 index 000000000..cef6636f3 --- /dev/null +++ b/priv/repo/migrations/20190818124341_create_moderation_log.exs @@ -0,0 +1,11 @@ +defmodule Pleroma.Repo.Migrations.CreateModerationLog do + use Ecto.Migration + + def change do + create table(:moderation_log) do + add(:data, :map) + + timestamps() + end + end +end diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs new file mode 100644 index 000000000..c78708471 --- /dev/null +++ b/test/moderation_log_test.exs @@ -0,0 +1,301 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ModerationLogTest do + alias Pleroma.Activity + alias Pleroma.ModerationLog + + use Pleroma.DataCase + + import Pleroma.Factory + + describe "user moderation" do + setup do + admin = insert(:user, info: %{is_admin: true}) + moderator = insert(:user, info: %{is_moderator: true}) + subject1 = insert(:user) + subject2 = insert(:user) + + [admin: admin, moderator: moderator, subject1: subject1, subject2: subject2] + end + + test "logging user deletion by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: subject1, + action: "delete" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} deleted user @#{subject1.nickname}" + end + + test "logging user creation by moderator", %{ + moderator: moderator, + subject1: subject1, + subject2: subject2 + } do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subjects: [subject1, subject2], + action: "create" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}" + end + + test "logging user follow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + followed: subject1, + follower: subject2, + action: "follow" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}" + end + + test "logging user unfollow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + followed: subject1, + follower: subject2, + action: "unfollow" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}" + end + + test "logging user tagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + nicknames: [subject1.nickname, subject2.nickname], + tags: ["foo", "bar"], + action: "tag" + }) + + log = Repo.one(ModerationLog) + + users = + [subject1.nickname, subject2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log) == + "@#{admin.nickname} added tags: #{tags} to users: #{users}" + end + + test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + nicknames: [subject1.nickname, subject2.nickname], + tags: ["foo", "bar"], + action: "untag" + }) + + log = Repo.one(ModerationLog) + + users = + [subject1.nickname, subject2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log) == + "@#{admin.nickname} removed tags: #{tags} from users: #{users}" + end + + test "logging user grant by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: subject1, + action: "grant", + permission: "moderator" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} made @#{subject1.nickname} moderator" + end + + test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: subject1, + action: "revoke", + permission: "moderator" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}" + end + + test "logging relay follow", %{moderator: moderator} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_follow", + target: "https://example.org/relay" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} followed relay: https://example.org/relay" + end + + test "logging relay unfollow", %{moderator: moderator} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_unfollow", + target: "https://example.org/relay" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} unfollowed relay: https://example.org/relay" + end + + test "logging report update", %{moderator: moderator} do + report = %Activity{ + id: "9m9I1F4p8ftrTP6QTI", + data: %{ + "type" => "Flag", + "state" => "resolved" + } + } + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "report_update", + subject: report + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state" + end + + test "logging report response", %{moderator: moderator} do + report = %Activity{ + id: "9m9I1F4p8ftrTP6QTI", + data: %{ + "type" => "Note" + } + } + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "report_response", + subject: report, + text: "look at this" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} responded with 'look at this' to report ##{report.id}" + end + + test "logging status sensitivity update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: "true", + visibility: nil + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'" + end + + test "logging status visibility update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: nil, + visibility: "private" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'" + end + + test "logging status sensitivity & visibility update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: "true", + visibility: "private" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'" + end + + test "logging status deletion", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_delete", + subject_id: note.id + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} deleted status ##{note.id}" + end + end +end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index ab829d6bd..1afdb6a50 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -7,6 +7,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.Activity alias Pleroma.HTML + alias Pleroma.ModerationLog + alias Pleroma.Repo alias Pleroma.User alias Pleroma.UserInviteToken alias Pleroma.Web.CommonAPI @@ -24,6 +26,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do |> put_req_header("accept", "application/json") |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") + log_entry = Repo.one(ModerationLog) + + assert log_entry.data["subject"]["nickname"] == user.nickname + assert log_entry.data["action"] == "delete" + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted user @#{user.nickname}" + assert json_response(conn, 200) == user.nickname end @@ -51,6 +61,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type")) assert response == ["success", "success"] + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} created users: @lain2, @lain" end test "Cannot create user with exisiting email" do @@ -218,6 +233,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do follower = User.get_cached_by_id(follower.id) assert User.following?(follower, user) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}" end end @@ -241,6 +261,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do follower = User.get_cached_by_id(follower.id) refute User.following?(follower, user) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}" end end @@ -261,17 +286,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do }&tags[]=foo&tags[]=bar" ) - %{conn: conn, user1: user1, user2: user2, user3: user3} + %{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3} end test "it appends specified tags to users with specified nicknames", %{ conn: conn, + admin: admin, user1: user1, user2: user2 } do assert json_response(conn, :no_content) assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"] assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"] + + log_entry = Repo.one(ModerationLog) + + users = + [user1.nickname, user2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} added tags: #{tags} to users: #{users}" end test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do @@ -297,17 +335,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do }&tags[]=x&tags[]=z" ) - %{conn: conn, user1: user1, user2: user2, user3: user3} + %{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3} end test "it removes specified tags from users with specified nicknames", %{ conn: conn, + admin: admin, user1: user1, user2: user2 } do assert json_response(conn, :no_content) assert User.get_cached_by_id(user1.id).tags == [] assert User.get_cached_by_id(user2.id).tags == ["y"] + + log_entry = Repo.one(ModerationLog) + + users = + [user1.nickname, user2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["x", "z"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} removed tags: #{tags} from users: #{users}" end test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do @@ -345,6 +396,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert json_response(conn, 200) == %{ "is_admin" => true } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{user.nickname} admin" end test "/:right DELETE, can remove from a permission group" do @@ -360,6 +416,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert json_response(conn, 200) == %{ "is_admin" => false } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} revoked admin role from @#{user.nickname}" end end @@ -372,10 +433,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do |> assign(:user, admin) |> put_req_header("accept", "application/json") - %{conn: conn} + %{conn: conn, admin: admin} end - test "deactivates the user", %{conn: conn} do + test "deactivates the user", %{conn: conn, admin: admin} do user = insert(:user) conn = @@ -385,9 +446,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do user = User.get_cached_by_id(user.id) assert user.info.deactivated == true assert json_response(conn, :no_content) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deactivated user @#{user.nickname}" end - test "activates the user", %{conn: conn} do + test "activates the user", %{conn: conn, admin: admin} do user = insert(:user, info: %{deactivated: true}) conn = @@ -397,6 +463,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do user = User.get_cached_by_id(user.id) assert user.info.deactivated == false assert json_response(conn, :no_content) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} activated user @#{user.nickname}" end test "returns 403 when requested by a non-admin", %{conn: conn} do @@ -987,6 +1058,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname) } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deactivated user @#{user.nickname}" end describe "GET /api/pleroma/admin/users/invite_token" do @@ -1172,25 +1248,35 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "status_ids" => [activity.id] }) - %{conn: assign(conn, :user, admin), id: report_id} + %{conn: assign(conn, :user, admin), id: report_id, admin: admin} end - test "mark report as resolved", %{conn: conn, id: id} do + test "mark report as resolved", %{conn: conn, id: id, admin: admin} do response = conn |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"}) |> json_response(:ok) assert response["state"] == "resolved" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated report ##{id} with 'resolved' state" end - test "closes report", %{conn: conn, id: id} do + test "closes report", %{conn: conn, id: id, admin: admin} do response = conn |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"}) |> json_response(:ok) assert response["state"] == "closed" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated report ##{id} with 'closed' state" end test "returns 400 when state is unknown", %{conn: conn, id: id} do @@ -1321,14 +1407,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end end + # describe "POST /api/pleroma/admin/reports/:id/respond" do setup %{conn: conn} do admin = insert(:user, info: %{is_admin: true}) - %{conn: assign(conn, :user, admin)} + %{conn: assign(conn, :user, admin), admin: admin} end - test "returns created dm", %{conn: conn} do + test "returns created dm", %{conn: conn, admin: admin} do [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) @@ -1351,6 +1438,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert reporter.nickname in recipients assert response["content"] == "I will check it out" assert response["visibility"] == "direct" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} responded with 'I will check it out' to report ##{ + response["id"] + }" end test "returns 400 when status is missing", %{conn: conn} do @@ -1374,10 +1468,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do admin = insert(:user, info: %{is_admin: true}) activity = insert(:note_activity) - %{conn: assign(conn, :user, admin), id: activity.id} + %{conn: assign(conn, :user, admin), id: activity.id, admin: admin} end - test "toggle sensitive flag", %{conn: conn, id: id} do + test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do response = conn |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"}) @@ -1385,6 +1479,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert response["sensitive"] + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated status ##{id}, set sensitive: 'true'" + response = conn |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"}) @@ -1393,7 +1492,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do refute response["sensitive"] end - test "change visibility flag", %{conn: conn, id: id} do + test "change visibility flag", %{conn: conn, id: id, admin: admin} do response = conn |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"}) @@ -1401,6 +1500,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert response["visibility"] == "public" + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated status ##{id}, set visibility: 'public'" + response = conn |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"}) @@ -1430,15 +1534,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do admin = insert(:user, info: %{is_admin: true}) activity = insert(:note_activity) - %{conn: assign(conn, :user, admin), id: activity.id} + %{conn: assign(conn, :user, admin), id: activity.id, admin: admin} end - test "deletes status", %{conn: conn, id: id} do + test "deletes status", %{conn: conn, id: id, admin: admin} do conn |> delete("/api/pleroma/admin/statuses/#{id}") |> json_response(:ok) refute Activity.get_by_id(id) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted status ##{id}" end test "returns error when status is not exist", %{conn: conn} do @@ -2139,6 +2248,108 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert json_response(conn, 200) |> length() == 5 end end + + describe "GET /api/pleroma/admin/moderation_log" do + setup %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + + %{conn: assign(conn, :user, admin), admin: admin} + end + + test "returns the log", %{conn: conn, admin: admin} do + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) + }) + + conn = get(conn, "/api/pleroma/admin/moderation_log") + + response = json_response(conn, 200) + [first_entry, second_entry] = response + + assert response |> length() == 2 + assert first_entry["data"]["action"] == "relay_unfollow" + + assert first_entry["message"] == + "@#{admin.nickname} unfollowed relay: https://example.org/relay" + + assert second_entry["data"]["action"] == "relay_follow" + + assert second_entry["message"] == + "@#{admin.nickname} followed relay: https://example.org/relay" + end + + test "returns the log with pagination", %{conn: conn, admin: admin} do + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) + }) + + conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1") + + response1 = json_response(conn1, 200) + [first_entry] = response1 + + assert response1 |> length() == 1 + assert first_entry["data"]["action"] == "relay_unfollow" + + assert first_entry["message"] == + "@#{admin.nickname} unfollowed relay: https://example.org/relay" + + conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2") + + response2 = json_response(conn2, 200) + [second_entry] = response2 + + assert response2 |> length() == 1 + assert second_entry["data"]["action"] == "relay_follow" + + assert second_entry["message"] == + "@#{admin.nickname} followed relay: https://example.org/relay" + end + end end # Needed for testing -- cgit v1.2.3 From 3b1b631c2aedc8e359c296b11237fa4f6edd31e5 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 26 Aug 2019 18:59:57 +0700 Subject: Add validation in Pleroma.List.create/2 --- lib/pleroma/list.ex | 22 +++++++++++++--------- test/list_test.exs | 7 +++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index 1d320206e..c572380c2 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -109,15 +109,19 @@ defmodule Pleroma.List do end def create(title, %User{} = creator) do - list = %Pleroma.List{user_id: creator.id, title: title} - - Repo.transaction(fn -> - list = Repo.insert!(list) - - list - |> change(ap_id: "#{creator.ap_id}/lists/#{list.id}") - |> Repo.update!() - end) + changeset = title_changeset(%Pleroma.List{user_id: creator.id}, %{title: title}) + + if changeset.valid? do + Repo.transaction(fn -> + list = Repo.insert!(changeset) + + list + |> change(ap_id: "#{creator.ap_id}/lists/#{list.id}") + |> Repo.update!() + end) + else + {:error, changeset} + end end def follow(%Pleroma.List{following: following} = list, %User{} = followed) do diff --git a/test/list_test.exs b/test/list_test.exs index f39033d02..8efba75ea 100644 --- a/test/list_test.exs +++ b/test/list_test.exs @@ -15,6 +15,13 @@ defmodule Pleroma.ListTest do assert title == "title" end + test "validates title" do + user = insert(:user) + + assert {:error, changeset} = Pleroma.List.create("", user) + assert changeset.errors == [title: {"can't be blank", [validation: :required]}] + end + test "getting a list not belonging to the user" do user = insert(:user) other_user = insert(:user) -- cgit v1.2.3 From 4d82bc8b0b5a0b8b584b43330f902f8dc9637d3d Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 26 Aug 2019 19:16:40 +0700 Subject: Extract MastodonAPI.MastodonAPIController.errors/2 to MastodonAPI.FallbackController --- .../controllers/fallback_controller.ex | 34 ++++++++++++++++++++++ .../web/mastodon_api/mastodon_api_controller.ex | 31 +------------------- .../web/mastodon_api/subscription_controller.ex | 4 +-- 3 files changed, 36 insertions(+), 33 deletions(-) create mode 100644 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex diff --git a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex new file mode 100644 index 000000000..41243d5e7 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.FallbackController do + use Pleroma.Web, :controller + + def call(conn, {:error, %Ecto.Changeset{} = changeset}) do + error_message = + changeset + |> Ecto.Changeset.traverse_errors(fn {message, _opt} -> message end) + |> Enum.map_join(", ", fn {_k, v} -> v end) + + conn + |> put_status(:unprocessable_entity) + |> json(%{error: error_message}) + end + + def call(conn, {:error, :not_found}) do + render_error(conn, :not_found, "Record not found") + end + + def call(conn, {:error, error_message}) do + conn + |> put_status(:bad_request) + |> json(%{error: error_message}) + end + + def call(conn, _) do + conn + |> put_status(:internal_server_error) + |> json(dgettext("errors", "Something went wrong")) + end +end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 53cf95fbb..e51b2d89c 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -83,7 +83,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do @local_mastodon_name "Mastodon-Local" - action_fallback(:errors) + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) def create_app(conn, params) do scopes = Scopes.fetch_scopes(params, ["read"]) @@ -1587,35 +1587,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, %{}) end - # fallback action - # - def errors(conn, {:error, %Changeset{} = changeset}) do - error_message = - changeset - |> Changeset.traverse_errors(fn {message, _opt} -> message end) - |> Enum.map_join(", ", fn {_k, v} -> v end) - - conn - |> put_status(:unprocessable_entity) - |> json(%{error: error_message}) - end - - def errors(conn, {:error, :not_found}) do - render_error(conn, :not_found, "Record not found") - end - - def errors(conn, {:error, error_message}) do - conn - |> put_status(:bad_request) - |> json(%{error: error_message}) - end - - def errors(conn, _) do - conn - |> put_status(:internal_server_error) - |> json(dgettext("errors", "Something went wrong")) - end - def suggestions(%{assigns: %{user: user}} = conn, _) do suggestions = Config.get(:suggestions) diff --git a/lib/pleroma/web/mastodon_api/subscription_controller.ex b/lib/pleroma/web/mastodon_api/subscription_controller.ex index 255ee2f18..e2b17aab1 100644 --- a/lib/pleroma/web/mastodon_api/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/subscription_controller.ex @@ -64,8 +64,6 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do end def errors(conn, _) do - conn - |> put_status(:internal_server_error) - |> json(dgettext("errors", "Something went wrong")) + Pleroma.Web.MastodonAPI.FallbackController.call(conn, nil) end end -- cgit v1.2.3 From 30510ade0e2f813413c5599245adc4dae8c7ffd8 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 26 Aug 2019 19:37:54 +0700 Subject: Extract MastodonAPIController's list actions into MastodonAPI.ListController; Add more tests --- .../mastodon_api/controllers/list_controller.ex | 84 +++++++++++ .../web/mastodon_api/mastodon_api_controller.ex | 76 ---------- lib/pleroma/web/mastodon_api/views/list_view.ex | 6 +- lib/pleroma/web/router.ex | 16 +- .../controllers/list_controller_test.exs | 166 +++++++++++++++++++++ test/web/mastodon_api/list_view_test.exs | 22 --- .../mastodon_api/mastodon_api_controller_test.exs | 101 +------------ test/web/mastodon_api/views/list_view_test.exs | 32 ++++ 8 files changed, 294 insertions(+), 209 deletions(-) create mode 100644 lib/pleroma/web/mastodon_api/controllers/list_controller.ex create mode 100644 test/web/mastodon_api/controllers/list_controller_test.exs delete mode 100644 test/web/mastodon_api/list_view_test.exs create mode 100644 test/web/mastodon_api/views/list_view_test.exs diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex new file mode 100644 index 000000000..2873deda8 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ListController do + use Pleroma.Web, :controller + + alias Pleroma.User + alias Pleroma.Web.MastodonAPI.AccountView + + plug(:list_by_id_and_user when action not in [:index, :create]) + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + # GET /api/v1/lists + def index(%{assigns: %{user: user}} = conn, opts) do + lists = Pleroma.List.for_user(user, opts) + render(conn, "index.json", lists: lists) + end + + # POST /api/v1/lists + def create(%{assigns: %{user: user}} = conn, %{"title" => title}) do + with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do + render(conn, "show.json", list: list) + end + end + + # GET /api/v1/lists/:id + def show(%{assigns: %{list: list}} = conn, _) do + render(conn, "show.json", list: list) + end + + # PUT /api/v1/lists/:id + def update(%{assigns: %{list: list}} = conn, %{"title" => title}) do + with {:ok, list} <- Pleroma.List.rename(list, title) do + render(conn, "show.json", list: list) + end + end + + # DELETE /api/v1/lists/:id + def delete(%{assigns: %{list: list}} = conn, _) do + with {:ok, _list} <- Pleroma.List.delete(list) do + json(conn, %{}) + end + end + + # GET /api/v1/lists/:id/accounts + def list_accounts(%{assigns: %{user: user, list: list}} = conn, _) do + with {:ok, users} <- Pleroma.List.get_following(list) do + conn + |> put_view(AccountView) + |> render("accounts.json", for: user, users: users, as: :user) + end + end + + # POST /api/v1/lists/:id/accounts + def add_to_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do + Enum.each(account_ids, fn account_id -> + with %User{} = followed <- User.get_cached_by_id(account_id) do + Pleroma.List.follow(list, followed) + end + end) + + json(conn, %{}) + end + + # DELETE /api/v1/lists/:id/accounts + def remove_from_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do + Enum.each(account_ids, fn account_id -> + with %User{} = followed <- User.get_cached_by_id(account_id) do + Pleroma.List.unfollow(list, followed) + end + end) + + json(conn, %{}) + end + + defp list_by_id_and_user(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do + case Pleroma.List.get(id, user) do + %Pleroma.List{} = list -> assign(conn, :list, list) + nil -> conn |> render_error(:not_found, "List not found") |> halt() + end + end +end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index e51b2d89c..31b0aaca0 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1205,88 +1205,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> render("index.json", %{activities: activities, for: user, as: :activity}) end - def get_lists(%{assigns: %{user: user}} = conn, opts) do - lists = Pleroma.List.for_user(user, opts) - res = ListView.render("lists.json", lists: lists) - json(conn, res) - end - - def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do - res = ListView.render("list.json", list: list) - json(conn, res) - else - _e -> render_error(conn, :not_found, "Record not found") - end - end - def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do lists = Pleroma.List.get_lists_account_belongs(user, account_id) res = ListView.render("lists.json", lists: lists) json(conn, res) end - def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Pleroma.List{} = list <- Pleroma.List.get(id, user), - {:ok, _list} <- Pleroma.List.delete(list) do - json(conn, %{}) - else - _e -> - json(conn, dgettext("errors", "error")) - end - end - - def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do - with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do - res = ListView.render("list.json", list: list) - json(conn, res) - end - end - - def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do - accounts - |> Enum.each(fn account_id -> - with %Pleroma.List{} = list <- Pleroma.List.get(id, user), - %User{} = followed <- User.get_cached_by_id(account_id) do - Pleroma.List.follow(list, followed) - end - end) - - json(conn, %{}) - end - - def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do - accounts - |> Enum.each(fn account_id -> - with %Pleroma.List{} = list <- Pleroma.List.get(id, user), - %User{} = followed <- User.get_cached_by_id(account_id) do - Pleroma.List.unfollow(list, followed) - end - end) - - json(conn, %{}) - end - - def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Pleroma.List{} = list <- Pleroma.List.get(id, user), - {:ok, users} = Pleroma.List.get_following(list) do - conn - |> put_view(AccountView) - |> render("accounts.json", %{for: user, users: users, as: :user}) - end - end - - def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do - with %Pleroma.List{} = list <- Pleroma.List.get(id, user), - {:ok, list} <- Pleroma.List.rename(list, title) do - res = ListView.render("list.json", list: list) - json(conn, res) - else - _e -> - json(conn, dgettext("errors", "error")) - end - end - def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do params = diff --git a/lib/pleroma/web/mastodon_api/views/list_view.ex b/lib/pleroma/web/mastodon_api/views/list_view.ex index 0f86e2512..bfda6f5b3 100644 --- a/lib/pleroma/web/mastodon_api/views/list_view.ex +++ b/lib/pleroma/web/mastodon_api/views/list_view.ex @@ -6,11 +6,11 @@ defmodule Pleroma.Web.MastodonAPI.ListView do use Pleroma.Web, :view alias Pleroma.Web.MastodonAPI.ListView - def render("lists.json", %{lists: lists} = opts) do - render_many(lists, ListView, "list.json", opts) + def render("index.json", %{lists: lists} = opts) do + render_many(lists, ListView, "show.json", opts) end - def render("list.json", %{list: list}) do + def render("show.json", %{list: list}) do %{ id: to_string(list.id), title: list.title diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 1ad33630c..969dc66fd 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -312,9 +312,9 @@ defmodule Pleroma.Web.Router do get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) - get("/lists", MastodonAPIController, :get_lists) - get("/lists/:id", MastodonAPIController, :get_list) - get("/lists/:id/accounts", MastodonAPIController, :list_accounts) + get("/lists", ListController, :index) + get("/lists/:id", ListController, :show) + get("/lists/:id/accounts", ListController, :list_accounts) get("/domain_blocks", MastodonAPIController, :domain_blocks) @@ -355,12 +355,12 @@ defmodule Pleroma.Web.Router do post("/media", MastodonAPIController, :upload) put("/media/:id", MastodonAPIController, :update_media) - delete("/lists/:id", MastodonAPIController, :delete_list) - post("/lists", MastodonAPIController, :create_list) - put("/lists/:id", MastodonAPIController, :rename_list) + delete("/lists/:id", ListController, :delete) + post("/lists", ListController, :create) + put("/lists/:id", ListController, :update) - post("/lists/:id/accounts", MastodonAPIController, :add_to_list) - delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list) + post("/lists/:id/accounts", ListController, :add_to_list) + delete("/lists/:id/accounts", ListController, :remove_from_list) post("/filters", MastodonAPIController, :create_filter) get("/filters/:id", MastodonAPIController, :get_filter) diff --git a/test/web/mastodon_api/controllers/list_controller_test.exs b/test/web/mastodon_api/controllers/list_controller_test.exs new file mode 100644 index 000000000..093506309 --- /dev/null +++ b/test/web/mastodon_api/controllers/list_controller_test.exs @@ -0,0 +1,166 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ListControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Repo + + import Pleroma.Factory + + test "creating a list", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/lists", %{"title" => "cuties"}) + + assert %{"title" => title} = json_response(conn, 200) + assert title == "cuties" + end + + test "renders error for invalid params", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/lists", %{"title" => nil}) + + assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity) + end + + test "listing a user's lists", %{conn: conn} do + user = insert(:user) + + conn + |> assign(:user, user) + |> post("/api/v1/lists", %{"title" => "cuties"}) + + conn + |> assign(:user, user) + |> post("/api/v1/lists", %{"title" => "cofe"}) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/lists") + + assert [ + %{"id" => _, "title" => "cofe"}, + %{"id" => _, "title" => "cuties"} + ] = json_response(conn, :ok) + end + + test "adding users to a list", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + + assert %{} == json_response(conn, 200) + %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) + assert following == [other_user.follower_address] + end + + test "removing users from a list", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + {:ok, list} = Pleroma.List.follow(list, third_user) + + conn = + conn + |> assign(:user, user) + |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + + assert %{} == json_response(conn, 200) + %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) + assert following == [third_user.follower_address] + end + + test "listing users in a list", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + + assert [%{"id" => id}] = json_response(conn, 200) + assert id == to_string(other_user.id) + end + + test "retrieving a list", %{conn: conn} do + user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/lists/#{list.id}") + + assert %{"id" => id} = json_response(conn, 200) + assert id == to_string(list.id) + end + + test "renders 404 if list is not found", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/lists/666") + + assert %{"error" => "List not found"} = json_response(conn, :not_found) + end + + test "renaming a list", %{conn: conn} do + user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + + conn = + conn + |> assign(:user, user) + |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"}) + + assert %{"title" => name} = json_response(conn, 200) + assert name == "newname" + end + + test "validates title when renaming a list", %{conn: conn} do + user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + + conn = + conn + |> assign(:user, user) + |> put("/api/v1/lists/#{list.id}", %{"title" => " "}) + + assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity) + end + + test "deleting a list", %{conn: conn} do + user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + + conn = + conn + |> assign(:user, user) + |> delete("/api/v1/lists/#{list.id}") + + assert %{} = json_response(conn, 200) + assert is_nil(Repo.get(Pleroma.List, list.id)) + end +end diff --git a/test/web/mastodon_api/list_view_test.exs b/test/web/mastodon_api/list_view_test.exs deleted file mode 100644 index 73143467f..000000000 --- a/test/web/mastodon_api/list_view_test.exs +++ /dev/null @@ -1,22 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ListViewTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.MastodonAPI.ListView - - test "Represent a list" do - user = insert(:user) - title = "mortal enemies" - {:ok, list} = Pleroma.List.create(title, user) - - expected = %{ - id: to_string(list.id), - title: title - } - - assert expected == ListView.render("list.json", %{list: list}) - end -end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 6fcdc19aa..4fd0a5aeb 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -927,106 +927,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do end end - describe "lists" do - test "creating a list", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> post("/api/v1/lists", %{"title" => "cuties"}) - - assert %{"title" => title} = json_response(conn, 200) - assert title == "cuties" - end - - test "adding users to a list", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - - conn = - conn - |> assign(:user, user) - |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - - assert %{} == json_response(conn, 200) - %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) - assert following == [other_user.follower_address] - end - - test "removing users from a list", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - {:ok, list} = Pleroma.List.follow(list, third_user) - - conn = - conn - |> assign(:user, user) - |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - - assert %{} == json_response(conn, 200) - %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) - assert following == [third_user.follower_address] - end - - test "listing users in a list", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - - assert [%{"id" => id}] = json_response(conn, 200) - assert id == to_string(other_user.id) - end - - test "retrieving a list", %{conn: conn} do - user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/lists/#{list.id}") - - assert %{"id" => id} = json_response(conn, 200) - assert id == to_string(list.id) - end - - test "renaming a list", %{conn: conn} do - user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - - conn = - conn - |> assign(:user, user) - |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"}) - - assert %{"title" => name} = json_response(conn, 200) - assert name == "newname" - end - - test "deleting a list", %{conn: conn} do - user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - - conn = - conn - |> assign(:user, user) - |> delete("/api/v1/lists/#{list.id}") - - assert %{} = json_response(conn, 200) - assert is_nil(Repo.get(Pleroma.List, list.id)) - end - + describe "list timelines" do test "list timeline", %{conn: conn} do user = insert(:user) other_user = insert(:user) diff --git a/test/web/mastodon_api/views/list_view_test.exs b/test/web/mastodon_api/views/list_view_test.exs new file mode 100644 index 000000000..fb00310b9 --- /dev/null +++ b/test/web/mastodon_api/views/list_view_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ListViewTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.MastodonAPI.ListView + + test "show" do + user = insert(:user) + title = "mortal enemies" + {:ok, list} = Pleroma.List.create(title, user) + + expected = %{ + id: to_string(list.id), + title: title + } + + assert expected == ListView.render("show.json", %{list: list}) + end + + test "index" do + user = insert(:user) + + {:ok, list} = Pleroma.List.create("my list", user) + {:ok, list2} = Pleroma.List.create("cofe", user) + + assert [%{id: _, title: "my list"}, %{id: _, title: "cofe"}] = + ListView.render("index.json", lists: [list, list2]) + end +end -- cgit v1.2.3 From 4194abbc8fbc8003d9923edaa491e798bea92107 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 26 Aug 2019 19:32:47 +0700 Subject: Move mastodon_api/*_controller.ex to mastodon_api/controllers/ --- .../controllers/mastodon_api_controller.ex | 1700 ++++++++++++++++++++ .../mastodon_api/controllers/search_controller.ex | 120 ++ .../controllers/subscription_controller.ex | 69 + .../web/mastodon_api/mastodon_api_controller.ex | 1700 -------------------- lib/pleroma/web/mastodon_api/search_controller.ex | 120 -- .../web/mastodon_api/subscription_controller.ex | 69 - 6 files changed, 1889 insertions(+), 1889 deletions(-) create mode 100644 lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex create mode 100644 lib/pleroma/web/mastodon_api/controllers/search_controller.ex create mode 100644 lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex delete mode 100644 lib/pleroma/web/mastodon_api/mastodon_api_controller.ex delete mode 100644 lib/pleroma/web/mastodon_api/search_controller.ex delete mode 100644 lib/pleroma/web/mastodon_api/subscription_controller.ex diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex new file mode 100644 index 000000000..83e877c0e --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -0,0 +1,1700 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, + only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3] + + alias Ecto.Changeset + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.Config + alias Pleroma.Conversation.Participation + alias Pleroma.Filter + alias Pleroma.Formatter + alias Pleroma.HTTP + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Pagination + alias Pleroma.Plugs.RateLimiter + alias Pleroma.Repo + alias Pleroma.ScheduledActivity + alias Pleroma.Stats + alias Pleroma.User + alias Pleroma.Web + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.AppView + alias Pleroma.Web.MastodonAPI.ConversationView + alias Pleroma.Web.MastodonAPI.FilterView + alias Pleroma.Web.MastodonAPI.ListView + alias Pleroma.Web.MastodonAPI.MastodonAPI + alias Pleroma.Web.MastodonAPI.MastodonView + alias Pleroma.Web.MastodonAPI.NotificationView + alias Pleroma.Web.MastodonAPI.ReportView + alias Pleroma.Web.MastodonAPI.ScheduledActivityView + alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.MediaProxy + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Scopes + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.TwitterAPI.TwitterAPI + + alias Pleroma.Web.ControllerHelper + import Ecto.Query + + require Logger + require Pleroma.Constants + + @rate_limited_relations_actions ~w(follow unfollow)a + + @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status + post_status delete_status)a + + plug( + RateLimiter, + {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]} + when action in ~w(reblog_status unreblog_status)a + ) + + plug( + RateLimiter, + {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} + when action in ~w(fav_status unfav_status)a + ) + + plug( + RateLimiter, + {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions + ) + + plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions) + plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions) + plug(RateLimiter, :app_account_creation when action == :account_register) + plug(RateLimiter, :search when action in [:search, :search2, :account_search]) + plug(RateLimiter, :password_reset when action == :password_reset) + plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend) + + @local_mastodon_name "Mastodon-Local" + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + def create_app(conn, params) do + scopes = Scopes.fetch_scopes(params, ["read"]) + + app_attrs = + params + |> Map.drop(["scope", "scopes"]) + |> Map.put("scopes", scopes) + + with cs <- App.register_changeset(%App{}, app_attrs), + false <- cs.changes[:client_name] == @local_mastodon_name, + {:ok, app} <- Repo.insert(cs) do + conn + |> put_view(AppView) + |> render("show.json", %{app: app}) + end + end + + defp add_if_present( + map, + params, + params_field, + map_field, + value_function \\ fn x -> {:ok, x} end + ) do + if Map.has_key?(params, params_field) do + case value_function.(params[params_field]) do + {:ok, new_value} -> Map.put(map, map_field, new_value) + :error -> map + end + else + map + end + end + + def update_credentials(%{assigns: %{user: user}} = conn, params) do + original_user = user + + user_params = + %{} + |> add_if_present(params, "display_name", :name) + |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end) + |> add_if_present(params, "avatar", :avatar, fn value -> + with %Plug.Upload{} <- value, + {:ok, object} <- ActivityPub.upload(value, type: :avatar) do + {:ok, object.data} + else + _ -> :error + end + end) + + emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") + + user_info_emojis = + user.info + |> Map.get(:emoji, []) + |> Enum.concat(Formatter.get_emoji_map(emojis_text)) + |> Enum.dedup() + + info_params = + [ + :no_rich_text, + :locked, + :hide_followers, + :hide_follows, + :hide_favorites, + :show_role, + :skip_thread_containment + ] + |> Enum.reduce(%{}, fn key, acc -> + add_if_present(acc, params, to_string(key), key, fn value -> + {:ok, ControllerHelper.truthy_param?(value)} + end) + end) + |> add_if_present(params, "default_scope", :default_scope) + |> add_if_present(params, "fields", :fields, fn fields -> + fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) + + {:ok, fields} + end) + |> add_if_present(params, "fields", :raw_fields) + |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> + {:ok, Map.merge(user.info.pleroma_settings_store, value)} + end) + |> add_if_present(params, "header", :banner, fn value -> + with %Plug.Upload{} <- value, + {:ok, object} <- ActivityPub.upload(value, type: :banner) do + {:ok, object.data} + else + _ -> :error + end + end) + |> add_if_present(params, "pleroma_background_image", :background, fn value -> + with %Plug.Upload{} <- value, + {:ok, object} <- ActivityPub.upload(value, type: :background) do + {:ok, object.data} + else + _ -> :error + end + end) + |> Map.put(:emoji, user_info_emojis) + + info_cng = User.Info.profile_update(user.info, info_params) + + with changeset <- User.update_changeset(user, user_params), + changeset <- Changeset.put_embed(changeset, :info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + if original_user != user do + CommonAPI.update(user) + end + + json( + conn, + AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) + ) + else + _e -> render_error(conn, :forbidden, "Invalid request") + end + end + + def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + change = Changeset.change(user, %{avatar: nil}) + {:ok, user} = User.update_and_set_cache(change) + CommonAPI.update(user) + + json(conn, %{url: nil}) + end + + def update_avatar(%{assigns: %{user: user}} = conn, params) do + {:ok, object} = ActivityPub.upload(params, type: :avatar) + change = Changeset.change(user, %{avatar: object.data}) + {:ok, user} = User.update_and_set_cache(change) + CommonAPI.update(user) + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) + end + + def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do + with new_info <- %{"banner" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + CommonAPI.update(user) + + 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), + new_info <- %{"banner" => object.data}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + CommonAPI.update(user) + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) + end + end + + def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + with new_info <- %{"background" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + json(conn, %{url: nil}) + end + end + + def update_background(%{assigns: %{user: user}} = conn, params) do + with {:ok, object} <- ActivityPub.upload(params, type: :background), + new_info <- %{"background" => object.data}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) + end + end + + def verify_credentials(%{assigns: %{user: user}} = conn, _) do + chat_token = Phoenix.Token.sign(conn, "user socket", user.id) + + account = + AccountView.render("account.json", %{ + user: user, + for: user, + with_pleroma_settings: true, + with_chat_token: chat_token + }) + + json(conn, account) + end + + def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do + with %Token{app: %App{} = app} <- Repo.preload(token, :app) do + conn + |> put_view(AppView) + |> render("short.json", %{app: app}) + end + end + + def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do + with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id), + true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do + account = AccountView.render("account.json", %{user: user, for: for_user}) + json(conn, account) + else + _e -> render_error(conn, :not_found, "Can't find user") + end + end + + @mastodon_api_level "2.7.2" + + def masto_instance(conn, _params) do + instance = Config.get(:instance) + + response = %{ + uri: Web.base_url(), + title: Keyword.get(instance, :name), + description: Keyword.get(instance, :description), + version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})", + email: Keyword.get(instance, :email), + urls: %{ + streaming_api: Pleroma.Web.Endpoint.websocket_url() + }, + stats: Stats.get_stats(), + thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg", + languages: ["en"], + registrations: Pleroma.Config.get([:instance, :registrations_open]), + # Extra (not present in Mastodon): + max_toot_chars: Keyword.get(instance, :limit), + poll_limits: Keyword.get(instance, :poll_limits) + } + + json(conn, response) + end + + def peers(conn, _params) do + json(conn, Stats.get_peers()) + end + + defp mastodonized_emoji do + Pleroma.Emoji.get_all() + |> Enum.map(fn {shortcode, relative_url, tags} -> + url = to_string(URI.merge(Web.base_url(), relative_url)) + + %{ + "shortcode" => shortcode, + "static_url" => url, + "visible_in_picker" => true, + "url" => url, + "tags" => tags, + # Assuming that a comma is authorized in the category name + "category" => (tags -- ["Custom"]) |> Enum.join(",") + } + end) + end + + def custom_emojis(conn, _params) do + mastodon_emoji = mastodonized_emoji() + json(conn, mastodon_emoji) + end + + def home_timeline(%{assigns: %{user: user}} = conn, params) do + params = + params + |> Map.put("type", ["Create", "Announce"]) + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> Map.put("user", user) + + activities = + [user.ap_id | user.following] + |> ActivityPub.fetch_activities(params) + |> Enum.reverse() + + conn + |> add_link_headers(:home_timeline, activities) + |> put_view(StatusView) + |> render("index.json", %{activities: activities, for: user, as: :activity}) + end + + def public_timeline(%{assigns: %{user: user}} = conn, params) do + local_only = params["local"] in [true, "True", "true", "1"] + + activities = + params + |> Map.put("type", ["Create", "Announce"]) + |> Map.put("local_only", local_only) + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> Map.put("user", user) + |> ActivityPub.fetch_public_activities() + |> Enum.reverse() + + conn + |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only}) + |> put_view(StatusView) + |> render("index.json", %{activities: activities, for: user, as: :activity}) + end + + def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do + with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do + params = + params + |> Map.put("tag", params["tagged"]) + + activities = ActivityPub.fetch_user_activities(user, reading_user, params) + + conn + |> add_link_headers(:user_statuses, activities, params["id"]) + |> put_view(StatusView) + |> render("index.json", %{ + activities: activities, + for: reading_user, + as: :activity + }) + end + end + + def dm_timeline(%{assigns: %{user: user}} = conn, params) do + params = + params + |> Map.put("type", "Create") + |> Map.put("blocking_user", user) + |> Map.put("user", user) + |> Map.put(:visibility, "direct") + + activities = + [user.ap_id] + |> ActivityPub.fetch_activities_query(params) + |> Pagination.fetch_paginated(params) + + conn + |> add_link_headers(:dm_timeline, activities) + |> put_view(StatusView) + |> render("index.json", %{activities: activities, for: user, as: :activity}) + end + + def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + true <- Visibility.visible_for_user?(activity, user) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user}) + end + end + + def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id(id), + activities <- + ActivityPub.fetch_activities_for_context(activity.data["context"], %{ + "blocking_user" => user, + "user" => user, + "exclude_id" => activity.id + }), + grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do + result = %{ + ancestors: + StatusView.render( + "index.json", + for: user, + activities: grouped_activities[true] || [], + as: :activity + ) + |> Enum.reverse(), + # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart + descendants: + StatusView.render( + "index.json", + for: user, + activities: grouped_activities[false] || [], + as: :activity + ) + |> Enum.reverse() + # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart + } + + json(conn, result) + end + end + + def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Object{} = object <- Object.get_by_id(id), + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + true <- Visibility.visible_for_user?(activity, user) do + conn + |> put_view(StatusView) + |> try_render("poll.json", %{object: object, for: user}) + else + error when is_nil(error) or error == false -> + render_error(conn, :not_found, "Record not found") + end + end + + defp get_cached_vote_or_vote(user, object, choices) do + idempotency_key = "polls:#{user.id}:#{object.data["id"]}" + + {_, res} = + Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> + case CommonAPI.vote(user, object, choices) do + {:error, _message} = res -> {:ignore, res} + res -> {:commit, res} + end + end) + + res + end + + def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do + with %Object{} = object <- Object.get_by_id(id), + true <- object.data["type"] == "Question", + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + true <- Visibility.visible_for_user?(activity, user), + {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do + conn + |> put_view(StatusView) + |> try_render("poll.json", %{object: object, for: user}) + else + nil -> + render_error(conn, :not_found, "Record not found") + + false -> + render_error(conn, :not_found, "Record not found") + + {:error, message} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) + end + end + + def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do + with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do + conn + |> add_link_headers(:scheduled_statuses, scheduled_activities) + |> put_view(ScheduledActivityView) + |> render("index.json", %{scheduled_activities: scheduled_activities}) + end + end + + def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do + with %ScheduledActivity{} = scheduled_activity <- + ScheduledActivity.get(user, scheduled_activity_id) do + conn + |> put_view(ScheduledActivityView) + |> render("show.json", %{scheduled_activity: scheduled_activity}) + else + _ -> {:error, :not_found} + end + end + + def update_scheduled_status( + %{assigns: %{user: user}} = conn, + %{"id" => scheduled_activity_id} = params + ) do + with %ScheduledActivity{} = scheduled_activity <- + ScheduledActivity.get(user, scheduled_activity_id), + {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do + conn + |> put_view(ScheduledActivityView) + |> render("show.json", %{scheduled_activity: scheduled_activity}) + else + nil -> {:error, :not_found} + error -> error + end + end + + def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do + with %ScheduledActivity{} = scheduled_activity <- + ScheduledActivity.get(user, scheduled_activity_id), + {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do + conn + |> put_view(ScheduledActivityView) + |> render("show.json", %{scheduled_activity: scheduled_activity}) + else + nil -> {:error, :not_found} + error -> error + end + end + + def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do + params = + params + |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) + + scheduled_at = params["scheduled_at"] + + if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do + with {:ok, scheduled_activity} <- + ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do + conn + |> put_view(ScheduledActivityView) + |> render("show.json", %{scheduled_activity: scheduled_activity}) + end + else + params = Map.drop(params, ["scheduled_at"]) + + case CommonAPI.post(user, params) do + {:error, message} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) + + {:ok, activity} -> + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end + end + end + + def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + json(conn, %{}) + else + _e -> render_error(conn, :forbidden, "Can't delete this post") + end + end + + def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user), + %Activity{} = announce <- Activity.normalize(announce.data) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: announce, for: user, as: :activity}) + end + end + + def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end + end + + def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end + end + + def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end + end + + def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end + end + + def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end + end + + def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %User{} = user <- User.get_cached_by_nickname(user.nickname), + true <- Visibility.visible_for_user?(activity, user), + {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end + end + + def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %User{} = user <- User.get_cached_by_nickname(user.nickname), + true <- Visibility.visible_for_user?(activity, user), + {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end + end + + def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do + activity = Activity.get_by_id(id) + + with {:ok, activity} <- CommonAPI.add_mute(user, activity) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end + end + + def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do + activity = Activity.get_by_id(id) + + with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end + end + + def notifications(%{assigns: %{user: user}} = conn, params) do + notifications = MastodonAPI.get_notifications(user, params) + + conn + |> add_link_headers(:notifications, notifications) + |> put_view(NotificationView) + |> render("index.json", %{notifications: notifications, for: user}) + end + + def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do + with {:ok, notification} <- Notification.get(user, id) do + conn + |> put_view(NotificationView) + |> render("show.json", %{notification: notification, for: user}) + else + {:error, reason} -> + conn + |> put_status(:forbidden) + |> json(%{"error" => reason}) + end + end + + def clear_notifications(%{assigns: %{user: user}} = conn, _params) do + Notification.clear(user) + json(conn, %{}) + end + + def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do + with {:ok, _notif} <- Notification.dismiss(user, id) do + json(conn, %{}) + else + {:error, reason} -> + conn + |> put_status(:forbidden) + |> json(%{"error" => reason}) + end + end + + def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do + Notification.destroy_multiple(user, ids) + json(conn, %{}) + end + + def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do + id = List.wrap(id) + q = from(u in User, where: u.id in ^id) + targets = Repo.all(q) + + conn + |> put_view(AccountView) + |> render("relationships.json", %{user: user, targets: targets}) + end + + # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array. + def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, []) + + def update_media(%{assigns: %{user: user}} = conn, data) do + with %Object{} = object <- Repo.get(Object, data["id"]), + true <- Object.authorize_mutation(object, user), + true <- is_binary(data["description"]), + description <- data["description"] do + new_data = %{object.data | "name" => description} + + {:ok, _} = + object + |> Object.change(%{data: new_data}) + |> Repo.update() + + attachment_data = Map.put(new_data, "id", object.id) + + conn + |> put_view(StatusView) + |> render("attachment.json", %{attachment: attachment_data}) + end + end + + def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do + with {:ok, object} <- + ActivityPub.upload( + file, + actor: User.ap_id(user), + description: Map.get(data, "description") + ) do + attachment_data = Map.put(object.data, "id", object.id) + + conn + |> put_view(StatusView) + |> render("attachment.json", %{attachment: attachment_data}) + end + end + + def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do + with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), + %{} = attachment_data <- Map.put(object.data, "id", object.id), + %{type: type} = rendered <- + StatusView.render("attachment.json", %{attachment: attachment_data}) do + # Reject if not an image + if type == "image" do + # Sure! + # Save to the user's info + info_changeset = User.Info.mascot_update(user.info, rendered) + + user_changeset = + user + |> Changeset.change() + |> Changeset.put_embed(:info, info_changeset) + + {:ok, _user} = User.update_and_set_cache(user_changeset) + + conn + |> json(rendered) + else + render_error(conn, :unsupported_media_type, "mascots can only be images") + end + end + end + + def get_mascot(%{assigns: %{user: user}} = conn, _params) do + mascot = User.get_mascot(user) + + conn + |> json(mascot) + end + + def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do + q = from(u in User, where: u.ap_id in ^likes) + + users = + Repo.all(q) + |> Enum.filter(&(not User.blocks?(user, &1))) + + conn + |> put_view(AccountView) + |> render("accounts.json", %{for: user, users: users, as: :user}) + else + _ -> json(conn, []) + end + end + + def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do + q = from(u in User, where: u.ap_id in ^announces) + + users = + Repo.all(q) + |> Enum.filter(&(not User.blocks?(user, &1))) + + conn + |> put_view(AccountView) + |> render("accounts.json", %{for: user, users: users, as: :user}) + else + _ -> json(conn, []) + end + end + + def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do + local_only = params["local"] in [true, "True", "true", "1"] + + tags = + [params["tag"], params["any"]] + |> List.flatten() + |> Enum.uniq() + |> Enum.filter(& &1) + |> Enum.map(&String.downcase(&1)) + + tag_all = + params["all"] || + [] + |> Enum.map(&String.downcase(&1)) + + tag_reject = + params["none"] || + [] + |> Enum.map(&String.downcase(&1)) + + activities = + params + |> Map.put("type", "Create") + |> Map.put("local_only", local_only) + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> Map.put("user", user) + |> Map.put("tag", tags) + |> Map.put("tag_all", tag_all) + |> Map.put("tag_reject", tag_reject) + |> ActivityPub.fetch_public_activities() + |> Enum.reverse() + + conn + |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only}) + |> put_view(StatusView) + |> render("index.json", %{activities: activities, for: user, as: :activity}) + end + + def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do + with %User{} = user <- User.get_cached_by_id(id), + followers <- MastodonAPI.get_followers(user, params) do + followers = + cond do + for_user && user.id == for_user.id -> followers + user.info.hide_followers -> [] + true -> followers + end + + conn + |> add_link_headers(:followers, followers, user) + |> put_view(AccountView) + |> render("accounts.json", %{for: for_user, users: followers, as: :user}) + end + end + + def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do + with %User{} = user <- User.get_cached_by_id(id), + followers <- MastodonAPI.get_friends(user, params) do + followers = + cond do + for_user && user.id == for_user.id -> followers + user.info.hide_follows -> [] + true -> followers + end + + conn + |> add_link_headers(:following, followers, user) + |> put_view(AccountView) + |> render("accounts.json", %{for: for_user, users: followers, as: :user}) + end + end + + def follow_requests(%{assigns: %{user: followed}} = conn, _params) do + with {:ok, follow_requests} <- User.get_follow_requests(followed) do + conn + |> put_view(AccountView) + |> render("accounts.json", %{for: followed, users: follow_requests, as: :user}) + end + end + + def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do + with %User{} = follower <- User.get_cached_by_id(id), + {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: followed, target: follower}) + else + {:error, message} -> + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end + + def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do + with %User{} = follower <- User.get_cached_by_id(id), + {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: followed, target: follower}) + else + {:error, message} -> + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end + + def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do + with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, + {_, true} <- {:followed, follower.id != followed.id}, + {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: follower, target: followed}) + else + {:followed, _} -> + {:error, :not_found} + + {:error, message} -> + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end + + def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do + with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)}, + {_, true} <- {:followed, follower.id != followed.id}, + {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do + conn + |> put_view(AccountView) + |> render("account.json", %{user: followed, for: follower}) + else + {:followed, _} -> + {:error, :not_found} + + {:error, message} -> + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end + + def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do + with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, + {_, true} <- {:followed, follower.id != followed.id}, + {:ok, follower} <- CommonAPI.unfollow(follower, followed) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: follower, target: followed}) + else + {:followed, _} -> + {:error, :not_found} + + error -> + error + end + end + + def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do + notifications = + if Map.has_key?(params, "notifications"), + do: params["notifications"] in [true, "True", "true", "1"], + else: true + + with %User{} = muted <- User.get_cached_by_id(id), + {:ok, muter} <- User.mute(muter, muted, notifications) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: muter, target: muted}) + else + {:error, message} -> + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end + + def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do + with %User{} = muted <- User.get_cached_by_id(id), + {:ok, muter} <- User.unmute(muter, muted) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: muter, target: muted}) + else + {:error, message} -> + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end + + def mutes(%{assigns: %{user: user}} = conn, _) do + with muted_accounts <- User.muted_users(user) do + res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user) + json(conn, res) + end + end + + def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do + with %User{} = blocked <- User.get_cached_by_id(id), + {:ok, blocker} <- User.block(blocker, blocked), + {:ok, _activity} <- ActivityPub.block(blocker, blocked) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: blocker, target: blocked}) + else + {:error, message} -> + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end + + def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do + with %User{} = blocked <- User.get_cached_by_id(id), + {:ok, blocker} <- User.unblock(blocker, blocked), + {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: blocker, target: blocked}) + else + {:error, message} -> + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end + + def blocks(%{assigns: %{user: user}} = conn, _) do + with blocked_accounts <- User.blocked_users(user) do + res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user) + json(conn, res) + end + end + + def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do + json(conn, info.domain_blocks || []) + end + + def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do + User.block_domain(blocker, domain) + json(conn, %{}) + end + + def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do + User.unblock_domain(blocker, domain) + json(conn, %{}) + end + + def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %User{} = subscription_target <- User.get_cached_by_id(id), + {:ok, subscription_target} = User.subscribe(user, subscription_target) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: user, target: subscription_target}) + else + {:error, message} -> + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end + + def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %User{} = subscription_target <- User.get_cached_by_id(id), + {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: user, target: subscription_target}) + else + {:error, message} -> + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end + + def favourites(%{assigns: %{user: user}} = conn, params) do + params = + params + |> Map.put("type", "Create") + |> Map.put("favorited_by", user.ap_id) + |> Map.put("blocking_user", user) + + activities = + ActivityPub.fetch_activities([], params) + |> Enum.reverse() + + conn + |> add_link_headers(:favourites, activities) + |> put_view(StatusView) + |> render("index.json", %{activities: activities, for: user, as: :activity}) + end + + def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do + with %User{} = user <- User.get_by_id(id), + false <- user.info.hide_favorites do + params = + params + |> Map.put("type", "Create") + |> Map.put("favorited_by", user.ap_id) + |> Map.put("blocking_user", for_user) + + recipients = + if for_user do + [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following] + else + [Pleroma.Constants.as_public()] + end + + activities = + recipients + |> ActivityPub.fetch_activities(params) + |> Enum.reverse() + + conn + |> add_link_headers(:favourites, activities) + |> put_view(StatusView) + |> render("index.json", %{activities: activities, for: for_user, as: :activity}) + else + nil -> {:error, :not_found} + true -> render_error(conn, :forbidden, "Can't get favorites") + end + end + + def bookmarks(%{assigns: %{user: user}} = conn, params) do + user = User.get_cached_by_id(user.id) + + bookmarks = + Bookmark.for_user_query(user.id) + |> Pagination.fetch_paginated(params) + + activities = + bookmarks + |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end) + + conn + |> add_link_headers(:bookmarks, bookmarks) + |> put_view(StatusView) + |> render("index.json", %{activities: activities, for: user, as: :activity}) + end + + def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do + lists = Pleroma.List.get_lists_account_belongs(user, account_id) + res = ListView.render("lists.json", lists: lists) + json(conn, res) + end + + def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do + with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do + params = + params + |> Map.put("type", "Create") + |> Map.put("blocking_user", user) + |> Map.put("user", user) + |> Map.put("muting_user", user) + + # we must filter the following list for the user to avoid leaking statuses the user + # does not actually have permission to see (for more info, peruse security issue #270). + activities = + following + |> Enum.filter(fn x -> x in user.following end) + |> ActivityPub.fetch_activities_bounded(following, params) + |> Enum.reverse() + + conn + |> put_view(StatusView) + |> render("index.json", %{activities: activities, for: user, as: :activity}) + else + _e -> render_error(conn, :forbidden, "Error.") + end + end + + def index(%{assigns: %{user: user}} = conn, _params) do + token = get_session(conn, :oauth_token) + + if user && token do + mastodon_emoji = mastodonized_emoji() + + limit = Config.get([:instance, :limit]) + + accounts = + Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) + + initial_state = + %{ + meta: %{ + streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(), + access_token: token, + locale: "en", + domain: Pleroma.Web.Endpoint.host(), + admin: "1", + me: "#{user.id}", + unfollow_modal: false, + boost_modal: false, + delete_modal: true, + auto_play_gif: false, + display_sensitive_media: false, + reduce_motion: false, + max_toot_chars: limit, + mascot: User.get_mascot(user)["url"] + }, + poll_limits: Config.get([:instance, :poll_limits]), + rights: %{ + delete_others_notice: present?(user.info.is_moderator), + admin: present?(user.info.is_admin) + }, + compose: %{ + me: "#{user.id}", + default_privacy: user.info.default_scope, + default_sensitive: false, + allow_content_types: Config.get([:instance, :allowed_post_formats]) + }, + media_attachments: %{ + accept_content_types: [ + ".jpg", + ".jpeg", + ".png", + ".gif", + ".webm", + ".mp4", + ".m4v", + "image\/jpeg", + "image\/png", + "image\/gif", + "video\/webm", + "video\/mp4" + ] + }, + settings: + user.info.settings || + %{ + onboarded: true, + home: %{ + shows: %{ + reblog: true, + reply: true + } + }, + notifications: %{ + alerts: %{ + follow: true, + favourite: true, + reblog: true, + mention: true + }, + shows: %{ + follow: true, + favourite: true, + reblog: true, + mention: true + }, + sounds: %{ + follow: true, + favourite: true, + reblog: true, + mention: true + } + } + }, + push_subscription: nil, + accounts: accounts, + custom_emojis: mastodon_emoji, + char_limit: limit + } + |> Jason.encode!() + + conn + |> put_layout(false) + |> put_view(MastodonView) + |> render("index.html", %{initial_state: initial_state}) + else + conn + |> put_session(:return_to, conn.request_path) + |> redirect(to: "/web/login") + end + end + + def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do + info_cng = User.Info.mastodon_settings_update(user.info, settings) + + with changeset <- Changeset.change(user), + changeset <- Changeset.put_embed(changeset, :info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + json(conn, %{}) + else + e -> + conn + |> put_status(:internal_server_error) + |> json(%{error: inspect(e)}) + end + end + + def login(%{assigns: %{user: %User{}}} = conn, _params) do + redirect(conn, to: local_mastodon_root_path(conn)) + end + + @doc "Local Mastodon FE login init action" + def login(conn, %{"code" => auth_token}) do + with {:ok, app} <- get_or_make_app(), + %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id), + {:ok, token} <- Token.exchange_token(app, auth) do + conn + |> put_session(:oauth_token, token.token) + |> redirect(to: local_mastodon_root_path(conn)) + end + end + + @doc "Local Mastodon FE callback action" + def login(conn, _) do + with {:ok, app} <- get_or_make_app() do + path = + o_auth_path( + conn, + :authorize, + response_type: "code", + client_id: app.client_id, + redirect_uri: ".", + scope: Enum.join(app.scopes, " ") + ) + + redirect(conn, to: path) + end + end + + defp local_mastodon_root_path(conn) do + case get_session(conn, :return_to) do + nil -> + mastodon_api_path(conn, :index, ["getting-started"]) + + return_to -> + delete_session(conn, :return_to) + return_to + end + end + + defp get_or_make_app do + find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."} + scopes = ["read", "write", "follow", "push"] + + with %App{} = app <- Repo.get_by(App, find_attrs) do + {:ok, app} = + if app.scopes == scopes do + {:ok, app} + else + app + |> Changeset.change(%{scopes: scopes}) + |> Repo.update() + end + + {:ok, app} + else + _e -> + cs = + App.register_changeset( + %App{}, + Map.put(find_attrs, :scopes, scopes) + ) + + Repo.insert(cs) + end + end + + def logout(conn, _) do + conn + |> clear_session + |> redirect(to: "/") + end + + def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do + Logger.debug("Unimplemented, returning unmodified relationship") + + with %User{} = target <- User.get_cached_by_id(id) do + conn + |> put_view(AccountView) + |> render("relationship.json", %{user: user, target: target}) + end + end + + def empty_array(conn, _) do + Logger.debug("Unimplemented, returning an empty array") + json(conn, []) + end + + def empty_object(conn, _) do + Logger.debug("Unimplemented, returning an empty object") + json(conn, %{}) + end + + def get_filters(%{assigns: %{user: user}} = conn, _) do + filters = Filter.get_filters(user) + res = FilterView.render("filters.json", filters: filters) + json(conn, res) + end + + def create_filter( + %{assigns: %{user: user}} = conn, + %{"phrase" => phrase, "context" => context} = params + ) do + query = %Filter{ + user_id: user.id, + phrase: phrase, + context: context, + hide: Map.get(params, "irreversible", false), + whole_word: Map.get(params, "boolean", true) + # expires_at + } + + {:ok, response} = Filter.create(query) + res = FilterView.render("filter.json", filter: response) + json(conn, res) + end + + def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do + filter = Filter.get(filter_id, user) + res = FilterView.render("filter.json", filter: filter) + json(conn, res) + end + + def update_filter( + %{assigns: %{user: user}} = conn, + %{"phrase" => phrase, "context" => context, "id" => filter_id} = params + ) do + query = %Filter{ + user_id: user.id, + filter_id: filter_id, + phrase: phrase, + context: context, + hide: Map.get(params, "irreversible", nil), + whole_word: Map.get(params, "boolean", true) + # expires_at + } + + {:ok, response} = Filter.update(query) + res = FilterView.render("filter.json", filter: response) + json(conn, res) + end + + def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do + query = %Filter{ + user_id: user.id, + filter_id: filter_id + } + + {:ok, _} = Filter.delete(query) + json(conn, %{}) + end + + def suggestions(%{assigns: %{user: user}} = conn, _) do + suggestions = Config.get(:suggestions) + + if Keyword.get(suggestions, :enabled, false) do + api = Keyword.get(suggestions, :third_party_engine, "") + timeout = Keyword.get(suggestions, :timeout, 5000) + limit = Keyword.get(suggestions, :limit, 23) + + host = Config.get([Pleroma.Web.Endpoint, :url, :host]) + + user = user.nickname + + url = + api + |> String.replace("{{host}}", host) + |> String.replace("{{user}}", user) + + with {:ok, %{status: 200, body: body}} <- + HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]), + {:ok, data} <- Jason.decode(body) do + data = + data + |> Enum.slice(0, limit) + |> Enum.map(fn x -> + x + |> Map.put("id", fetch_suggestion_id(x)) + |> Map.put("avatar", MediaProxy.url(x["avatar"])) + |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"])) + end) + + json(conn, data) + else + e -> + Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}") + end + else + json(conn, []) + end + end + + defp fetch_suggestion_id(attrs) do + case User.get_or_fetch(attrs["acct"]) do + {:ok, %User{id: id}} -> id + _ -> 0 + end + end + + def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do + with %Activity{} = activity <- Activity.get_by_id(status_id), + true <- Visibility.visible_for_user?(activity, user) do + data = + StatusView.render( + "card.json", + Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) + ) + + json(conn, data) + else + _e -> + %{} + end + end + + def reports(%{assigns: %{user: user}} = conn, params) do + case CommonAPI.report(user, params) do + {:ok, activity} -> + conn + |> put_view(ReportView) + |> try_render("report.json", %{activity: activity}) + + {:error, err} -> + conn + |> put_status(:bad_request) + |> json(%{error: err}) + end + end + + def account_register( + %{assigns: %{app: app}} = conn, + %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params + ) do + params = + params + |> Map.take([ + "email", + "captcha_solution", + "captcha_token", + "captcha_answer_data", + "token", + "password" + ]) + |> Map.put("nickname", nickname) + |> Map.put("fullname", params["fullname"] || nickname) + |> Map.put("bio", params["bio"] || "") + |> Map.put("confirm", params["password"]) + + with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), + {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do + json(conn, %{ + token_type: "Bearer", + access_token: token.token, + scope: app.scopes, + created_at: Token.Utils.format_created_at(token) + }) + else + {:error, errors} -> + conn + |> put_status(:bad_request) + |> json(errors) + end + end + + def account_register(%{assigns: %{app: _app}} = conn, _params) do + render_error(conn, :bad_request, "Missing parameters") + end + + def account_register(conn, _) do + render_error(conn, :forbidden, "Invalid credentials") + end + + def conversations(%{assigns: %{user: user}} = conn, params) do + participations = Participation.for_user_with_last_activity_id(user, params) + + conversations = + Enum.map(participations, fn participation -> + ConversationView.render("participation.json", %{participation: participation, for: user}) + end) + + conn + |> add_link_headers(:conversations, participations) + |> json(conversations) + end + + def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do + with %Participation{} = participation <- + Repo.get_by(Participation, id: participation_id, user_id: user.id), + {:ok, participation} <- Participation.mark_as_read(participation) do + participation_view = + ConversationView.render("participation.json", %{participation: participation, for: user}) + + conn + |> json(participation_view) + end + end + + def password_reset(conn, params) do + nickname_or_email = params["email"] || params["nickname"] + + with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do + conn + |> put_status(:no_content) + |> json("") + else + {:error, "unknown user"} -> + send_resp(conn, :not_found, "") + + {:error, _} -> + send_resp(conn, :bad_request, "") + end + end + + def account_confirmation_resend(conn, params) do + nickname_or_email = params["email"] || params["nickname"] + + with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email), + {:ok, _} <- User.try_send_confirmation_email(user) do + conn + |> json_response(:no_content, "") + end + end + + def try_render(conn, target, params) + when is_binary(target) do + case render(conn, target, params) do + nil -> render_error(conn, :not_implemented, "Can't display this activity") + res -> res + end + end + + def try_render(conn, _, _) do + render_error(conn, :not_implemented, "Can't display this activity") + end + + defp present?(nil), do: false + defp present?(false), do: false + defp present?(_), do: true +end diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex new file mode 100644 index 000000000..9072aa7a4 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -0,0 +1,120 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SearchController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + 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 + + require Logger + plug(RateLimiter, :search when action in [:search, :search2, :account_search]) + + def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = User.search(query, search_options(params, user)) + res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) + + json(conn, res) + end + + def search2(conn, params), do: do_search(:v2, conn, params) + def search(conn, params), do: do_search(:v1, conn, params) + + defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do + options = search_options(params, user) + timeout = Keyword.get(Repo.config(), :timeout, 15_000) + default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} + + result = + default_values + |> Enum.map(fn {resource, default_value} -> + if params["type"] == nil or params["type"] == resource do + {resource, fn -> resource_search(version, resource, query, options) end} + else + {resource, fn -> default_value end} + end + end) + |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end, + timeout: timeout, + on_timeout: :kill_task + ) + |> Enum.reduce(default_values, fn + {:ok, {resource, result}}, acc -> + Map.put(acc, resource, result) + + _error, acc -> + acc + end) + + json(conn, result) + end + + defp search_options(params, user) do + [ + resolve: params["resolve"] == "true", + following: params["following"] == "true", + limit: ControllerHelper.fetch_integer_param(params, "limit"), + offset: ControllerHelper.fetch_integer_param(params, "offset"), + type: params["type"], + author: get_author(params), + for_user: user + ] + |> Enum.filter(&elem(&1, 1)) + end + + defp resource_search(_, "accounts", query, options) do + accounts = with_fallback(fn -> User.search(query, options) end) + AccountView.render("accounts.json", users: accounts, for: options[:for_user], as: :user) + 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) + end + + defp resource_search(:v2, "hashtags", query, _options) do + tags_path = Web.base_url() <> "/tag/" + + query + |> prepare_tags() + |> Enum.map(fn tag -> + tag = String.trim_leading(tag, "#") + %{name: tag, url: tags_path <> tag} + end) + end + + defp resource_search(:v1, "hashtags", query, _options) do + query + |> prepare_tags() + |> Enum.map(fn tag -> String.trim_leading(tag, "#") end) + end + + defp prepare_tags(query) do + query + |> String.split() + |> Enum.uniq() + |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) + end + + defp with_fallback(f, fallback \\ []) do + try do + f.() + rescue + error -> + Logger.error("#{__MODULE__} search error: #{inspect(error)}") + fallback + end + end + + defp get_author(%{"account_id" => account_id}) when is_binary(account_id), + do: User.get_cached_by_id(account_id) + + defp get_author(_params), do: nil +end diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex new file mode 100644 index 000000000..e2b17aab1 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SubscriptionController do + @moduledoc "The module represents functions to manage user subscriptions." + use Pleroma.Web, :controller + + alias Pleroma.Web.Push + alias Pleroma.Web.Push.Subscription + alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View + + action_fallback(:errors) + + # Creates PushSubscription + # POST /api/v1/push/subscription + # + def create(%{assigns: %{user: user, token: token}} = conn, params) do + with true <- Push.enabled(), + {:ok, _} <- Subscription.delete_if_exists(user, token), + {:ok, subscription} <- Subscription.create(user, token, params) do + view = View.render("push_subscription.json", subscription: subscription) + json(conn, view) + end + end + + # Gets PushSubscription + # GET /api/v1/push/subscription + # + def get(%{assigns: %{user: user, token: token}} = conn, _params) do + with true <- Push.enabled(), + {:ok, subscription} <- Subscription.get(user, token) do + view = View.render("push_subscription.json", subscription: subscription) + json(conn, view) + end + end + + # Updates PushSubscription + # PUT /api/v1/push/subscription + # + def update(%{assigns: %{user: user, token: token}} = conn, params) do + with true <- Push.enabled(), + {:ok, subscription} <- Subscription.update(user, token, params) do + view = View.render("push_subscription.json", subscription: subscription) + json(conn, view) + end + end + + # Deletes PushSubscription + # DELETE /api/v1/push/subscription + # + def delete(%{assigns: %{user: user, token: token}} = conn, _params) do + with true <- Push.enabled(), + {:ok, _response} <- Subscription.delete(user, token), + do: json(conn, %{}) + end + + # fallback action + # + def errors(conn, {:error, :not_found}) do + conn + |> put_status(:not_found) + |> json(dgettext("errors", "Not found")) + end + + def errors(conn, _) do + Pleroma.Web.MastodonAPI.FallbackController.call(conn, nil) + end +end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex deleted file mode 100644 index 31b0aaca0..000000000 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ /dev/null @@ -1,1700 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do - use Pleroma.Web, :controller - - import Pleroma.Web.ControllerHelper, - only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3] - - alias Ecto.Changeset - alias Pleroma.Activity - alias Pleroma.Bookmark - alias Pleroma.Config - alias Pleroma.Conversation.Participation - alias Pleroma.Filter - alias Pleroma.Formatter - alias Pleroma.HTTP - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.Pagination - alias Pleroma.Plugs.RateLimiter - alias Pleroma.Repo - alias Pleroma.ScheduledActivity - alias Pleroma.Stats - alias Pleroma.User - alias Pleroma.Web - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.MastodonAPI.AppView - alias Pleroma.Web.MastodonAPI.ConversationView - alias Pleroma.Web.MastodonAPI.FilterView - alias Pleroma.Web.MastodonAPI.ListView - alias Pleroma.Web.MastodonAPI.MastodonAPI - alias Pleroma.Web.MastodonAPI.MastodonView - alias Pleroma.Web.MastodonAPI.NotificationView - alias Pleroma.Web.MastodonAPI.ReportView - alias Pleroma.Web.MastodonAPI.ScheduledActivityView - alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.MediaProxy - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.Scopes - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.TwitterAPI.TwitterAPI - - alias Pleroma.Web.ControllerHelper - import Ecto.Query - - require Logger - require Pleroma.Constants - - @rate_limited_relations_actions ~w(follow unfollow)a - - @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status - post_status delete_status)a - - plug( - RateLimiter, - {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]} - when action in ~w(reblog_status unreblog_status)a - ) - - plug( - RateLimiter, - {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} - when action in ~w(fav_status unfav_status)a - ) - - plug( - RateLimiter, - {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions - ) - - plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions) - plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions) - plug(RateLimiter, :app_account_creation when action == :account_register) - plug(RateLimiter, :search when action in [:search, :search2, :account_search]) - plug(RateLimiter, :password_reset when action == :password_reset) - plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend) - - @local_mastodon_name "Mastodon-Local" - - action_fallback(Pleroma.Web.MastodonAPI.FallbackController) - - def create_app(conn, params) do - scopes = Scopes.fetch_scopes(params, ["read"]) - - app_attrs = - params - |> Map.drop(["scope", "scopes"]) - |> Map.put("scopes", scopes) - - with cs <- App.register_changeset(%App{}, app_attrs), - false <- cs.changes[:client_name] == @local_mastodon_name, - {:ok, app} <- Repo.insert(cs) do - conn - |> put_view(AppView) - |> render("show.json", %{app: app}) - end - end - - defp add_if_present( - map, - params, - params_field, - map_field, - value_function \\ fn x -> {:ok, x} end - ) do - if Map.has_key?(params, params_field) do - case value_function.(params[params_field]) do - {:ok, new_value} -> Map.put(map, map_field, new_value) - :error -> map - end - else - map - end - end - - def update_credentials(%{assigns: %{user: user}} = conn, params) do - original_user = user - - user_params = - %{} - |> add_if_present(params, "display_name", :name) - |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end) - |> add_if_present(params, "avatar", :avatar, fn value -> - with %Plug.Upload{} <- value, - {:ok, object} <- ActivityPub.upload(value, type: :avatar) do - {:ok, object.data} - else - _ -> :error - end - end) - - emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") - - user_info_emojis = - user.info - |> Map.get(:emoji, []) - |> Enum.concat(Formatter.get_emoji_map(emojis_text)) - |> Enum.dedup() - - info_params = - [ - :no_rich_text, - :locked, - :hide_followers, - :hide_follows, - :hide_favorites, - :show_role, - :skip_thread_containment - ] - |> Enum.reduce(%{}, fn key, acc -> - add_if_present(acc, params, to_string(key), key, fn value -> - {:ok, ControllerHelper.truthy_param?(value)} - end) - end) - |> add_if_present(params, "default_scope", :default_scope) - |> add_if_present(params, "fields", :fields, fn fields -> - fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) - - {:ok, fields} - end) - |> add_if_present(params, "fields", :raw_fields) - |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> - {:ok, Map.merge(user.info.pleroma_settings_store, value)} - end) - |> add_if_present(params, "header", :banner, fn value -> - with %Plug.Upload{} <- value, - {:ok, object} <- ActivityPub.upload(value, type: :banner) do - {:ok, object.data} - else - _ -> :error - end - end) - |> add_if_present(params, "pleroma_background_image", :background, fn value -> - with %Plug.Upload{} <- value, - {:ok, object} <- ActivityPub.upload(value, type: :background) do - {:ok, object.data} - else - _ -> :error - end - end) - |> Map.put(:emoji, user_info_emojis) - - info_cng = User.Info.profile_update(user.info, info_params) - - with changeset <- User.update_changeset(user, user_params), - changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng), - {:ok, user} <- User.update_and_set_cache(changeset) do - if original_user != user do - CommonAPI.update(user) - end - - json( - conn, - AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) - ) - else - _e -> render_error(conn, :forbidden, "Invalid request") - end - end - - def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do - change = Changeset.change(user, %{avatar: nil}) - {:ok, user} = User.update_and_set_cache(change) - CommonAPI.update(user) - - json(conn, %{url: nil}) - end - - def update_avatar(%{assigns: %{user: user}} = conn, params) do - {:ok, object} = ActivityPub.upload(params, type: :avatar) - change = Changeset.change(user, %{avatar: object.data}) - {:ok, user} = User.update_and_set_cache(change) - CommonAPI.update(user) - %{"url" => [%{"href" => href} | _]} = object.data - - json(conn, %{url: href}) - end - - def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do - with new_info <- %{"banner" => %{}}, - info_cng <- User.Info.profile_update(user.info, new_info), - changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), - {:ok, user} <- User.update_and_set_cache(changeset) do - CommonAPI.update(user) - - 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), - new_info <- %{"banner" => object.data}, - info_cng <- User.Info.profile_update(user.info, new_info), - changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), - {:ok, user} <- User.update_and_set_cache(changeset) do - CommonAPI.update(user) - %{"url" => [%{"href" => href} | _]} = object.data - - json(conn, %{url: href}) - end - end - - def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do - with new_info <- %{"background" => %{}}, - info_cng <- User.Info.profile_update(user.info, new_info), - changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), - {:ok, _user} <- User.update_and_set_cache(changeset) do - json(conn, %{url: nil}) - end - end - - def update_background(%{assigns: %{user: user}} = conn, params) do - with {:ok, object} <- ActivityPub.upload(params, type: :background), - new_info <- %{"background" => object.data}, - info_cng <- User.Info.profile_update(user.info, new_info), - changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), - {:ok, _user} <- User.update_and_set_cache(changeset) do - %{"url" => [%{"href" => href} | _]} = object.data - - json(conn, %{url: href}) - end - end - - def verify_credentials(%{assigns: %{user: user}} = conn, _) do - chat_token = Phoenix.Token.sign(conn, "user socket", user.id) - - account = - AccountView.render("account.json", %{ - user: user, - for: user, - with_pleroma_settings: true, - with_chat_token: chat_token - }) - - json(conn, account) - end - - def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do - with %Token{app: %App{} = app} <- Repo.preload(token, :app) do - conn - |> put_view(AppView) - |> render("short.json", %{app: app}) - end - end - - def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do - with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id), - true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do - account = AccountView.render("account.json", %{user: user, for: for_user}) - json(conn, account) - else - _e -> render_error(conn, :not_found, "Can't find user") - end - end - - @mastodon_api_level "2.7.2" - - def masto_instance(conn, _params) do - instance = Config.get(:instance) - - response = %{ - uri: Web.base_url(), - title: Keyword.get(instance, :name), - description: Keyword.get(instance, :description), - version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})", - email: Keyword.get(instance, :email), - urls: %{ - streaming_api: Pleroma.Web.Endpoint.websocket_url() - }, - stats: Stats.get_stats(), - thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg", - languages: ["en"], - registrations: Pleroma.Config.get([:instance, :registrations_open]), - # Extra (not present in Mastodon): - max_toot_chars: Keyword.get(instance, :limit), - poll_limits: Keyword.get(instance, :poll_limits) - } - - json(conn, response) - end - - def peers(conn, _params) do - json(conn, Stats.get_peers()) - end - - defp mastodonized_emoji do - Pleroma.Emoji.get_all() - |> Enum.map(fn {shortcode, relative_url, tags} -> - url = to_string(URI.merge(Web.base_url(), relative_url)) - - %{ - "shortcode" => shortcode, - "static_url" => url, - "visible_in_picker" => true, - "url" => url, - "tags" => tags, - # Assuming that a comma is authorized in the category name - "category" => (tags -- ["Custom"]) |> Enum.join(",") - } - end) - end - - def custom_emojis(conn, _params) do - mastodon_emoji = mastodonized_emoji() - json(conn, mastodon_emoji) - end - - def home_timeline(%{assigns: %{user: user}} = conn, params) do - params = - params - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - - activities = - [user.ap_id | user.following] - |> ActivityPub.fetch_activities(params) - |> Enum.reverse() - - conn - |> add_link_headers(:home_timeline, activities) - |> put_view(StatusView) - |> render("index.json", %{activities: activities, for: user, as: :activity}) - end - - def public_timeline(%{assigns: %{user: user}} = conn, params) do - local_only = params["local"] in [true, "True", "true", "1"] - - activities = - params - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> ActivityPub.fetch_public_activities() - |> Enum.reverse() - - conn - |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only}) - |> put_view(StatusView) - |> render("index.json", %{activities: activities, for: user, as: :activity}) - end - - def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do - with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do - params = - params - |> Map.put("tag", params["tagged"]) - - activities = ActivityPub.fetch_user_activities(user, reading_user, params) - - conn - |> add_link_headers(:user_statuses, activities, params["id"]) - |> put_view(StatusView) - |> render("index.json", %{ - activities: activities, - for: reading_user, - as: :activity - }) - end - end - - def dm_timeline(%{assigns: %{user: user}} = conn, params) do - params = - params - |> Map.put("type", "Create") - |> Map.put("blocking_user", user) - |> Map.put("user", user) - |> Map.put(:visibility, "direct") - - activities = - [user.ap_id] - |> ActivityPub.fetch_activities_query(params) - |> Pagination.fetch_paginated(params) - - conn - |> add_link_headers(:dm_timeline, activities) - |> put_view(StatusView) - |> render("index.json", %{activities: activities, for: user, as: :activity}) - end - - def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Activity.get_by_id_with_object(id), - true <- Visibility.visible_for_user?(activity, user) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user}) - end - end - - def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Activity.get_by_id(id), - activities <- - ActivityPub.fetch_activities_for_context(activity.data["context"], %{ - "blocking_user" => user, - "user" => user, - "exclude_id" => activity.id - }), - grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do - result = %{ - ancestors: - StatusView.render( - "index.json", - for: user, - activities: grouped_activities[true] || [], - as: :activity - ) - |> Enum.reverse(), - # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart - descendants: - StatusView.render( - "index.json", - for: user, - activities: grouped_activities[false] || [], - as: :activity - ) - |> Enum.reverse() - # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart - } - - json(conn, result) - end - end - - def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Object{} = object <- Object.get_by_id(id), - %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), - true <- Visibility.visible_for_user?(activity, user) do - conn - |> put_view(StatusView) - |> try_render("poll.json", %{object: object, for: user}) - else - error when is_nil(error) or error == false -> - render_error(conn, :not_found, "Record not found") - end - end - - defp get_cached_vote_or_vote(user, object, choices) do - idempotency_key = "polls:#{user.id}:#{object.data["id"]}" - - {_, res} = - Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> - case CommonAPI.vote(user, object, choices) do - {:error, _message} = res -> {:ignore, res} - res -> {:commit, res} - end - end) - - res - end - - def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do - with %Object{} = object <- Object.get_by_id(id), - true <- object.data["type"] == "Question", - %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), - true <- Visibility.visible_for_user?(activity, user), - {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do - conn - |> put_view(StatusView) - |> try_render("poll.json", %{object: object, for: user}) - else - nil -> - render_error(conn, :not_found, "Record not found") - - false -> - render_error(conn, :not_found, "Record not found") - - {:error, message} -> - conn - |> put_status(:unprocessable_entity) - |> json(%{error: message}) - end - end - - def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do - with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do - conn - |> add_link_headers(:scheduled_statuses, scheduled_activities) - |> put_view(ScheduledActivityView) - |> render("index.json", %{scheduled_activities: scheduled_activities}) - end - end - - def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do - with %ScheduledActivity{} = scheduled_activity <- - ScheduledActivity.get(user, scheduled_activity_id) do - conn - |> put_view(ScheduledActivityView) - |> render("show.json", %{scheduled_activity: scheduled_activity}) - else - _ -> {:error, :not_found} - end - end - - def update_scheduled_status( - %{assigns: %{user: user}} = conn, - %{"id" => scheduled_activity_id} = params - ) do - with %ScheduledActivity{} = scheduled_activity <- - ScheduledActivity.get(user, scheduled_activity_id), - {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do - conn - |> put_view(ScheduledActivityView) - |> render("show.json", %{scheduled_activity: scheduled_activity}) - else - nil -> {:error, :not_found} - error -> error - end - end - - def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do - with %ScheduledActivity{} = scheduled_activity <- - ScheduledActivity.get(user, scheduled_activity_id), - {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do - conn - |> put_view(ScheduledActivityView) - |> render("show.json", %{scheduled_activity: scheduled_activity}) - else - nil -> {:error, :not_found} - error -> error - end - end - - def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do - params = - params - |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) - - scheduled_at = params["scheduled_at"] - - if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do - with {:ok, scheduled_activity} <- - ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do - conn - |> put_view(ScheduledActivityView) - |> render("show.json", %{scheduled_activity: scheduled_activity}) - end - else - params = Map.drop(params, ["scheduled_at"]) - - case CommonAPI.post(user, params) do - {:error, message} -> - conn - |> put_status(:unprocessable_entity) - |> json(%{error: message}) - - {:ok, activity} -> - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - end - end - end - - def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do - json(conn, %{}) - else - _e -> render_error(conn, :forbidden, "Can't delete this post") - end - end - - def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do - with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user), - %Activity{} = announce <- Activity.normalize(announce.data) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: announce, for: user, as: :activity}) - end - end - - def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do - with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), - %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - end - end - - def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do - with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user), - %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - end - end - - def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do - with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user), - %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - end - end - - def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do - with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - end - end - - def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do - with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - end - end - - def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Activity.get_by_id_with_object(id), - %User{} = user <- User.get_cached_by_nickname(user.nickname), - true <- Visibility.visible_for_user?(activity, user), - {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - end - end - - def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Activity.get_by_id_with_object(id), - %User{} = user <- User.get_cached_by_nickname(user.nickname), - true <- Visibility.visible_for_user?(activity, user), - {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - end - end - - def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do - activity = Activity.get_by_id(id) - - with {:ok, activity} <- CommonAPI.add_mute(user, activity) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - end - end - - def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do - activity = Activity.get_by_id(id) - - with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - end - end - - def notifications(%{assigns: %{user: user}} = conn, params) do - notifications = MastodonAPI.get_notifications(user, params) - - conn - |> add_link_headers(:notifications, notifications) - |> put_view(NotificationView) - |> render("index.json", %{notifications: notifications, for: user}) - end - - def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do - with {:ok, notification} <- Notification.get(user, id) do - conn - |> put_view(NotificationView) - |> render("show.json", %{notification: notification, for: user}) - else - {:error, reason} -> - conn - |> put_status(:forbidden) - |> json(%{"error" => reason}) - end - end - - def clear_notifications(%{assigns: %{user: user}} = conn, _params) do - Notification.clear(user) - json(conn, %{}) - end - - def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do - with {:ok, _notif} <- Notification.dismiss(user, id) do - json(conn, %{}) - else - {:error, reason} -> - conn - |> put_status(:forbidden) - |> json(%{"error" => reason}) - end - end - - def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do - Notification.destroy_multiple(user, ids) - json(conn, %{}) - end - - def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do - id = List.wrap(id) - q = from(u in User, where: u.id in ^id) - targets = Repo.all(q) - - conn - |> put_view(AccountView) - |> render("relationships.json", %{user: user, targets: targets}) - end - - # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array. - def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, []) - - def update_media(%{assigns: %{user: user}} = conn, data) do - with %Object{} = object <- Repo.get(Object, data["id"]), - true <- Object.authorize_mutation(object, user), - true <- is_binary(data["description"]), - description <- data["description"] do - new_data = %{object.data | "name" => description} - - {:ok, _} = - object - |> Object.change(%{data: new_data}) - |> Repo.update() - - attachment_data = Map.put(new_data, "id", object.id) - - conn - |> put_view(StatusView) - |> render("attachment.json", %{attachment: attachment_data}) - end - end - - def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do - with {:ok, object} <- - ActivityPub.upload( - file, - actor: User.ap_id(user), - description: Map.get(data, "description") - ) do - attachment_data = Map.put(object.data, "id", object.id) - - conn - |> put_view(StatusView) - |> render("attachment.json", %{attachment: attachment_data}) - end - end - - def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do - with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), - %{} = attachment_data <- Map.put(object.data, "id", object.id), - %{type: type} = rendered <- - StatusView.render("attachment.json", %{attachment: attachment_data}) do - # Reject if not an image - if type == "image" do - # Sure! - # Save to the user's info - info_changeset = User.Info.mascot_update(user.info, rendered) - - user_changeset = - user - |> Ecto.Changeset.change() - |> Ecto.Changeset.put_embed(:info, info_changeset) - - {:ok, _user} = User.update_and_set_cache(user_changeset) - - conn - |> json(rendered) - else - render_error(conn, :unsupported_media_type, "mascots can only be images") - end - end - end - - def get_mascot(%{assigns: %{user: user}} = conn, _params) do - mascot = User.get_mascot(user) - - conn - |> json(mascot) - end - - def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Activity.get_by_id_with_object(id), - %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do - q = from(u in User, where: u.ap_id in ^likes) - - users = - Repo.all(q) - |> Enum.filter(&(not User.blocks?(user, &1))) - - conn - |> put_view(AccountView) - |> render("accounts.json", %{for: user, users: users, as: :user}) - else - _ -> json(conn, []) - end - end - - def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Activity.get_by_id_with_object(id), - %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do - q = from(u in User, where: u.ap_id in ^announces) - - users = - Repo.all(q) - |> Enum.filter(&(not User.blocks?(user, &1))) - - conn - |> put_view(AccountView) - |> render("accounts.json", %{for: user, users: users, as: :user}) - else - _ -> json(conn, []) - end - end - - def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do - local_only = params["local"] in [true, "True", "true", "1"] - - tags = - [params["tag"], params["any"]] - |> List.flatten() - |> Enum.uniq() - |> Enum.filter(& &1) - |> Enum.map(&String.downcase(&1)) - - tag_all = - params["all"] || - [] - |> Enum.map(&String.downcase(&1)) - - tag_reject = - params["none"] || - [] - |> Enum.map(&String.downcase(&1)) - - activities = - params - |> Map.put("type", "Create") - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("tag", tags) - |> Map.put("tag_all", tag_all) - |> Map.put("tag_reject", tag_reject) - |> ActivityPub.fetch_public_activities() - |> Enum.reverse() - - conn - |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only}) - |> put_view(StatusView) - |> render("index.json", %{activities: activities, for: user, as: :activity}) - end - - def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do - with %User{} = user <- User.get_cached_by_id(id), - followers <- MastodonAPI.get_followers(user, params) do - followers = - cond do - for_user && user.id == for_user.id -> followers - user.info.hide_followers -> [] - true -> followers - end - - conn - |> add_link_headers(:followers, followers, user) - |> put_view(AccountView) - |> render("accounts.json", %{for: for_user, users: followers, as: :user}) - end - end - - def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do - with %User{} = user <- User.get_cached_by_id(id), - followers <- MastodonAPI.get_friends(user, params) do - followers = - cond do - for_user && user.id == for_user.id -> followers - user.info.hide_follows -> [] - true -> followers - end - - conn - |> add_link_headers(:following, followers, user) - |> put_view(AccountView) - |> render("accounts.json", %{for: for_user, users: followers, as: :user}) - end - end - - def follow_requests(%{assigns: %{user: followed}} = conn, _params) do - with {:ok, follow_requests} <- User.get_follow_requests(followed) do - conn - |> put_view(AccountView) - |> render("accounts.json", %{for: followed, users: follow_requests, as: :user}) - end - end - - def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do - with %User{} = follower <- User.get_cached_by_id(id), - {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: followed, target: follower}) - else - {:error, message} -> - conn - |> put_status(:forbidden) - |> json(%{error: message}) - end - end - - def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do - with %User{} = follower <- User.get_cached_by_id(id), - {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: followed, target: follower}) - else - {:error, message} -> - conn - |> put_status(:forbidden) - |> json(%{error: message}) - end - end - - def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do - with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, - {_, true} <- {:followed, follower.id != followed.id}, - {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: follower, target: followed}) - else - {:followed, _} -> - {:error, :not_found} - - {:error, message} -> - conn - |> put_status(:forbidden) - |> json(%{error: message}) - end - end - - def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do - with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)}, - {_, true} <- {:followed, follower.id != followed.id}, - {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do - conn - |> put_view(AccountView) - |> render("account.json", %{user: followed, for: follower}) - else - {:followed, _} -> - {:error, :not_found} - - {:error, message} -> - conn - |> put_status(:forbidden) - |> json(%{error: message}) - end - end - - def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do - with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, - {_, true} <- {:followed, follower.id != followed.id}, - {:ok, follower} <- CommonAPI.unfollow(follower, followed) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: follower, target: followed}) - else - {:followed, _} -> - {:error, :not_found} - - error -> - error - end - end - - def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do - notifications = - if Map.has_key?(params, "notifications"), - do: params["notifications"] in [true, "True", "true", "1"], - else: true - - with %User{} = muted <- User.get_cached_by_id(id), - {:ok, muter} <- User.mute(muter, muted, notifications) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: muter, target: muted}) - else - {:error, message} -> - conn - |> put_status(:forbidden) - |> json(%{error: message}) - end - end - - def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do - with %User{} = muted <- User.get_cached_by_id(id), - {:ok, muter} <- User.unmute(muter, muted) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: muter, target: muted}) - else - {:error, message} -> - conn - |> put_status(:forbidden) - |> json(%{error: message}) - end - end - - def mutes(%{assigns: %{user: user}} = conn, _) do - with muted_accounts <- User.muted_users(user) do - res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user) - json(conn, res) - end - end - - def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do - with %User{} = blocked <- User.get_cached_by_id(id), - {:ok, blocker} <- User.block(blocker, blocked), - {:ok, _activity} <- ActivityPub.block(blocker, blocked) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: blocker, target: blocked}) - else - {:error, message} -> - conn - |> put_status(:forbidden) - |> json(%{error: message}) - end - end - - def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do - with %User{} = blocked <- User.get_cached_by_id(id), - {:ok, blocker} <- User.unblock(blocker, blocked), - {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: blocker, target: blocked}) - else - {:error, message} -> - conn - |> put_status(:forbidden) - |> json(%{error: message}) - end - end - - def blocks(%{assigns: %{user: user}} = conn, _) do - with blocked_accounts <- User.blocked_users(user) do - res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user) - json(conn, res) - end - end - - def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do - json(conn, info.domain_blocks || []) - end - - def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do - User.block_domain(blocker, domain) - json(conn, %{}) - end - - def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do - User.unblock_domain(blocker, domain) - json(conn, %{}) - end - - def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %User{} = subscription_target <- User.get_cached_by_id(id), - {:ok, subscription_target} = User.subscribe(user, subscription_target) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: user, target: subscription_target}) - else - {:error, message} -> - conn - |> put_status(:forbidden) - |> json(%{error: message}) - end - end - - def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %User{} = subscription_target <- User.get_cached_by_id(id), - {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: user, target: subscription_target}) - else - {:error, message} -> - conn - |> put_status(:forbidden) - |> json(%{error: message}) - end - end - - def favourites(%{assigns: %{user: user}} = conn, params) do - params = - params - |> Map.put("type", "Create") - |> Map.put("favorited_by", user.ap_id) - |> Map.put("blocking_user", user) - - activities = - ActivityPub.fetch_activities([], params) - |> Enum.reverse() - - conn - |> add_link_headers(:favourites, activities) - |> put_view(StatusView) - |> render("index.json", %{activities: activities, for: user, as: :activity}) - end - - def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do - with %User{} = user <- User.get_by_id(id), - false <- user.info.hide_favorites do - params = - params - |> Map.put("type", "Create") - |> Map.put("favorited_by", user.ap_id) - |> Map.put("blocking_user", for_user) - - recipients = - if for_user do - [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following] - else - [Pleroma.Constants.as_public()] - end - - activities = - recipients - |> ActivityPub.fetch_activities(params) - |> Enum.reverse() - - conn - |> add_link_headers(:favourites, activities) - |> put_view(StatusView) - |> render("index.json", %{activities: activities, for: for_user, as: :activity}) - else - nil -> {:error, :not_found} - true -> render_error(conn, :forbidden, "Can't get favorites") - end - end - - def bookmarks(%{assigns: %{user: user}} = conn, params) do - user = User.get_cached_by_id(user.id) - - bookmarks = - Bookmark.for_user_query(user.id) - |> Pagination.fetch_paginated(params) - - activities = - bookmarks - |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end) - - conn - |> add_link_headers(:bookmarks, bookmarks) - |> put_view(StatusView) - |> render("index.json", %{activities: activities, for: user, as: :activity}) - end - - def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do - lists = Pleroma.List.get_lists_account_belongs(user, account_id) - res = ListView.render("lists.json", lists: lists) - json(conn, res) - end - - def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do - with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do - params = - params - |> Map.put("type", "Create") - |> Map.put("blocking_user", user) - |> Map.put("user", user) - |> Map.put("muting_user", user) - - # we must filter the following list for the user to avoid leaking statuses the user - # does not actually have permission to see (for more info, peruse security issue #270). - activities = - following - |> Enum.filter(fn x -> x in user.following end) - |> ActivityPub.fetch_activities_bounded(following, params) - |> Enum.reverse() - - conn - |> put_view(StatusView) - |> render("index.json", %{activities: activities, for: user, as: :activity}) - else - _e -> render_error(conn, :forbidden, "Error.") - end - end - - def index(%{assigns: %{user: user}} = conn, _params) do - token = get_session(conn, :oauth_token) - - if user && token do - mastodon_emoji = mastodonized_emoji() - - limit = Config.get([:instance, :limit]) - - accounts = - Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) - - initial_state = - %{ - meta: %{ - streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(), - access_token: token, - locale: "en", - domain: Pleroma.Web.Endpoint.host(), - admin: "1", - me: "#{user.id}", - unfollow_modal: false, - boost_modal: false, - delete_modal: true, - auto_play_gif: false, - display_sensitive_media: false, - reduce_motion: false, - max_toot_chars: limit, - mascot: User.get_mascot(user)["url"] - }, - poll_limits: Config.get([:instance, :poll_limits]), - rights: %{ - delete_others_notice: present?(user.info.is_moderator), - admin: present?(user.info.is_admin) - }, - compose: %{ - me: "#{user.id}", - default_privacy: user.info.default_scope, - default_sensitive: false, - allow_content_types: Config.get([:instance, :allowed_post_formats]) - }, - media_attachments: %{ - accept_content_types: [ - ".jpg", - ".jpeg", - ".png", - ".gif", - ".webm", - ".mp4", - ".m4v", - "image\/jpeg", - "image\/png", - "image\/gif", - "video\/webm", - "video\/mp4" - ] - }, - settings: - user.info.settings || - %{ - onboarded: true, - home: %{ - shows: %{ - reblog: true, - reply: true - } - }, - notifications: %{ - alerts: %{ - follow: true, - favourite: true, - reblog: true, - mention: true - }, - shows: %{ - follow: true, - favourite: true, - reblog: true, - mention: true - }, - sounds: %{ - follow: true, - favourite: true, - reblog: true, - mention: true - } - } - }, - push_subscription: nil, - accounts: accounts, - custom_emojis: mastodon_emoji, - char_limit: limit - } - |> Jason.encode!() - - conn - |> put_layout(false) - |> put_view(MastodonView) - |> render("index.html", %{initial_state: initial_state}) - else - conn - |> put_session(:return_to, conn.request_path) - |> redirect(to: "/web/login") - end - end - - def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do - info_cng = User.Info.mastodon_settings_update(user.info, settings) - - with changeset <- Ecto.Changeset.change(user), - changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng), - {:ok, _user} <- User.update_and_set_cache(changeset) do - json(conn, %{}) - else - e -> - conn - |> put_status(:internal_server_error) - |> json(%{error: inspect(e)}) - end - end - - def login(%{assigns: %{user: %User{}}} = conn, _params) do - redirect(conn, to: local_mastodon_root_path(conn)) - end - - @doc "Local Mastodon FE login init action" - def login(conn, %{"code" => auth_token}) do - with {:ok, app} <- get_or_make_app(), - %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id), - {:ok, token} <- Token.exchange_token(app, auth) do - conn - |> put_session(:oauth_token, token.token) - |> redirect(to: local_mastodon_root_path(conn)) - end - end - - @doc "Local Mastodon FE callback action" - def login(conn, _) do - with {:ok, app} <- get_or_make_app() do - path = - o_auth_path( - conn, - :authorize, - response_type: "code", - client_id: app.client_id, - redirect_uri: ".", - scope: Enum.join(app.scopes, " ") - ) - - redirect(conn, to: path) - end - end - - defp local_mastodon_root_path(conn) do - case get_session(conn, :return_to) do - nil -> - mastodon_api_path(conn, :index, ["getting-started"]) - - return_to -> - delete_session(conn, :return_to) - return_to - end - end - - defp get_or_make_app do - find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."} - scopes = ["read", "write", "follow", "push"] - - with %App{} = app <- Repo.get_by(App, find_attrs) do - {:ok, app} = - if app.scopes == scopes do - {:ok, app} - else - app - |> Ecto.Changeset.change(%{scopes: scopes}) - |> Repo.update() - end - - {:ok, app} - else - _e -> - cs = - App.register_changeset( - %App{}, - Map.put(find_attrs, :scopes, scopes) - ) - - Repo.insert(cs) - end - end - - def logout(conn, _) do - conn - |> clear_session - |> redirect(to: "/") - end - - def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do - Logger.debug("Unimplemented, returning unmodified relationship") - - with %User{} = target <- User.get_cached_by_id(id) do - conn - |> put_view(AccountView) - |> render("relationship.json", %{user: user, target: target}) - end - end - - def empty_array(conn, _) do - Logger.debug("Unimplemented, returning an empty array") - json(conn, []) - end - - def empty_object(conn, _) do - Logger.debug("Unimplemented, returning an empty object") - json(conn, %{}) - end - - def get_filters(%{assigns: %{user: user}} = conn, _) do - filters = Filter.get_filters(user) - res = FilterView.render("filters.json", filters: filters) - json(conn, res) - end - - def create_filter( - %{assigns: %{user: user}} = conn, - %{"phrase" => phrase, "context" => context} = params - ) do - query = %Filter{ - user_id: user.id, - phrase: phrase, - context: context, - hide: Map.get(params, "irreversible", false), - whole_word: Map.get(params, "boolean", true) - # expires_at - } - - {:ok, response} = Filter.create(query) - res = FilterView.render("filter.json", filter: response) - json(conn, res) - end - - def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do - filter = Filter.get(filter_id, user) - res = FilterView.render("filter.json", filter: filter) - json(conn, res) - end - - def update_filter( - %{assigns: %{user: user}} = conn, - %{"phrase" => phrase, "context" => context, "id" => filter_id} = params - ) do - query = %Filter{ - user_id: user.id, - filter_id: filter_id, - phrase: phrase, - context: context, - hide: Map.get(params, "irreversible", nil), - whole_word: Map.get(params, "boolean", true) - # expires_at - } - - {:ok, response} = Filter.update(query) - res = FilterView.render("filter.json", filter: response) - json(conn, res) - end - - def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do - query = %Filter{ - user_id: user.id, - filter_id: filter_id - } - - {:ok, _} = Filter.delete(query) - json(conn, %{}) - end - - def suggestions(%{assigns: %{user: user}} = conn, _) do - suggestions = Config.get(:suggestions) - - if Keyword.get(suggestions, :enabled, false) do - api = Keyword.get(suggestions, :third_party_engine, "") - timeout = Keyword.get(suggestions, :timeout, 5000) - limit = Keyword.get(suggestions, :limit, 23) - - host = Config.get([Pleroma.Web.Endpoint, :url, :host]) - - user = user.nickname - - url = - api - |> String.replace("{{host}}", host) - |> String.replace("{{user}}", user) - - with {:ok, %{status: 200, body: body}} <- - HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]), - {:ok, data} <- Jason.decode(body) do - data = - data - |> Enum.slice(0, limit) - |> Enum.map(fn x -> - x - |> Map.put("id", fetch_suggestion_id(x)) - |> Map.put("avatar", MediaProxy.url(x["avatar"])) - |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"])) - end) - - json(conn, data) - else - e -> - Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}") - end - else - json(conn, []) - end - end - - defp fetch_suggestion_id(attrs) do - case User.get_or_fetch(attrs["acct"]) do - {:ok, %User{id: id}} -> id - _ -> 0 - end - end - - def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do - with %Activity{} = activity <- Activity.get_by_id(status_id), - true <- Visibility.visible_for_user?(activity, user) do - data = - StatusView.render( - "card.json", - Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) - ) - - json(conn, data) - else - _e -> - %{} - end - end - - def reports(%{assigns: %{user: user}} = conn, params) do - case CommonAPI.report(user, params) do - {:ok, activity} -> - conn - |> put_view(ReportView) - |> try_render("report.json", %{activity: activity}) - - {:error, err} -> - conn - |> put_status(:bad_request) - |> json(%{error: err}) - end - end - - def account_register( - %{assigns: %{app: app}} = conn, - %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params - ) do - params = - params - |> Map.take([ - "email", - "captcha_solution", - "captcha_token", - "captcha_answer_data", - "token", - "password" - ]) - |> Map.put("nickname", nickname) - |> Map.put("fullname", params["fullname"] || nickname) - |> Map.put("bio", params["bio"] || "") - |> Map.put("confirm", params["password"]) - - with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), - {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do - json(conn, %{ - token_type: "Bearer", - access_token: token.token, - scope: app.scopes, - created_at: Token.Utils.format_created_at(token) - }) - else - {:error, errors} -> - conn - |> put_status(:bad_request) - |> json(errors) - end - end - - def account_register(%{assigns: %{app: _app}} = conn, _params) do - render_error(conn, :bad_request, "Missing parameters") - end - - def account_register(conn, _) do - render_error(conn, :forbidden, "Invalid credentials") - end - - def conversations(%{assigns: %{user: user}} = conn, params) do - participations = Participation.for_user_with_last_activity_id(user, params) - - conversations = - Enum.map(participations, fn participation -> - ConversationView.render("participation.json", %{participation: participation, for: user}) - end) - - conn - |> add_link_headers(:conversations, participations) - |> json(conversations) - end - - def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do - with %Participation{} = participation <- - Repo.get_by(Participation, id: participation_id, user_id: user.id), - {:ok, participation} <- Participation.mark_as_read(participation) do - participation_view = - ConversationView.render("participation.json", %{participation: participation, for: user}) - - conn - |> json(participation_view) - end - end - - def password_reset(conn, params) do - nickname_or_email = params["email"] || params["nickname"] - - with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do - conn - |> put_status(:no_content) - |> json("") - else - {:error, "unknown user"} -> - send_resp(conn, :not_found, "") - - {:error, _} -> - send_resp(conn, :bad_request, "") - end - end - - def account_confirmation_resend(conn, params) do - nickname_or_email = params["email"] || params["nickname"] - - with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email), - {:ok, _} <- User.try_send_confirmation_email(user) do - conn - |> json_response(:no_content, "") - end - end - - def try_render(conn, target, params) - when is_binary(target) do - case render(conn, target, params) do - nil -> render_error(conn, :not_implemented, "Can't display this activity") - res -> res - end - end - - def try_render(conn, _, _) do - render_error(conn, :not_implemented, "Can't display this activity") - end - - defp present?(nil), do: false - defp present?(false), do: false - defp present?(_), do: true -end diff --git a/lib/pleroma/web/mastodon_api/search_controller.ex b/lib/pleroma/web/mastodon_api/search_controller.ex deleted file mode 100644 index 9072aa7a4..000000000 --- a/lib/pleroma/web/mastodon_api/search_controller.ex +++ /dev/null @@ -1,120 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SearchController do - use Pleroma.Web, :controller - - alias Pleroma.Activity - 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 - - require Logger - plug(RateLimiter, :search when action in [:search, :search2, :account_search]) - - def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, search_options(params, user)) - res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) - - json(conn, res) - end - - def search2(conn, params), do: do_search(:v2, conn, params) - def search(conn, params), do: do_search(:v1, conn, params) - - defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do - options = search_options(params, user) - timeout = Keyword.get(Repo.config(), :timeout, 15_000) - default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} - - result = - default_values - |> Enum.map(fn {resource, default_value} -> - if params["type"] == nil or params["type"] == resource do - {resource, fn -> resource_search(version, resource, query, options) end} - else - {resource, fn -> default_value end} - end - end) - |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end, - timeout: timeout, - on_timeout: :kill_task - ) - |> Enum.reduce(default_values, fn - {:ok, {resource, result}}, acc -> - Map.put(acc, resource, result) - - _error, acc -> - acc - end) - - json(conn, result) - end - - defp search_options(params, user) do - [ - resolve: params["resolve"] == "true", - following: params["following"] == "true", - limit: ControllerHelper.fetch_integer_param(params, "limit"), - offset: ControllerHelper.fetch_integer_param(params, "offset"), - type: params["type"], - author: get_author(params), - for_user: user - ] - |> Enum.filter(&elem(&1, 1)) - end - - defp resource_search(_, "accounts", query, options) do - accounts = with_fallback(fn -> User.search(query, options) end) - AccountView.render("accounts.json", users: accounts, for: options[:for_user], as: :user) - 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) - end - - defp resource_search(:v2, "hashtags", query, _options) do - tags_path = Web.base_url() <> "/tag/" - - query - |> prepare_tags() - |> Enum.map(fn tag -> - tag = String.trim_leading(tag, "#") - %{name: tag, url: tags_path <> tag} - end) - end - - defp resource_search(:v1, "hashtags", query, _options) do - query - |> prepare_tags() - |> Enum.map(fn tag -> String.trim_leading(tag, "#") end) - end - - defp prepare_tags(query) do - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) - end - - defp with_fallback(f, fallback \\ []) do - try do - f.() - rescue - error -> - Logger.error("#{__MODULE__} search error: #{inspect(error)}") - fallback - end - end - - defp get_author(%{"account_id" => account_id}) when is_binary(account_id), - do: User.get_cached_by_id(account_id) - - defp get_author(_params), do: nil -end diff --git a/lib/pleroma/web/mastodon_api/subscription_controller.ex b/lib/pleroma/web/mastodon_api/subscription_controller.ex deleted file mode 100644 index e2b17aab1..000000000 --- a/lib/pleroma/web/mastodon_api/subscription_controller.ex +++ /dev/null @@ -1,69 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SubscriptionController do - @moduledoc "The module represents functions to manage user subscriptions." - use Pleroma.Web, :controller - - alias Pleroma.Web.Push - alias Pleroma.Web.Push.Subscription - alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View - - action_fallback(:errors) - - # Creates PushSubscription - # POST /api/v1/push/subscription - # - def create(%{assigns: %{user: user, token: token}} = conn, params) do - with true <- Push.enabled(), - {:ok, _} <- Subscription.delete_if_exists(user, token), - {:ok, subscription} <- Subscription.create(user, token, params) do - view = View.render("push_subscription.json", subscription: subscription) - json(conn, view) - end - end - - # Gets PushSubscription - # GET /api/v1/push/subscription - # - def get(%{assigns: %{user: user, token: token}} = conn, _params) do - with true <- Push.enabled(), - {:ok, subscription} <- Subscription.get(user, token) do - view = View.render("push_subscription.json", subscription: subscription) - json(conn, view) - end - end - - # Updates PushSubscription - # PUT /api/v1/push/subscription - # - def update(%{assigns: %{user: user, token: token}} = conn, params) do - with true <- Push.enabled(), - {:ok, subscription} <- Subscription.update(user, token, params) do - view = View.render("push_subscription.json", subscription: subscription) - json(conn, view) - end - end - - # Deletes PushSubscription - # DELETE /api/v1/push/subscription - # - def delete(%{assigns: %{user: user, token: token}} = conn, _params) do - with true <- Push.enabled(), - {:ok, _response} <- Subscription.delete(user, token), - do: json(conn, %{}) - end - - # fallback action - # - def errors(conn, {:error, :not_found}) do - conn - |> put_status(:not_found) - |> json(dgettext("errors", "Not found")) - end - - def errors(conn, _) do - Pleroma.Web.MastodonAPI.FallbackController.call(conn, nil) - end -end -- cgit v1.2.3 From 019ced055836b3d01ea95865549478dc5cdb3c0e Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 26 Aug 2019 19:34:43 +0700 Subject: Move test/web/mastodon_api/*_test.exs to test/web/mastodon_api/controllers and test/web/mastodon_api/views --- test/web/mastodon_api/account_view_test.exs | 389 -------------- .../update_credentials_test.exs | 369 +++++++++++++ .../controllers/search_controller_test.exs | 285 ++++++++++ .../controllers/subscription_controller_test.exs | 192 +++++++ test/web/mastodon_api/conversation_view_test.exs | 34 -- .../update_credentials_test.exs | 369 ------------- test/web/mastodon_api/notification_view_test.exs | 104 ---- .../mastodon_api/push_subscription_view_test.exs | 23 - .../mastodon_api/scheduled_activity_view_test.exs | 68 --- test/web/mastodon_api/search_controller_test.exs | 285 ---------- test/web/mastodon_api/status_view_test.exs | 584 --------------------- .../mastodon_api/subscription_controller_test.exs | 192 ------- test/web/mastodon_api/views/account_view_test.exs | 389 ++++++++++++++ .../mastodon_api/views/conversation_view_test.exs | 34 ++ .../mastodon_api/views/notification_view_test.exs | 104 ++++ .../views/push_subscription_view_test.exs | 23 + .../views/scheduled_activity_view_test.exs | 68 +++ test/web/mastodon_api/views/status_view_test.exs | 584 +++++++++++++++++++++ 18 files changed, 2048 insertions(+), 2048 deletions(-) delete mode 100644 test/web/mastodon_api/account_view_test.exs create mode 100644 test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs create mode 100644 test/web/mastodon_api/controllers/search_controller_test.exs create mode 100644 test/web/mastodon_api/controllers/subscription_controller_test.exs delete mode 100644 test/web/mastodon_api/conversation_view_test.exs delete mode 100644 test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs delete mode 100644 test/web/mastodon_api/notification_view_test.exs delete mode 100644 test/web/mastodon_api/push_subscription_view_test.exs delete mode 100644 test/web/mastodon_api/scheduled_activity_view_test.exs delete mode 100644 test/web/mastodon_api/search_controller_test.exs delete mode 100644 test/web/mastodon_api/status_view_test.exs delete mode 100644 test/web/mastodon_api/subscription_controller_test.exs create mode 100644 test/web/mastodon_api/views/account_view_test.exs create mode 100644 test/web/mastodon_api/views/conversation_view_test.exs create mode 100644 test/web/mastodon_api/views/notification_view_test.exs create mode 100644 test/web/mastodon_api/views/push_subscription_view_test.exs create mode 100644 test/web/mastodon_api/views/scheduled_activity_view_test.exs create mode 100644 test/web/mastodon_api/views/status_view_test.exs diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs deleted file mode 100644 index 1d8b28339..000000000 --- a/test/web/mastodon_api/account_view_test.exs +++ /dev/null @@ -1,389 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.AccountViewTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.AccountView - - test "Represent a user account" do - source_data = %{ - "tag" => [ - %{ - "type" => "Emoji", - "icon" => %{"url" => "/file.png"}, - "name" => ":karjalanpiirakka:" - } - ] - } - - background_image = %{ - "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}] - } - - user = - insert(:user, %{ - info: %{ - note_count: 5, - follower_count: 3, - source_data: source_data, - background: background_image - }, - nickname: "shp@shitposter.club", - name: ":karjalanpiirakka: shp", - bio: "valid html", - inserted_at: ~N[2017-08-15 15:47:06.597036] - }) - - expected = %{ - id: to_string(user.id), - username: "shp", - acct: user.nickname, - display_name: user.name, - locked: false, - created_at: "2017-08-15T15:47:06.000Z", - followers_count: 3, - following_count: 0, - statuses_count: 5, - note: "valid html", - url: user.ap_id, - avatar: "http://localhost:4001/images/avi.png", - avatar_static: "http://localhost:4001/images/avi.png", - header: "http://localhost:4001/images/banner.png", - header_static: "http://localhost:4001/images/banner.png", - emojis: [ - %{ - "static_url" => "/file.png", - "url" => "/file.png", - "shortcode" => "karjalanpiirakka", - "visible_in_picker" => false - } - ], - fields: [], - bot: false, - source: %{ - note: "valid html", - sensitive: false, - pleroma: %{}, - fields: [] - }, - pleroma: %{ - background_image: "https://example.com/images/asuka_hospital.png", - confirmation_pending: false, - tags: [], - is_admin: false, - is_moderator: false, - hide_favorites: true, - hide_followers: false, - hide_follows: false, - relationship: %{}, - skip_thread_containment: false - } - } - - assert expected == AccountView.render("account.json", %{user: user}) - end - - test "Represent the user account for the account owner" do - user = insert(:user) - - notification_settings = %{ - "followers" => true, - "follows" => true, - "non_follows" => true, - "non_followers" => true - } - - privacy = user.info.default_scope - - assert %{ - pleroma: %{notification_settings: ^notification_settings}, - source: %{privacy: ^privacy} - } = AccountView.render("account.json", %{user: user, for: user}) - end - - test "Represent a Service(bot) account" do - user = - insert(:user, %{ - info: %{note_count: 5, follower_count: 3, source_data: %{"type" => "Service"}}, - nickname: "shp@shitposter.club", - inserted_at: ~N[2017-08-15 15:47:06.597036] - }) - - expected = %{ - id: to_string(user.id), - username: "shp", - acct: user.nickname, - display_name: user.name, - locked: false, - created_at: "2017-08-15T15:47:06.000Z", - followers_count: 3, - following_count: 0, - statuses_count: 5, - note: user.bio, - url: user.ap_id, - avatar: "http://localhost:4001/images/avi.png", - avatar_static: "http://localhost:4001/images/avi.png", - header: "http://localhost:4001/images/banner.png", - header_static: "http://localhost:4001/images/banner.png", - emojis: [], - fields: [], - bot: true, - source: %{ - note: user.bio, - sensitive: false, - pleroma: %{}, - fields: [] - }, - pleroma: %{ - background_image: nil, - confirmation_pending: false, - tags: [], - is_admin: false, - is_moderator: false, - hide_favorites: true, - hide_followers: false, - hide_follows: false, - relationship: %{}, - skip_thread_containment: false - } - } - - assert expected == AccountView.render("account.json", %{user: user}) - end - - test "Represent a deactivated user for an admin" do - admin = insert(:user, %{info: %{is_admin: true}}) - deactivated_user = insert(:user, %{info: %{deactivated: true}}) - represented = AccountView.render("account.json", %{user: deactivated_user, for: admin}) - assert represented[:pleroma][:deactivated] == true - end - - test "Represent a smaller mention" do - user = insert(:user) - - expected = %{ - id: to_string(user.id), - acct: user.nickname, - username: user.nickname, - url: user.ap_id - } - - assert expected == AccountView.render("mention.json", %{user: user}) - end - - describe "relationship" do - test "represent a relationship for the following and followed user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, user} = User.follow(user, other_user) - {:ok, other_user} = User.follow(other_user, user) - {:ok, other_user} = User.subscribe(user, other_user) - {:ok, user} = User.mute(user, other_user, true) - {:ok, user} = CommonAPI.hide_reblogs(user, other_user) - - expected = %{ - id: to_string(other_user.id), - following: true, - followed_by: true, - blocking: false, - blocked_by: false, - muting: true, - muting_notifications: true, - subscribing: true, - requested: false, - domain_blocking: false, - showing_reblogs: false, - endorsed: false - } - - assert expected == - AccountView.render("relationship.json", %{user: user, target: other_user}) - end - - test "represent a relationship for the blocking and blocked user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, user} = User.follow(user, other_user) - {:ok, other_user} = User.subscribe(user, other_user) - {:ok, user} = User.block(user, other_user) - {:ok, other_user} = User.block(other_user, user) - - expected = %{ - id: to_string(other_user.id), - following: false, - followed_by: false, - blocking: true, - blocked_by: true, - muting: false, - muting_notifications: false, - subscribing: false, - requested: false, - domain_blocking: false, - showing_reblogs: true, - endorsed: false - } - - assert expected == - AccountView.render("relationship.json", %{user: user, target: other_user}) - end - - test "represent a relationship for the user blocking a domain" do - user = insert(:user) - other_user = insert(:user, ap_id: "https://bad.site/users/other_user") - - {:ok, user} = User.block_domain(user, "bad.site") - - assert %{domain_blocking: true, blocking: false} = - AccountView.render("relationship.json", %{user: user, target: other_user}) - end - - test "represent a relationship for the user with a pending follow request" do - user = insert(:user) - other_user = insert(:user, %{info: %User.Info{locked: true}}) - - {:ok, user, other_user, _} = CommonAPI.follow(user, other_user) - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - expected = %{ - id: to_string(other_user.id), - following: false, - followed_by: false, - blocking: false, - blocked_by: false, - muting: false, - muting_notifications: false, - subscribing: false, - requested: true, - domain_blocking: false, - showing_reblogs: true, - endorsed: false - } - - assert expected == - AccountView.render("relationship.json", %{user: user, target: other_user}) - end - end - - test "represent an embedded relationship" do - user = - insert(:user, %{ - info: %{note_count: 5, follower_count: 0, source_data: %{"type" => "Service"}}, - nickname: "shp@shitposter.club", - inserted_at: ~N[2017-08-15 15:47:06.597036] - }) - - other_user = insert(:user) - {:ok, other_user} = User.follow(other_user, user) - {:ok, other_user} = User.block(other_user, user) - {:ok, _} = User.follow(insert(:user), user) - - expected = %{ - id: to_string(user.id), - username: "shp", - acct: user.nickname, - display_name: user.name, - locked: false, - created_at: "2017-08-15T15:47:06.000Z", - followers_count: 1, - following_count: 0, - statuses_count: 5, - note: user.bio, - url: user.ap_id, - avatar: "http://localhost:4001/images/avi.png", - avatar_static: "http://localhost:4001/images/avi.png", - header: "http://localhost:4001/images/banner.png", - header_static: "http://localhost:4001/images/banner.png", - emojis: [], - fields: [], - bot: true, - source: %{ - note: user.bio, - sensitive: false, - pleroma: %{}, - fields: [] - }, - pleroma: %{ - background_image: nil, - confirmation_pending: false, - tags: [], - is_admin: false, - is_moderator: false, - hide_favorites: true, - hide_followers: false, - hide_follows: false, - relationship: %{ - id: to_string(user.id), - following: false, - followed_by: false, - blocking: true, - blocked_by: false, - subscribing: false, - muting: false, - muting_notifications: false, - requested: false, - domain_blocking: false, - showing_reblogs: true, - endorsed: false - }, - skip_thread_containment: false - } - } - - assert expected == AccountView.render("account.json", %{user: user, for: other_user}) - end - - test "returns the settings store if the requesting user is the represented user and it's requested specifically" do - user = insert(:user, %{info: %User.Info{pleroma_settings_store: %{fe: "test"}}}) - - result = - AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) - - assert result.pleroma.settings_store == %{:fe => "test"} - - result = AccountView.render("account.json", %{user: user, with_pleroma_settings: true}) - assert result.pleroma[:settings_store] == nil - - result = AccountView.render("account.json", %{user: user, for: user}) - assert result.pleroma[:settings_store] == nil - end - - test "sanitizes display names" do - user = insert(:user, name: " username ") - result = AccountView.render("account.json", %{user: user}) - refute result.display_name == " username " - end - - describe "hiding follows/following" do - test "shows when follows/following are hidden and sets follower/following count to 0" do - user = insert(:user, info: %{hide_followers: true, hide_follows: true}) - other_user = insert(:user) - {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{ - followers_count: 0, - following_count: 0, - pleroma: %{hide_follows: true, hide_followers: true} - } = AccountView.render("account.json", %{user: user}) - end - - test "shows actual follower/following count to the account owner" do - user = insert(:user, info: %{hide_followers: true, hide_follows: true}) - other_user = insert(:user) - {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{ - followers_count: 1, - following_count: 1 - } = AccountView.render("account.json", %{user: user, for: user}) - end - end -end diff --git a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs new file mode 100644 index 000000000..87ee82050 --- /dev/null +++ b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs @@ -0,0 +1,369 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do + alias Pleroma.Repo + alias Pleroma.User + + use Pleroma.Web.ConnCase + + import Pleroma.Factory + clear_config([:instance, :max_account_fields]) + + describe "updating credentials" do + test "sets user settings in a generic way", %{conn: conn} do + user = insert(:user) + + res_conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "pleroma_settings_store" => %{ + pleroma_fe: %{ + theme: "bla" + } + } + }) + + assert user = json_response(res_conn, 200) + assert user["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}} + + user = Repo.get(User, user["id"]) + + res_conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "pleroma_settings_store" => %{ + masto_fe: %{ + theme: "bla" + } + } + }) + + assert user = json_response(res_conn, 200) + + assert user["pleroma"]["settings_store"] == + %{ + "pleroma_fe" => %{"theme" => "bla"}, + "masto_fe" => %{"theme" => "bla"} + } + + user = Repo.get(User, user["id"]) + + res_conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "pleroma_settings_store" => %{ + masto_fe: %{ + theme: "blub" + } + } + }) + + assert user = json_response(res_conn, 200) + + assert user["pleroma"]["settings_store"] == + %{ + "pleroma_fe" => %{"theme" => "bla"}, + "masto_fe" => %{"theme" => "blub"} + } + end + + test "updates the user's bio", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "note" => "I drink #cofe with @#{user2.nickname}" + }) + + assert user = json_response(conn, 200) + + assert user["note"] == + ~s(I drink with @) <> user2.nickname <> ~s() + end + + test "updates the user's locking status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{locked: "true"}) + + assert user = json_response(conn, 200) + assert user["locked"] == true + end + + test "updates the user's default scope", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{default_scope: "cofe"}) + + assert user = json_response(conn, 200) + assert user["source"]["privacy"] == "cofe" + end + + test "updates the user's hide_followers status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{hide_followers: "true"}) + + assert user = json_response(conn, 200) + assert user["pleroma"]["hide_followers"] == true + end + + test "updates the user's skip_thread_containment option", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"}) + |> json_response(200) + + assert response["pleroma"]["skip_thread_containment"] == true + assert refresh_record(user).info.skip_thread_containment + end + + test "updates the user's hide_follows status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{hide_follows: "true"}) + + assert user = json_response(conn, 200) + assert user["pleroma"]["hide_follows"] == true + end + + test "updates the user's hide_favorites status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{hide_favorites: "true"}) + + assert user = json_response(conn, 200) + assert user["pleroma"]["hide_favorites"] == true + end + + test "updates the user's show_role status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{show_role: "false"}) + + assert user = json_response(conn, 200) + assert user["source"]["pleroma"]["show_role"] == false + end + + test "updates the user's no_rich_text status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{no_rich_text: "true"}) + + assert user = json_response(conn, 200) + assert user["source"]["pleroma"]["no_rich_text"] == true + end + + test "updates the user's name", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"}) + + assert user = json_response(conn, 200) + assert user["display_name"] == "markorepairs" + end + + test "updates the user's avatar", %{conn: conn} do + user = insert(:user) + + new_avatar = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) + + assert user_response = json_response(conn, 200) + assert user_response["avatar"] != User.avatar_url(user) + end + + test "updates the user's banner", %{conn: conn} do + user = insert(:user) + + new_header = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"header" => new_header}) + + assert user_response = json_response(conn, 200) + assert user_response["header"] != User.banner_url(user) + end + + test "updates the user's background", %{conn: conn} do + user = insert(:user) + + new_header = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "pleroma_background_image" => new_header + }) + + assert user_response = json_response(conn, 200) + assert user_response["pleroma"]["background_image"] + end + + test "requires 'write' permission", %{conn: conn} do + token1 = insert(:oauth_token, scopes: ["read"]) + token2 = insert(:oauth_token, scopes: ["write", "follow"]) + + for token <- [token1, token2] do + conn = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> patch("/api/v1/accounts/update_credentials", %{}) + + if token == token1 do + assert %{"error" => "Insufficient permissions: write."} == json_response(conn, 403) + else + assert json_response(conn, 200) + end + end + end + + test "updates profile emojos", %{conn: conn} do + user = insert(:user) + + note = "*sips :blank:*" + name = "I am :firefox:" + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "note" => note, + "display_name" => name + }) + + assert json_response(conn, 200) + + conn = + conn + |> get("/api/v1/accounts/#{user.id}") + + assert user = json_response(conn, 200) + + assert user["note"] == note + assert user["display_name"] == name + assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user["emojis"] + end + + test "update fields", %{conn: conn} do + user = insert(:user) + + fields = [ + %{"name" => "foo", "value" => ""}, + %{"name" => "link", "value" => "cofe.io"} + ] + + account = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) + |> json_response(200) + + assert account["fields"] == [ + %{"name" => "foo", "value" => "bar"}, + %{"name" => "link", "value" => "cofe.io"} + ] + + assert account["source"]["fields"] == [ + %{ + "name" => "foo", + "value" => "" + }, + %{"name" => "link", "value" => "cofe.io"} + ] + + name_limit = Pleroma.Config.get([:instance, :account_field_name_length]) + value_limit = Pleroma.Config.get([:instance, :account_field_value_length]) + + long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join() + + fields = [%{"name" => "foo", "value" => long_value}] + + assert %{"error" => "Invalid request"} == + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) + |> json_response(403) + + long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join() + + fields = [%{"name" => long_name, "value" => "bar"}] + + assert %{"error" => "Invalid request"} == + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) + |> json_response(403) + + Pleroma.Config.put([:instance, :max_account_fields], 1) + + fields = [ + %{"name" => "foo", "value" => "bar"}, + %{"name" => "link", "value" => "cofe.io"} + ] + + assert %{"error" => "Invalid request"} == + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) + |> json_response(403) + end + end +end diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs new file mode 100644 index 000000000..49c79ff0a --- /dev/null +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -0,0 +1,285 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Object + alias Pleroma.Web + alias Pleroma.Web.CommonAPI + import Pleroma.Factory + import ExUnit.CaptureLog + import Tesla.Mock + import Mock + + setup do + mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe ".search2" do + test "it returns empty result if user or status search return undefined error", %{conn: conn} do + with_mocks [ + {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, + {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} + ] do + capture_log(fn -> + results = + conn + |> get("/api/v2/search", %{"q" => "2hu"}) + |> json_response(200) + + assert results["accounts"] == [] + assert results["statuses"] == [] + end) =~ + "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" + end + end + + test "search", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu private"}) + + {:ok, _activity} = + CommonAPI.post(user, %{ + "status" => "This is about 2hu, but private", + "visibility" => "private" + }) + + {:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"}) + + conn = get(conn, "/api/v2/search", %{"q" => "2hu #private"}) + + assert results = json_response(conn, 200) + + [account | _] = results["accounts"] + assert account["id"] == to_string(user_three.id) + + assert results["hashtags"] == [ + %{"name" => "private", "url" => "#{Web.base_url()}/tag/private"} + ] + + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) + end + end + + describe ".account_search" do + test "account search", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + results = + conn + |> assign(:user, user) + |> get("/api/v1/accounts/search", %{"q" => "shp"}) + |> json_response(200) + + result_ids = for result <- results, do: result["acct"] + + assert user_two.nickname in result_ids + assert user_three.nickname in result_ids + + results = + conn + |> assign(:user, user) + |> get("/api/v1/accounts/search", %{"q" => "2hu"}) + |> json_response(200) + + result_ids = for result <- results, do: result["acct"] + + assert user_three.nickname in result_ids + end + + test "returns account if query contains a space", %{conn: conn} do + user = insert(:user, %{nickname: "shp@shitposter.club"}) + + results = + conn + |> assign(:user, user) + |> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "}) + |> json_response(200) + + assert length(results) == 1 + end + end + + describe ".search" do + test "it returns empty result if user or status search return undefined error", %{conn: conn} do + with_mocks [ + {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, + {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} + ] do + capture_log(fn -> + results = + conn + |> get("/api/v1/search", %{"q" => "2hu"}) + |> json_response(200) + + assert results["accounts"] == [] + assert results["statuses"] == [] + end) =~ + "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" + end + end + + test "search", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"}) + + {:ok, _activity} = + CommonAPI.post(user, %{ + "status" => "This is about 2hu, but private", + "visibility" => "private" + }) + + {:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"}) + + conn = + conn + |> get("/api/v1/search", %{"q" => "2hu"}) + + assert results = json_response(conn, 200) + + [account | _] = results["accounts"] + assert account["id"] == to_string(user_three.id) + + assert results["hashtags"] == [] + + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) + end + + test "search fetches remote statuses", %{conn: conn} do + capture_log(fn -> + conn = + conn + |> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"}) + + assert results = json_response(conn, 200) + + [status] = results["statuses"] + + assert status["uri"] == + "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" + end) + end + + test "search doesn't show statuses that it shouldn't", %{conn: conn} do + {:ok, activity} = + CommonAPI.post(insert(:user), %{ + "status" => "This is about 2hu, but private", + "visibility" => "private" + }) + + capture_log(fn -> + conn = + conn + |> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]}) + + assert results = json_response(conn, 200) + + [] = results["statuses"] + end) + end + + test "search fetches remote accounts", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "true"}) + + assert results = json_response(conn, 200) + [account] = results["accounts"] + assert account["acct"] == "shp@social.heldscal.la" + end + + test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do + conn = + conn + |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "false"}) + + assert results = json_response(conn, 200) + assert [] == results["accounts"] + end + + test "search with limit and offset", %{conn: conn} do + user = insert(:user) + _user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + _user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, _activity1} = CommonAPI.post(user, %{"status" => "This is about 2hu"}) + {:ok, _activity2} = CommonAPI.post(user, %{"status" => "This is also about 2hu"}) + + result = + conn + |> get("/api/v1/search", %{"q" => "2hu", "limit" => 1}) + + assert results = json_response(result, 200) + assert [%{"id" => activity_id1}] = results["statuses"] + assert [_] = results["accounts"] + + results = + conn + |> get("/api/v1/search", %{"q" => "2hu", "limit" => 1, "offset" => 1}) + |> json_response(200) + + assert [%{"id" => activity_id2}] = results["statuses"] + assert [] = results["accounts"] + + assert activity_id1 != activity_id2 + end + + test "search returns results only for the given type", %{conn: conn} do + user = insert(:user) + _user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, _activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"}) + + assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} = + conn + |> get("/api/v1/search", %{"q" => "2hu", "type" => "statuses"}) + |> json_response(200) + + assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} = + conn + |> get("/api/v1/search", %{"q" => "2hu", "type" => "accounts"}) + |> json_response(200) + end + + test "search uses account_id to filter statuses by the author", %{conn: conn} do + user = insert(:user, %{nickname: "shp@shitposter.club"}) + user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, activity1} = CommonAPI.post(user, %{"status" => "This is about 2hu"}) + {:ok, activity2} = CommonAPI.post(user_two, %{"status" => "This is also about 2hu"}) + + results = + conn + |> get("/api/v1/search", %{"q" => "2hu", "account_id" => user.id}) + |> json_response(200) + + assert [%{"id" => activity_id1}] = results["statuses"] + assert activity_id1 == activity1.id + assert [_] = results["accounts"] + + results = + conn + |> get("/api/v1/search", %{"q" => "2hu", "account_id" => user_two.id}) + |> json_response(200) + + assert [%{"id" => activity_id2}] = results["statuses"] + assert activity_id2 == activity2.id + end + end +end diff --git a/test/web/mastodon_api/controllers/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs new file mode 100644 index 000000000..7dfb02f63 --- /dev/null +++ b/test/web/mastodon_api/controllers/subscription_controller_test.exs @@ -0,0 +1,192 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + alias Pleroma.Web.Push + alias Pleroma.Web.Push.Subscription + + @sub %{ + "endpoint" => "https://example.com/example/1234", + "keys" => %{ + "auth" => "8eDyX_uCN0XRhSbY5hs7Hg==", + "p256dh" => + "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" + } + } + @server_key Keyword.get(Push.vapid_config(), :public_key) + + setup do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["push"]) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + + %{conn: conn, user: user, token: token} + end + + defmacro assert_error_when_disable_push(do: yield) do + quote do + vapid_details = Application.get_env(:web_push_encryption, :vapid_details, []) + Application.put_env(:web_push_encryption, :vapid_details, []) + assert "Something went wrong" == unquote(yield) + Application.put_env(:web_push_encryption, :vapid_details, vapid_details) + end + end + + describe "creates push subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> post("/api/v1/push/subscription", %{}) + |> json_response(500) + end + end + + test "successful creation", %{conn: conn} do + result = + conn + |> post("/api/v1/push/subscription", %{ + "data" => %{"alerts" => %{"mention" => true, "test" => true}}, + "subscription" => @sub + }) + |> json_response(200) + + [subscription] = Pleroma.Repo.all(Subscription) + + assert %{ + "alerts" => %{"mention" => true}, + "endpoint" => subscription.endpoint, + "id" => to_string(subscription.id), + "server_key" => @server_key + } == result + end + end + + describe "gets a user subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response(500) + end + end + + test "returns error when user hasn't subscription", %{conn: conn} do + res = + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response(404) + + assert "Not found" == res + end + + test "returns a user subsciption", %{conn: conn, user: user, token: token} do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + res = + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response(200) + + expect = %{ + "alerts" => %{"mention" => true}, + "endpoint" => "https://example.com/example/1234", + "id" => to_string(subscription.id), + "server_key" => @server_key + } + + assert expect == res + end + end + + describe "updates a user subsciption" do + setup %{conn: conn, user: user, token: token} do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + %{conn: conn, user: user, token: token, subscription: subscription} + end + + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}}) + |> json_response(500) + end + end + + test "returns updated subsciption", %{conn: conn, subscription: subscription} do + res = + conn + |> put("/api/v1/push/subscription", %{ + data: %{"alerts" => %{"mention" => false, "follow" => true}} + }) + |> json_response(200) + + expect = %{ + "alerts" => %{"follow" => true, "mention" => false}, + "endpoint" => "https://example.com/example/1234", + "id" => to_string(subscription.id), + "server_key" => @server_key + } + + assert expect == res + end + end + + describe "deletes the user subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response(500) + end + end + + test "returns error when user hasn't subscription", %{conn: conn} do + res = + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response(404) + + assert "Not found" == res + end + + test "returns empty result and delete user subsciption", %{ + conn: conn, + user: user, + token: token + } do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + res = + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response(200) + + assert %{} == res + refute Pleroma.Repo.get(Subscription, subscription.id) + end + end +end diff --git a/test/web/mastodon_api/conversation_view_test.exs b/test/web/mastodon_api/conversation_view_test.exs deleted file mode 100644 index a2a880705..000000000 --- a/test/web/mastodon_api/conversation_view_test.exs +++ /dev/null @@ -1,34 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do - use Pleroma.DataCase - - alias Pleroma.Conversation.Participation - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.ConversationView - - import Pleroma.Factory - - test "represents a Mastodon Conversation entity" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}", "visibility" => "direct"}) - - [participation] = Participation.for_user_with_last_activity_id(user) - - assert participation - - conversation = - ConversationView.render("participation.json", %{participation: participation, for: user}) - - assert conversation.id == participation.id |> to_string() - assert conversation.last_status.id == activity.id - - assert [account] = conversation.accounts - assert account.id == other_user.id - end -end diff --git a/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs deleted file mode 100644 index 87ee82050..000000000 --- a/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs +++ /dev/null @@ -1,369 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do - alias Pleroma.Repo - alias Pleroma.User - - use Pleroma.Web.ConnCase - - import Pleroma.Factory - clear_config([:instance, :max_account_fields]) - - describe "updating credentials" do - test "sets user settings in a generic way", %{conn: conn} do - user = insert(:user) - - res_conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "pleroma_settings_store" => %{ - pleroma_fe: %{ - theme: "bla" - } - } - }) - - assert user = json_response(res_conn, 200) - assert user["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}} - - user = Repo.get(User, user["id"]) - - res_conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "pleroma_settings_store" => %{ - masto_fe: %{ - theme: "bla" - } - } - }) - - assert user = json_response(res_conn, 200) - - assert user["pleroma"]["settings_store"] == - %{ - "pleroma_fe" => %{"theme" => "bla"}, - "masto_fe" => %{"theme" => "bla"} - } - - user = Repo.get(User, user["id"]) - - res_conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "pleroma_settings_store" => %{ - masto_fe: %{ - theme: "blub" - } - } - }) - - assert user = json_response(res_conn, 200) - - assert user["pleroma"]["settings_store"] == - %{ - "pleroma_fe" => %{"theme" => "bla"}, - "masto_fe" => %{"theme" => "blub"} - } - end - - test "updates the user's bio", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "note" => "I drink #cofe with @#{user2.nickname}" - }) - - assert user = json_response(conn, 200) - - assert user["note"] == - ~s(I drink with @) <> user2.nickname <> ~s() - end - - test "updates the user's locking status", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{locked: "true"}) - - assert user = json_response(conn, 200) - assert user["locked"] == true - end - - test "updates the user's default scope", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{default_scope: "cofe"}) - - assert user = json_response(conn, 200) - assert user["source"]["privacy"] == "cofe" - end - - test "updates the user's hide_followers status", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{hide_followers: "true"}) - - assert user = json_response(conn, 200) - assert user["pleroma"]["hide_followers"] == true - end - - test "updates the user's skip_thread_containment option", %{conn: conn} do - user = insert(:user) - - response = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"}) - |> json_response(200) - - assert response["pleroma"]["skip_thread_containment"] == true - assert refresh_record(user).info.skip_thread_containment - end - - test "updates the user's hide_follows status", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{hide_follows: "true"}) - - assert user = json_response(conn, 200) - assert user["pleroma"]["hide_follows"] == true - end - - test "updates the user's hide_favorites status", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{hide_favorites: "true"}) - - assert user = json_response(conn, 200) - assert user["pleroma"]["hide_favorites"] == true - end - - test "updates the user's show_role status", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{show_role: "false"}) - - assert user = json_response(conn, 200) - assert user["source"]["pleroma"]["show_role"] == false - end - - test "updates the user's no_rich_text status", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{no_rich_text: "true"}) - - assert user = json_response(conn, 200) - assert user["source"]["pleroma"]["no_rich_text"] == true - end - - test "updates the user's name", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"}) - - assert user = json_response(conn, 200) - assert user["display_name"] == "markorepairs" - end - - test "updates the user's avatar", %{conn: conn} do - user = insert(:user) - - new_avatar = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) - - assert user_response = json_response(conn, 200) - assert user_response["avatar"] != User.avatar_url(user) - end - - test "updates the user's banner", %{conn: conn} do - user = insert(:user) - - new_header = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{"header" => new_header}) - - assert user_response = json_response(conn, 200) - assert user_response["header"] != User.banner_url(user) - end - - test "updates the user's background", %{conn: conn} do - user = insert(:user) - - new_header = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "pleroma_background_image" => new_header - }) - - assert user_response = json_response(conn, 200) - assert user_response["pleroma"]["background_image"] - end - - test "requires 'write' permission", %{conn: conn} do - token1 = insert(:oauth_token, scopes: ["read"]) - token2 = insert(:oauth_token, scopes: ["write", "follow"]) - - for token <- [token1, token2] do - conn = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> patch("/api/v1/accounts/update_credentials", %{}) - - if token == token1 do - assert %{"error" => "Insufficient permissions: write."} == json_response(conn, 403) - else - assert json_response(conn, 200) - end - end - end - - test "updates profile emojos", %{conn: conn} do - user = insert(:user) - - note = "*sips :blank:*" - name = "I am :firefox:" - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "note" => note, - "display_name" => name - }) - - assert json_response(conn, 200) - - conn = - conn - |> get("/api/v1/accounts/#{user.id}") - - assert user = json_response(conn, 200) - - assert user["note"] == note - assert user["display_name"] == name - assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user["emojis"] - end - - test "update fields", %{conn: conn} do - user = insert(:user) - - fields = [ - %{"name" => "foo", "value" => ""}, - %{"name" => "link", "value" => "cofe.io"} - ] - - account = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) - |> json_response(200) - - assert account["fields"] == [ - %{"name" => "foo", "value" => "bar"}, - %{"name" => "link", "value" => "cofe.io"} - ] - - assert account["source"]["fields"] == [ - %{ - "name" => "foo", - "value" => "" - }, - %{"name" => "link", "value" => "cofe.io"} - ] - - name_limit = Pleroma.Config.get([:instance, :account_field_name_length]) - value_limit = Pleroma.Config.get([:instance, :account_field_value_length]) - - long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join() - - fields = [%{"name" => "foo", "value" => long_value}] - - assert %{"error" => "Invalid request"} == - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) - |> json_response(403) - - long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join() - - fields = [%{"name" => long_name, "value" => "bar"}] - - assert %{"error" => "Invalid request"} == - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) - |> json_response(403) - - Pleroma.Config.put([:instance, :max_account_fields], 1) - - fields = [ - %{"name" => "foo", "value" => "bar"}, - %{"name" => "link", "value" => "cofe.io"} - ] - - assert %{"error" => "Invalid request"} == - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) - |> json_response(403) - end - end -end diff --git a/test/web/mastodon_api/notification_view_test.exs b/test/web/mastodon_api/notification_view_test.exs deleted file mode 100644 index 977ea1e87..000000000 --- a/test/web/mastodon_api/notification_view_test.exs +++ /dev/null @@ -1,104 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.MastodonAPI.NotificationView - alias Pleroma.Web.MastodonAPI.StatusView - import Pleroma.Factory - - test "Mention notification" do - user = insert(:user) - mentioned_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{mentioned_user.nickname}"}) - {:ok, [notification]} = Notification.create_notifications(activity) - user = User.get_cached_by_id(user.id) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false}, - type: "mention", - account: AccountView.render("account.json", %{user: user, for: mentioned_user}), - status: StatusView.render("status.json", %{activity: activity, for: mentioned_user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - result = - NotificationView.render("index.json", %{notifications: [notification], for: mentioned_user}) - - assert [expected] == result - end - - test "Favourite notification" do - user = insert(:user) - another_user = insert(:user) - {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"}) - {:ok, favorite_activity, _object} = CommonAPI.favorite(create_activity.id, another_user) - {:ok, [notification]} = Notification.create_notifications(favorite_activity) - create_activity = Activity.get_by_id(create_activity.id) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false}, - type: "favourite", - account: AccountView.render("account.json", %{user: another_user, for: user}), - status: StatusView.render("status.json", %{activity: create_activity, for: user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - result = NotificationView.render("index.json", %{notifications: [notification], for: user}) - - assert [expected] == result - end - - test "Reblog notification" do - user = insert(:user) - another_user = insert(:user) - {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"}) - {:ok, reblog_activity, _object} = CommonAPI.repeat(create_activity.id, another_user) - {:ok, [notification]} = Notification.create_notifications(reblog_activity) - reblog_activity = Activity.get_by_id(create_activity.id) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false}, - type: "reblog", - account: AccountView.render("account.json", %{user: another_user, for: user}), - status: StatusView.render("status.json", %{activity: reblog_activity, for: user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - result = NotificationView.render("index.json", %{notifications: [notification], for: user}) - - assert [expected] == result - end - - test "Follow notification" do - follower = insert(:user) - followed = insert(:user) - {:ok, follower, followed, _activity} = CommonAPI.follow(follower, followed) - notification = Notification |> Repo.one() |> Repo.preload(:activity) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false}, - type: "follow", - account: AccountView.render("account.json", %{user: follower, for: followed}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - result = - NotificationView.render("index.json", %{notifications: [notification], for: followed}) - - assert [expected] == result - end -end diff --git a/test/web/mastodon_api/push_subscription_view_test.exs b/test/web/mastodon_api/push_subscription_view_test.exs deleted file mode 100644 index dc935fc82..000000000 --- a/test/web/mastodon_api/push_subscription_view_test.exs +++ /dev/null @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.PushSubscriptionViewTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View - alias Pleroma.Web.Push - - test "Represent a subscription" do - subscription = insert(:push_subscription, data: %{"alerts" => %{"mention" => true}}) - - expected = %{ - alerts: %{"mention" => true}, - endpoint: subscription.endpoint, - id: to_string(subscription.id), - server_key: Keyword.get(Push.vapid_config(), :public_key) - } - - assert expected == View.render("push_subscription.json", %{subscription: subscription}) - end -end diff --git a/test/web/mastodon_api/scheduled_activity_view_test.exs b/test/web/mastodon_api/scheduled_activity_view_test.exs deleted file mode 100644 index ecbb855d4..000000000 --- a/test/web/mastodon_api/scheduled_activity_view_test.exs +++ /dev/null @@ -1,68 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do - use Pleroma.DataCase - alias Pleroma.ScheduledActivity - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.ScheduledActivityView - alias Pleroma.Web.MastodonAPI.StatusView - import Pleroma.Factory - - test "A scheduled activity with a media attachment" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "hi"}) - - scheduled_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(10), :millisecond) - |> NaiveDateTime.to_iso8601() - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - attrs = %{ - params: %{ - "media_ids" => [upload.id], - "status" => "hi", - "sensitive" => true, - "spoiler_text" => "spoiler", - "visibility" => "unlisted", - "in_reply_to_id" => to_string(activity.id) - }, - scheduled_at: scheduled_at - } - - {:ok, scheduled_activity} = ScheduledActivity.create(user, attrs) - result = ScheduledActivityView.render("show.json", %{scheduled_activity: scheduled_activity}) - - expected = %{ - id: to_string(scheduled_activity.id), - media_attachments: - %{"media_ids" => [upload.id]} - |> Utils.attachments_from_ids() - |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), - params: %{ - in_reply_to_id: to_string(activity.id), - media_ids: [upload.id], - poll: nil, - scheduled_at: nil, - sensitive: true, - spoiler_text: "spoiler", - text: "hi", - visibility: "unlisted" - }, - scheduled_at: Utils.to_masto_date(scheduled_activity.scheduled_at) - } - - assert expected == result - end -end diff --git a/test/web/mastodon_api/search_controller_test.exs b/test/web/mastodon_api/search_controller_test.exs deleted file mode 100644 index 49c79ff0a..000000000 --- a/test/web/mastodon_api/search_controller_test.exs +++ /dev/null @@ -1,285 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Object - alias Pleroma.Web - alias Pleroma.Web.CommonAPI - import Pleroma.Factory - import ExUnit.CaptureLog - import Tesla.Mock - import Mock - - setup do - mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe ".search2" do - test "it returns empty result if user or status search return undefined error", %{conn: conn} do - with_mocks [ - {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, - {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} - ] do - capture_log(fn -> - results = - conn - |> get("/api/v2/search", %{"q" => "2hu"}) - |> json_response(200) - - assert results["accounts"] == [] - assert results["statuses"] == [] - end) =~ - "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" - end - end - - test "search", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu private"}) - - {:ok, _activity} = - CommonAPI.post(user, %{ - "status" => "This is about 2hu, but private", - "visibility" => "private" - }) - - {:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"}) - - conn = get(conn, "/api/v2/search", %{"q" => "2hu #private"}) - - assert results = json_response(conn, 200) - - [account | _] = results["accounts"] - assert account["id"] == to_string(user_three.id) - - assert results["hashtags"] == [ - %{"name" => "private", "url" => "#{Web.base_url()}/tag/private"} - ] - - [status] = results["statuses"] - assert status["id"] == to_string(activity.id) - end - end - - describe ".account_search" do - test "account search", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - results = - conn - |> assign(:user, user) - |> get("/api/v1/accounts/search", %{"q" => "shp"}) - |> json_response(200) - - result_ids = for result <- results, do: result["acct"] - - assert user_two.nickname in result_ids - assert user_three.nickname in result_ids - - results = - conn - |> assign(:user, user) - |> get("/api/v1/accounts/search", %{"q" => "2hu"}) - |> json_response(200) - - result_ids = for result <- results, do: result["acct"] - - assert user_three.nickname in result_ids - end - - test "returns account if query contains a space", %{conn: conn} do - user = insert(:user, %{nickname: "shp@shitposter.club"}) - - results = - conn - |> assign(:user, user) - |> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "}) - |> json_response(200) - - assert length(results) == 1 - end - end - - describe ".search" do - test "it returns empty result if user or status search return undefined error", %{conn: conn} do - with_mocks [ - {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, - {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} - ] do - capture_log(fn -> - results = - conn - |> get("/api/v1/search", %{"q" => "2hu"}) - |> json_response(200) - - assert results["accounts"] == [] - assert results["statuses"] == [] - end) =~ - "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" - end - end - - test "search", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"}) - - {:ok, _activity} = - CommonAPI.post(user, %{ - "status" => "This is about 2hu, but private", - "visibility" => "private" - }) - - {:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"}) - - conn = - conn - |> get("/api/v1/search", %{"q" => "2hu"}) - - assert results = json_response(conn, 200) - - [account | _] = results["accounts"] - assert account["id"] == to_string(user_three.id) - - assert results["hashtags"] == [] - - [status] = results["statuses"] - assert status["id"] == to_string(activity.id) - end - - test "search fetches remote statuses", %{conn: conn} do - capture_log(fn -> - conn = - conn - |> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"}) - - assert results = json_response(conn, 200) - - [status] = results["statuses"] - - assert status["uri"] == - "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" - end) - end - - test "search doesn't show statuses that it shouldn't", %{conn: conn} do - {:ok, activity} = - CommonAPI.post(insert(:user), %{ - "status" => "This is about 2hu, but private", - "visibility" => "private" - }) - - capture_log(fn -> - conn = - conn - |> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]}) - - assert results = json_response(conn, 200) - - [] = results["statuses"] - end) - end - - test "search fetches remote accounts", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "true"}) - - assert results = json_response(conn, 200) - [account] = results["accounts"] - assert account["acct"] == "shp@social.heldscal.la" - end - - test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do - conn = - conn - |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "false"}) - - assert results = json_response(conn, 200) - assert [] == results["accounts"] - end - - test "search with limit and offset", %{conn: conn} do - user = insert(:user) - _user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - _user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, _activity1} = CommonAPI.post(user, %{"status" => "This is about 2hu"}) - {:ok, _activity2} = CommonAPI.post(user, %{"status" => "This is also about 2hu"}) - - result = - conn - |> get("/api/v1/search", %{"q" => "2hu", "limit" => 1}) - - assert results = json_response(result, 200) - assert [%{"id" => activity_id1}] = results["statuses"] - assert [_] = results["accounts"] - - results = - conn - |> get("/api/v1/search", %{"q" => "2hu", "limit" => 1, "offset" => 1}) - |> json_response(200) - - assert [%{"id" => activity_id2}] = results["statuses"] - assert [] = results["accounts"] - - assert activity_id1 != activity_id2 - end - - test "search returns results only for the given type", %{conn: conn} do - user = insert(:user) - _user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, _activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"}) - - assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} = - conn - |> get("/api/v1/search", %{"q" => "2hu", "type" => "statuses"}) - |> json_response(200) - - assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} = - conn - |> get("/api/v1/search", %{"q" => "2hu", "type" => "accounts"}) - |> json_response(200) - end - - test "search uses account_id to filter statuses by the author", %{conn: conn} do - user = insert(:user, %{nickname: "shp@shitposter.club"}) - user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, activity1} = CommonAPI.post(user, %{"status" => "This is about 2hu"}) - {:ok, activity2} = CommonAPI.post(user_two, %{"status" => "This is also about 2hu"}) - - results = - conn - |> get("/api/v1/search", %{"q" => "2hu", "account_id" => user.id}) - |> json_response(200) - - assert [%{"id" => activity_id1}] = results["statuses"] - assert activity_id1 == activity1.id - assert [_] = results["accounts"] - - results = - conn - |> get("/api/v1/search", %{"q" => "2hu", "account_id" => user_two.id}) - |> json_response(200) - - assert [%{"id" => activity_id2}] = results["statuses"] - assert activity_id2 == activity2.id - end - end -end diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs deleted file mode 100644 index 1b6beb6d2..000000000 --- a/test/web/mastodon_api/status_view_test.exs +++ /dev/null @@ -1,584 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.StatusViewTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Bookmark - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.OStatus - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "returns the direct conversation id when given the `with_conversation_id` option" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) - - status = - StatusView.render("status.json", - activity: activity, - with_direct_conversation_id: true, - for: user - ) - - assert status[:pleroma][:direct_conversation_id] - end - - test "returns a temporary ap_id based user for activities missing db users" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) - - Repo.delete(user) - Cachex.clear(:user_cache) - - %{account: ms_user} = StatusView.render("status.json", activity: activity) - - assert ms_user.acct == "erroruser@example.com" - end - - test "tries to get a user by nickname if fetching by ap_id doesn't work" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) - - {:ok, user} = - user - |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"}) - |> Repo.update() - - Cachex.clear(:user_cache) - - result = StatusView.render("status.json", activity: activity) - - assert result[:account][:id] == to_string(user.id) - end - - test "a note with null content" do - note = insert(:note_activity) - note_object = Object.normalize(note) - - data = - note_object.data - |> Map.put("content", nil) - - Object.change(note_object, %{data: data}) - |> Object.update_and_set_cache() - - User.get_cached_by_ap_id(note.data["actor"]) - - status = StatusView.render("status.json", %{activity: note}) - - assert status.content == "" - end - - test "a note activity" do - note = insert(:note_activity) - object_data = Object.normalize(note).data - user = User.get_cached_by_ap_id(note.data["actor"]) - - convo_id = Utils.context_to_conversation_id(object_data["context"]) - - status = StatusView.render("status.json", %{activity: note}) - - created_at = - (object_data["published"] || "") - |> String.replace(~r/\.\d+Z/, ".000Z") - - expected = %{ - id: to_string(note.id), - uri: object_data["id"], - url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note), - account: AccountView.render("account.json", %{user: user}), - in_reply_to_id: nil, - in_reply_to_account_id: nil, - card: nil, - reblog: nil, - content: HtmlSanitizeEx.basic_html(object_data["content"]), - created_at: created_at, - reblogs_count: 0, - replies_count: 0, - favourites_count: 0, - reblogged: false, - bookmarked: false, - favourited: false, - muted: false, - pinned: false, - sensitive: false, - poll: nil, - spoiler_text: HtmlSanitizeEx.basic_html(object_data["summary"]), - visibility: "public", - media_attachments: [], - mentions: [], - tags: [ - %{ - name: "#{object_data["tag"]}", - url: "/tag/#{object_data["tag"]}" - } - ], - application: %{ - name: "Web", - website: nil - }, - language: nil, - emojis: [ - %{ - shortcode: "2hu", - url: "corndog.png", - static_url: "corndog.png", - visible_in_picker: false - } - ], - pleroma: %{ - local: true, - conversation_id: convo_id, - in_reply_to_account_acct: nil, - content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])}, - spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}, - expires_at: nil, - direct_conversation_id: nil - } - } - - assert status == expected - end - - test "tells if the message is muted for some reason" do - user = insert(:user) - other_user = insert(:user) - - {:ok, user} = User.mute(user, other_user) - - {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) - status = StatusView.render("status.json", %{activity: activity}) - - assert status.muted == false - - status = StatusView.render("status.json", %{activity: activity, for: user}) - - assert status.muted == true - end - - test "tells if the status is bookmarked" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{"status" => "Cute girls doing cute things"}) - status = StatusView.render("status.json", %{activity: activity}) - - assert status.bookmarked == false - - status = StatusView.render("status.json", %{activity: activity, for: user}) - - assert status.bookmarked == false - - {:ok, _bookmark} = Bookmark.create(user.id, activity.id) - - activity = Activity.get_by_id_with_object(activity.id) - - status = StatusView.render("status.json", %{activity: activity, for: user}) - - assert status.bookmarked == true - end - - test "a reply" do - note = insert(:note_activity) - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{"status" => "he", "in_reply_to_status_id" => note.id}) - - status = StatusView.render("status.json", %{activity: activity}) - - assert status.in_reply_to_id == to_string(note.id) - - [status] = StatusView.render("index.json", %{activities: [activity], as: :activity}) - - assert status.in_reply_to_id == to_string(note.id) - end - - test "contains mentions" do - incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml") - # a user with this ap id might be in the cache. - recipient = "https://pleroma.soykaf.com/users/lain" - user = insert(:user, %{ap_id: recipient}) - - {:ok, [activity]} = OStatus.handle_incoming(incoming) - - status = StatusView.render("status.json", %{activity: activity}) - - assert status.mentions == - Enum.map([user], fn u -> AccountView.render("mention.json", %{user: u}) end) - end - - test "create mentions from the 'to' field" do - %User{ap_id: recipient_ap_id} = insert(:user) - cc = insert_pair(:user) |> Enum.map(& &1.ap_id) - - object = - insert(:note, %{ - data: %{ - "to" => [recipient_ap_id], - "cc" => cc - } - }) - - activity = - insert(:note_activity, %{ - note: object, - recipients: [recipient_ap_id | cc] - }) - - assert length(activity.recipients) == 3 - - %{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity}) - - assert length(mentions) == 1 - assert mention.url == recipient_ap_id - end - - test "create mentions from the 'tag' field" do - recipient = insert(:user) - cc = insert_pair(:user) |> Enum.map(& &1.ap_id) - - object = - insert(:note, %{ - data: %{ - "cc" => cc, - "tag" => [ - %{ - "href" => recipient.ap_id, - "name" => recipient.nickname, - "type" => "Mention" - }, - %{ - "href" => "https://example.com/search?tag=test", - "name" => "#test", - "type" => "Hashtag" - } - ] - } - }) - - activity = - insert(:note_activity, %{ - note: object, - recipients: [recipient.ap_id | cc] - }) - - assert length(activity.recipients) == 3 - - %{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity}) - - assert length(mentions) == 1 - assert mention.url == recipient.ap_id - end - - test "attachments" do - object = %{ - "type" => "Image", - "url" => [ - %{ - "mediaType" => "image/png", - "href" => "someurl" - } - ], - "uuid" => 6 - } - - expected = %{ - id: "1638338801", - type: "image", - url: "someurl", - remote_url: "someurl", - preview_url: "someurl", - text_url: "someurl", - description: nil, - pleroma: %{mime_type: "image/png"} - } - - assert expected == StatusView.render("attachment.json", %{attachment: object}) - - # If theres a "id", use that instead of the generated one - object = Map.put(object, "id", 2) - assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object}) - end - - test "put the url advertised in the Activity in to the url attribute" do - id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810" - [activity] = Activity.search(nil, id) - - status = StatusView.render("status.json", %{activity: activity}) - - assert status.uri == id - assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" - end - - test "a reblog" do - user = insert(:user) - activity = insert(:note_activity) - - {:ok, reblog, _} = CommonAPI.repeat(activity.id, user) - - represented = StatusView.render("status.json", %{for: user, activity: reblog}) - - assert represented[:id] == to_string(reblog.id) - assert represented[:reblog][:id] == to_string(activity.id) - assert represented[:emojis] == [] - end - - test "a peertube video" do - user = insert(:user) - - {:ok, object} = - Pleroma.Object.Fetcher.fetch_object_from_id( - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - ) - - %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) - - represented = StatusView.render("status.json", %{for: user, activity: activity}) - - assert represented[:id] == to_string(activity.id) - assert length(represented[:media_attachments]) == 1 - end - - describe "build_tags/1" do - test "it returns a a dictionary tags" do - object_tags = [ - "fediverse", - "mastodon", - "nextcloud", - %{ - "href" => "https://kawen.space/users/lain", - "name" => "@lain@kawen.space", - "type" => "Mention" - } - ] - - assert StatusView.build_tags(object_tags) == [ - %{name: "fediverse", url: "/tag/fediverse"}, - %{name: "mastodon", url: "/tag/mastodon"}, - %{name: "nextcloud", url: "/tag/nextcloud"} - ] - end - end - - describe "rich media cards" do - test "a rich media card without a site name renders correctly" do - page_url = "http://example.com" - - card = %{ - url: page_url, - image: page_url <> "/example.jpg", - title: "Example website" - } - - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) - end - - test "a rich media card without a site name or image renders correctly" do - page_url = "http://example.com" - - card = %{ - url: page_url, - title: "Example website" - } - - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) - end - - test "a rich media card without an image renders correctly" do - page_url = "http://example.com" - - card = %{ - url: page_url, - site_name: "Example site name", - title: "Example website" - } - - %{provider_name: "Example site name"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) - end - - test "a rich media card with all relevant data renders correctly" do - page_url = "http://example.com" - - card = %{ - url: page_url, - site_name: "Example site name", - title: "Example website", - image: page_url <> "/example.jpg", - description: "Example description" - } - - %{provider_name: "Example site name"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) - end - end - - describe "poll view" do - test "renders a poll" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - "status" => "Is Tenshi eating a corndog cute?", - "poll" => %{ - "options" => ["absolutely!", "sure", "yes", "why are you even asking?"], - "expires_in" => 20 - } - }) - - object = Object.normalize(activity) - - expected = %{ - emojis: [], - expired: false, - id: to_string(object.id), - multiple: false, - options: [ - %{title: "absolutely!", votes_count: 0}, - %{title: "sure", votes_count: 0}, - %{title: "yes", votes_count: 0}, - %{title: "why are you even asking?", votes_count: 0} - ], - voted: false, - votes_count: 0 - } - - result = StatusView.render("poll.json", %{object: object}) - expires_at = result.expires_at - result = Map.delete(result, :expires_at) - - assert result == expected - - expires_at = NaiveDateTime.from_iso8601!(expires_at) - assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20 - end - - test "detects if it is multiple choice" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - "status" => "Which Mastodon developer is your favourite?", - "poll" => %{ - "options" => ["Gargron", "Eugen"], - "expires_in" => 20, - "multiple" => true - } - }) - - object = Object.normalize(activity) - - assert %{multiple: true} = StatusView.render("poll.json", %{object: object}) - end - - test "detects emoji" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - "status" => "What's with the smug face?", - "poll" => %{ - "options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"], - "expires_in" => 20 - } - }) - - object = Object.normalize(activity) - - assert %{emojis: [%{shortcode: "blank"}]} = - StatusView.render("poll.json", %{object: object}) - end - - test "detects vote status" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - "status" => "Which input devices do you use?", - "poll" => %{ - "options" => ["mouse", "trackball", "trackpoint"], - "multiple" => true, - "expires_in" => 20 - } - }) - - object = Object.normalize(activity) - - {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2]) - - result = StatusView.render("poll.json", %{object: object, for: other_user}) - - assert result[:voted] == true - assert Enum.at(result[:options], 1)[:votes_count] == 1 - assert Enum.at(result[:options], 2)[:votes_count] == 1 - end - end - - test "embeds a relationship in the account" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - "status" => "drink more water" - }) - - result = StatusView.render("status.json", %{activity: activity, for: other_user}) - - assert result[:account][:pleroma][:relationship] == - AccountView.render("relationship.json", %{user: other_user, target: user}) - end - - test "embeds a relationship in the account in reposts" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - "status" => "˙˙ɐʎns" - }) - - {:ok, activity, _object} = CommonAPI.repeat(activity.id, other_user) - - result = StatusView.render("status.json", %{activity: activity, for: user}) - - assert result[:account][:pleroma][:relationship] == - AccountView.render("relationship.json", %{user: user, target: other_user}) - - assert result[:reblog][:account][:pleroma][:relationship] == - AccountView.render("relationship.json", %{user: user, target: user}) - end - - test "visibility/list" do - user = insert(:user) - - {:ok, list} = Pleroma.List.create("foo", user) - - {:ok, activity} = - CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"}) - - status = StatusView.render("status.json", activity: activity) - - assert status.visibility == "list" - end -end diff --git a/test/web/mastodon_api/subscription_controller_test.exs b/test/web/mastodon_api/subscription_controller_test.exs deleted file mode 100644 index 7dfb02f63..000000000 --- a/test/web/mastodon_api/subscription_controller_test.exs +++ /dev/null @@ -1,192 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - alias Pleroma.Web.Push - alias Pleroma.Web.Push.Subscription - - @sub %{ - "endpoint" => "https://example.com/example/1234", - "keys" => %{ - "auth" => "8eDyX_uCN0XRhSbY5hs7Hg==", - "p256dh" => - "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" - } - } - @server_key Keyword.get(Push.vapid_config(), :public_key) - - setup do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: ["push"]) - - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, token) - - %{conn: conn, user: user, token: token} - end - - defmacro assert_error_when_disable_push(do: yield) do - quote do - vapid_details = Application.get_env(:web_push_encryption, :vapid_details, []) - Application.put_env(:web_push_encryption, :vapid_details, []) - assert "Something went wrong" == unquote(yield) - Application.put_env(:web_push_encryption, :vapid_details, vapid_details) - end - end - - describe "creates push subscription" do - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> post("/api/v1/push/subscription", %{}) - |> json_response(500) - end - end - - test "successful creation", %{conn: conn} do - result = - conn - |> post("/api/v1/push/subscription", %{ - "data" => %{"alerts" => %{"mention" => true, "test" => true}}, - "subscription" => @sub - }) - |> json_response(200) - - [subscription] = Pleroma.Repo.all(Subscription) - - assert %{ - "alerts" => %{"mention" => true}, - "endpoint" => subscription.endpoint, - "id" => to_string(subscription.id), - "server_key" => @server_key - } == result - end - end - - describe "gets a user subscription" do - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> get("/api/v1/push/subscription", %{}) - |> json_response(500) - end - end - - test "returns error when user hasn't subscription", %{conn: conn} do - res = - conn - |> get("/api/v1/push/subscription", %{}) - |> json_response(404) - - assert "Not found" == res - end - - test "returns a user subsciption", %{conn: conn, user: user, token: token} do - subscription = - insert(:push_subscription, - user: user, - token: token, - data: %{"alerts" => %{"mention" => true}} - ) - - res = - conn - |> get("/api/v1/push/subscription", %{}) - |> json_response(200) - - expect = %{ - "alerts" => %{"mention" => true}, - "endpoint" => "https://example.com/example/1234", - "id" => to_string(subscription.id), - "server_key" => @server_key - } - - assert expect == res - end - end - - describe "updates a user subsciption" do - setup %{conn: conn, user: user, token: token} do - subscription = - insert(:push_subscription, - user: user, - token: token, - data: %{"alerts" => %{"mention" => true}} - ) - - %{conn: conn, user: user, token: token, subscription: subscription} - end - - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}}) - |> json_response(500) - end - end - - test "returns updated subsciption", %{conn: conn, subscription: subscription} do - res = - conn - |> put("/api/v1/push/subscription", %{ - data: %{"alerts" => %{"mention" => false, "follow" => true}} - }) - |> json_response(200) - - expect = %{ - "alerts" => %{"follow" => true, "mention" => false}, - "endpoint" => "https://example.com/example/1234", - "id" => to_string(subscription.id), - "server_key" => @server_key - } - - assert expect == res - end - end - - describe "deletes the user subscription" do - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> delete("/api/v1/push/subscription", %{}) - |> json_response(500) - end - end - - test "returns error when user hasn't subscription", %{conn: conn} do - res = - conn - |> delete("/api/v1/push/subscription", %{}) - |> json_response(404) - - assert "Not found" == res - end - - test "returns empty result and delete user subsciption", %{ - conn: conn, - user: user, - token: token - } do - subscription = - insert(:push_subscription, - user: user, - token: token, - data: %{"alerts" => %{"mention" => true}} - ) - - res = - conn - |> delete("/api/v1/push/subscription", %{}) - |> json_response(200) - - assert %{} == res - refute Pleroma.Repo.get(Subscription, subscription.id) - end - end -end diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs new file mode 100644 index 000000000..1d8b28339 --- /dev/null +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -0,0 +1,389 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AccountViewTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.AccountView + + test "Represent a user account" do + source_data = %{ + "tag" => [ + %{ + "type" => "Emoji", + "icon" => %{"url" => "/file.png"}, + "name" => ":karjalanpiirakka:" + } + ] + } + + background_image = %{ + "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}] + } + + user = + insert(:user, %{ + info: %{ + note_count: 5, + follower_count: 3, + source_data: source_data, + background: background_image + }, + nickname: "shp@shitposter.club", + name: ":karjalanpiirakka: shp", + bio: "valid html", + inserted_at: ~N[2017-08-15 15:47:06.597036] + }) + + expected = %{ + id: to_string(user.id), + username: "shp", + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: "2017-08-15T15:47:06.000Z", + followers_count: 3, + following_count: 0, + statuses_count: 5, + note: "valid html", + url: user.ap_id, + avatar: "http://localhost:4001/images/avi.png", + avatar_static: "http://localhost:4001/images/avi.png", + header: "http://localhost:4001/images/banner.png", + header_static: "http://localhost:4001/images/banner.png", + emojis: [ + %{ + "static_url" => "/file.png", + "url" => "/file.png", + "shortcode" => "karjalanpiirakka", + "visible_in_picker" => false + } + ], + fields: [], + bot: false, + source: %{ + note: "valid html", + sensitive: false, + pleroma: %{}, + fields: [] + }, + pleroma: %{ + background_image: "https://example.com/images/asuka_hospital.png", + confirmation_pending: false, + tags: [], + is_admin: false, + is_moderator: false, + hide_favorites: true, + hide_followers: false, + hide_follows: false, + relationship: %{}, + skip_thread_containment: false + } + } + + assert expected == AccountView.render("account.json", %{user: user}) + end + + test "Represent the user account for the account owner" do + user = insert(:user) + + notification_settings = %{ + "followers" => true, + "follows" => true, + "non_follows" => true, + "non_followers" => true + } + + privacy = user.info.default_scope + + assert %{ + pleroma: %{notification_settings: ^notification_settings}, + source: %{privacy: ^privacy} + } = AccountView.render("account.json", %{user: user, for: user}) + end + + test "Represent a Service(bot) account" do + user = + insert(:user, %{ + info: %{note_count: 5, follower_count: 3, source_data: %{"type" => "Service"}}, + nickname: "shp@shitposter.club", + inserted_at: ~N[2017-08-15 15:47:06.597036] + }) + + expected = %{ + id: to_string(user.id), + username: "shp", + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: "2017-08-15T15:47:06.000Z", + followers_count: 3, + following_count: 0, + statuses_count: 5, + note: user.bio, + url: user.ap_id, + avatar: "http://localhost:4001/images/avi.png", + avatar_static: "http://localhost:4001/images/avi.png", + header: "http://localhost:4001/images/banner.png", + header_static: "http://localhost:4001/images/banner.png", + emojis: [], + fields: [], + bot: true, + source: %{ + note: user.bio, + sensitive: false, + pleroma: %{}, + fields: [] + }, + pleroma: %{ + background_image: nil, + confirmation_pending: false, + tags: [], + is_admin: false, + is_moderator: false, + hide_favorites: true, + hide_followers: false, + hide_follows: false, + relationship: %{}, + skip_thread_containment: false + } + } + + assert expected == AccountView.render("account.json", %{user: user}) + end + + test "Represent a deactivated user for an admin" do + admin = insert(:user, %{info: %{is_admin: true}}) + deactivated_user = insert(:user, %{info: %{deactivated: true}}) + represented = AccountView.render("account.json", %{user: deactivated_user, for: admin}) + assert represented[:pleroma][:deactivated] == true + end + + test "Represent a smaller mention" do + user = insert(:user) + + expected = %{ + id: to_string(user.id), + acct: user.nickname, + username: user.nickname, + url: user.ap_id + } + + assert expected == AccountView.render("mention.json", %{user: user}) + end + + describe "relationship" do + test "represent a relationship for the following and followed user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.follow(user, other_user) + {:ok, other_user} = User.follow(other_user, user) + {:ok, other_user} = User.subscribe(user, other_user) + {:ok, user} = User.mute(user, other_user, true) + {:ok, user} = CommonAPI.hide_reblogs(user, other_user) + + expected = %{ + id: to_string(other_user.id), + following: true, + followed_by: true, + blocking: false, + blocked_by: false, + muting: true, + muting_notifications: true, + subscribing: true, + requested: false, + domain_blocking: false, + showing_reblogs: false, + endorsed: false + } + + assert expected == + AccountView.render("relationship.json", %{user: user, target: other_user}) + end + + test "represent a relationship for the blocking and blocked user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.follow(user, other_user) + {:ok, other_user} = User.subscribe(user, other_user) + {:ok, user} = User.block(user, other_user) + {:ok, other_user} = User.block(other_user, user) + + expected = %{ + id: to_string(other_user.id), + following: false, + followed_by: false, + blocking: true, + blocked_by: true, + muting: false, + muting_notifications: false, + subscribing: false, + requested: false, + domain_blocking: false, + showing_reblogs: true, + endorsed: false + } + + assert expected == + AccountView.render("relationship.json", %{user: user, target: other_user}) + end + + test "represent a relationship for the user blocking a domain" do + user = insert(:user) + other_user = insert(:user, ap_id: "https://bad.site/users/other_user") + + {:ok, user} = User.block_domain(user, "bad.site") + + assert %{domain_blocking: true, blocking: false} = + AccountView.render("relationship.json", %{user: user, target: other_user}) + end + + test "represent a relationship for the user with a pending follow request" do + user = insert(:user) + other_user = insert(:user, %{info: %User.Info{locked: true}}) + + {:ok, user, other_user, _} = CommonAPI.follow(user, other_user) + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + expected = %{ + id: to_string(other_user.id), + following: false, + followed_by: false, + blocking: false, + blocked_by: false, + muting: false, + muting_notifications: false, + subscribing: false, + requested: true, + domain_blocking: false, + showing_reblogs: true, + endorsed: false + } + + assert expected == + AccountView.render("relationship.json", %{user: user, target: other_user}) + end + end + + test "represent an embedded relationship" do + user = + insert(:user, %{ + info: %{note_count: 5, follower_count: 0, source_data: %{"type" => "Service"}}, + nickname: "shp@shitposter.club", + inserted_at: ~N[2017-08-15 15:47:06.597036] + }) + + other_user = insert(:user) + {:ok, other_user} = User.follow(other_user, user) + {:ok, other_user} = User.block(other_user, user) + {:ok, _} = User.follow(insert(:user), user) + + expected = %{ + id: to_string(user.id), + username: "shp", + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: "2017-08-15T15:47:06.000Z", + followers_count: 1, + following_count: 0, + statuses_count: 5, + note: user.bio, + url: user.ap_id, + avatar: "http://localhost:4001/images/avi.png", + avatar_static: "http://localhost:4001/images/avi.png", + header: "http://localhost:4001/images/banner.png", + header_static: "http://localhost:4001/images/banner.png", + emojis: [], + fields: [], + bot: true, + source: %{ + note: user.bio, + sensitive: false, + pleroma: %{}, + fields: [] + }, + pleroma: %{ + background_image: nil, + confirmation_pending: false, + tags: [], + is_admin: false, + is_moderator: false, + hide_favorites: true, + hide_followers: false, + hide_follows: false, + relationship: %{ + id: to_string(user.id), + following: false, + followed_by: false, + blocking: true, + blocked_by: false, + subscribing: false, + muting: false, + muting_notifications: false, + requested: false, + domain_blocking: false, + showing_reblogs: true, + endorsed: false + }, + skip_thread_containment: false + } + } + + assert expected == AccountView.render("account.json", %{user: user, for: other_user}) + end + + test "returns the settings store if the requesting user is the represented user and it's requested specifically" do + user = insert(:user, %{info: %User.Info{pleroma_settings_store: %{fe: "test"}}}) + + result = + AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) + + assert result.pleroma.settings_store == %{:fe => "test"} + + result = AccountView.render("account.json", %{user: user, with_pleroma_settings: true}) + assert result.pleroma[:settings_store] == nil + + result = AccountView.render("account.json", %{user: user, for: user}) + assert result.pleroma[:settings_store] == nil + end + + test "sanitizes display names" do + user = insert(:user, name: " username ") + result = AccountView.render("account.json", %{user: user}) + refute result.display_name == " username " + end + + describe "hiding follows/following" do + test "shows when follows/following are hidden and sets follower/following count to 0" do + user = insert(:user, info: %{hide_followers: true, hide_follows: true}) + other_user = insert(:user) + {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{ + followers_count: 0, + following_count: 0, + pleroma: %{hide_follows: true, hide_followers: true} + } = AccountView.render("account.json", %{user: user}) + end + + test "shows actual follower/following count to the account owner" do + user = insert(:user, info: %{hide_followers: true, hide_follows: true}) + other_user = insert(:user) + {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{ + followers_count: 1, + following_count: 1 + } = AccountView.render("account.json", %{user: user, for: user}) + end + end +end diff --git a/test/web/mastodon_api/views/conversation_view_test.exs b/test/web/mastodon_api/views/conversation_view_test.exs new file mode 100644 index 000000000..a2a880705 --- /dev/null +++ b/test/web/mastodon_api/views/conversation_view_test.exs @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do + use Pleroma.DataCase + + alias Pleroma.Conversation.Participation + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.ConversationView + + import Pleroma.Factory + + test "represents a Mastodon Conversation entity" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}", "visibility" => "direct"}) + + [participation] = Participation.for_user_with_last_activity_id(user) + + assert participation + + conversation = + ConversationView.render("participation.json", %{participation: participation, for: user}) + + assert conversation.id == participation.id |> to_string() + assert conversation.last_status.id == activity.id + + assert [account] = conversation.accounts + assert account.id == other_user.id + end +end diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs new file mode 100644 index 000000000..977ea1e87 --- /dev/null +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -0,0 +1,104 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.NotificationView + alias Pleroma.Web.MastodonAPI.StatusView + import Pleroma.Factory + + test "Mention notification" do + user = insert(:user) + mentioned_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{mentioned_user.nickname}"}) + {:ok, [notification]} = Notification.create_notifications(activity) + user = User.get_cached_by_id(user.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "mention", + account: AccountView.render("account.json", %{user: user, for: mentioned_user}), + status: StatusView.render("status.json", %{activity: activity, for: mentioned_user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + result = + NotificationView.render("index.json", %{notifications: [notification], for: mentioned_user}) + + assert [expected] == result + end + + test "Favourite notification" do + user = insert(:user) + another_user = insert(:user) + {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"}) + {:ok, favorite_activity, _object} = CommonAPI.favorite(create_activity.id, another_user) + {:ok, [notification]} = Notification.create_notifications(favorite_activity) + create_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "favourite", + account: AccountView.render("account.json", %{user: another_user, for: user}), + status: StatusView.render("status.json", %{activity: create_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + result = NotificationView.render("index.json", %{notifications: [notification], for: user}) + + assert [expected] == result + end + + test "Reblog notification" do + user = insert(:user) + another_user = insert(:user) + {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"}) + {:ok, reblog_activity, _object} = CommonAPI.repeat(create_activity.id, another_user) + {:ok, [notification]} = Notification.create_notifications(reblog_activity) + reblog_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "reblog", + account: AccountView.render("account.json", %{user: another_user, for: user}), + status: StatusView.render("status.json", %{activity: reblog_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + result = NotificationView.render("index.json", %{notifications: [notification], for: user}) + + assert [expected] == result + end + + test "Follow notification" do + follower = insert(:user) + followed = insert(:user) + {:ok, follower, followed, _activity} = CommonAPI.follow(follower, followed) + notification = Notification |> Repo.one() |> Repo.preload(:activity) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "follow", + account: AccountView.render("account.json", %{user: follower, for: followed}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + result = + NotificationView.render("index.json", %{notifications: [notification], for: followed}) + + assert [expected] == result + end +end diff --git a/test/web/mastodon_api/views/push_subscription_view_test.exs b/test/web/mastodon_api/views/push_subscription_view_test.exs new file mode 100644 index 000000000..dc935fc82 --- /dev/null +++ b/test/web/mastodon_api/views/push_subscription_view_test.exs @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.PushSubscriptionViewTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View + alias Pleroma.Web.Push + + test "Represent a subscription" do + subscription = insert(:push_subscription, data: %{"alerts" => %{"mention" => true}}) + + expected = %{ + alerts: %{"mention" => true}, + endpoint: subscription.endpoint, + id: to_string(subscription.id), + server_key: Keyword.get(Push.vapid_config(), :public_key) + } + + assert expected == View.render("push_subscription.json", %{subscription: subscription}) + end +end diff --git a/test/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/web/mastodon_api/views/scheduled_activity_view_test.exs new file mode 100644 index 000000000..ecbb855d4 --- /dev/null +++ b/test/web/mastodon_api/views/scheduled_activity_view_test.exs @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do + use Pleroma.DataCase + alias Pleroma.ScheduledActivity + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.ScheduledActivityView + alias Pleroma.Web.MastodonAPI.StatusView + import Pleroma.Factory + + test "A scheduled activity with a media attachment" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "hi"}) + + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(10), :millisecond) + |> NaiveDateTime.to_iso8601() + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + attrs = %{ + params: %{ + "media_ids" => [upload.id], + "status" => "hi", + "sensitive" => true, + "spoiler_text" => "spoiler", + "visibility" => "unlisted", + "in_reply_to_id" => to_string(activity.id) + }, + scheduled_at: scheduled_at + } + + {:ok, scheduled_activity} = ScheduledActivity.create(user, attrs) + result = ScheduledActivityView.render("show.json", %{scheduled_activity: scheduled_activity}) + + expected = %{ + id: to_string(scheduled_activity.id), + media_attachments: + %{"media_ids" => [upload.id]} + |> Utils.attachments_from_ids() + |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), + params: %{ + in_reply_to_id: to_string(activity.id), + media_ids: [upload.id], + poll: nil, + scheduled_at: nil, + sensitive: true, + spoiler_text: "spoiler", + text: "hi", + visibility: "unlisted" + }, + scheduled_at: Utils.to_masto_date(scheduled_activity.scheduled_at) + } + + assert expected == result + end +end diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs new file mode 100644 index 000000000..1b6beb6d2 --- /dev/null +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -0,0 +1,584 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.StatusViewTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.OStatus + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "returns the direct conversation id when given the `with_conversation_id` option" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + + status = + StatusView.render("status.json", + activity: activity, + with_direct_conversation_id: true, + for: user + ) + + assert status[:pleroma][:direct_conversation_id] + end + + test "returns a temporary ap_id based user for activities missing db users" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + + Repo.delete(user) + Cachex.clear(:user_cache) + + %{account: ms_user} = StatusView.render("status.json", activity: activity) + + assert ms_user.acct == "erroruser@example.com" + end + + test "tries to get a user by nickname if fetching by ap_id doesn't work" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + + {:ok, user} = + user + |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"}) + |> Repo.update() + + Cachex.clear(:user_cache) + + result = StatusView.render("status.json", activity: activity) + + assert result[:account][:id] == to_string(user.id) + end + + test "a note with null content" do + note = insert(:note_activity) + note_object = Object.normalize(note) + + data = + note_object.data + |> Map.put("content", nil) + + Object.change(note_object, %{data: data}) + |> Object.update_and_set_cache() + + User.get_cached_by_ap_id(note.data["actor"]) + + status = StatusView.render("status.json", %{activity: note}) + + assert status.content == "" + end + + test "a note activity" do + note = insert(:note_activity) + object_data = Object.normalize(note).data + user = User.get_cached_by_ap_id(note.data["actor"]) + + convo_id = Utils.context_to_conversation_id(object_data["context"]) + + status = StatusView.render("status.json", %{activity: note}) + + created_at = + (object_data["published"] || "") + |> String.replace(~r/\.\d+Z/, ".000Z") + + expected = %{ + id: to_string(note.id), + uri: object_data["id"], + url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note), + account: AccountView.render("account.json", %{user: user}), + in_reply_to_id: nil, + in_reply_to_account_id: nil, + card: nil, + reblog: nil, + content: HtmlSanitizeEx.basic_html(object_data["content"]), + created_at: created_at, + reblogs_count: 0, + replies_count: 0, + favourites_count: 0, + reblogged: false, + bookmarked: false, + favourited: false, + muted: false, + pinned: false, + sensitive: false, + poll: nil, + spoiler_text: HtmlSanitizeEx.basic_html(object_data["summary"]), + visibility: "public", + media_attachments: [], + mentions: [], + tags: [ + %{ + name: "#{object_data["tag"]}", + url: "/tag/#{object_data["tag"]}" + } + ], + application: %{ + name: "Web", + website: nil + }, + language: nil, + emojis: [ + %{ + shortcode: "2hu", + url: "corndog.png", + static_url: "corndog.png", + visible_in_picker: false + } + ], + pleroma: %{ + local: true, + conversation_id: convo_id, + in_reply_to_account_acct: nil, + content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])}, + spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}, + expires_at: nil, + direct_conversation_id: nil + } + } + + assert status == expected + end + + test "tells if the message is muted for some reason" do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.mute(user, other_user) + + {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) + status = StatusView.render("status.json", %{activity: activity}) + + assert status.muted == false + + status = StatusView.render("status.json", %{activity: activity, for: user}) + + assert status.muted == true + end + + test "tells if the status is bookmarked" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Cute girls doing cute things"}) + status = StatusView.render("status.json", %{activity: activity}) + + assert status.bookmarked == false + + status = StatusView.render("status.json", %{activity: activity, for: user}) + + assert status.bookmarked == false + + {:ok, _bookmark} = Bookmark.create(user.id, activity.id) + + activity = Activity.get_by_id_with_object(activity.id) + + status = StatusView.render("status.json", %{activity: activity, for: user}) + + assert status.bookmarked == true + end + + test "a reply" do + note = insert(:note_activity) + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "he", "in_reply_to_status_id" => note.id}) + + status = StatusView.render("status.json", %{activity: activity}) + + assert status.in_reply_to_id == to_string(note.id) + + [status] = StatusView.render("index.json", %{activities: [activity], as: :activity}) + + assert status.in_reply_to_id == to_string(note.id) + end + + test "contains mentions" do + incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml") + # a user with this ap id might be in the cache. + recipient = "https://pleroma.soykaf.com/users/lain" + user = insert(:user, %{ap_id: recipient}) + + {:ok, [activity]} = OStatus.handle_incoming(incoming) + + status = StatusView.render("status.json", %{activity: activity}) + + assert status.mentions == + Enum.map([user], fn u -> AccountView.render("mention.json", %{user: u}) end) + end + + test "create mentions from the 'to' field" do + %User{ap_id: recipient_ap_id} = insert(:user) + cc = insert_pair(:user) |> Enum.map(& &1.ap_id) + + object = + insert(:note, %{ + data: %{ + "to" => [recipient_ap_id], + "cc" => cc + } + }) + + activity = + insert(:note_activity, %{ + note: object, + recipients: [recipient_ap_id | cc] + }) + + assert length(activity.recipients) == 3 + + %{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity}) + + assert length(mentions) == 1 + assert mention.url == recipient_ap_id + end + + test "create mentions from the 'tag' field" do + recipient = insert(:user) + cc = insert_pair(:user) |> Enum.map(& &1.ap_id) + + object = + insert(:note, %{ + data: %{ + "cc" => cc, + "tag" => [ + %{ + "href" => recipient.ap_id, + "name" => recipient.nickname, + "type" => "Mention" + }, + %{ + "href" => "https://example.com/search?tag=test", + "name" => "#test", + "type" => "Hashtag" + } + ] + } + }) + + activity = + insert(:note_activity, %{ + note: object, + recipients: [recipient.ap_id | cc] + }) + + assert length(activity.recipients) == 3 + + %{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity}) + + assert length(mentions) == 1 + assert mention.url == recipient.ap_id + end + + test "attachments" do + object = %{ + "type" => "Image", + "url" => [ + %{ + "mediaType" => "image/png", + "href" => "someurl" + } + ], + "uuid" => 6 + } + + expected = %{ + id: "1638338801", + type: "image", + url: "someurl", + remote_url: "someurl", + preview_url: "someurl", + text_url: "someurl", + description: nil, + pleroma: %{mime_type: "image/png"} + } + + assert expected == StatusView.render("attachment.json", %{attachment: object}) + + # If theres a "id", use that instead of the generated one + object = Map.put(object, "id", 2) + assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object}) + end + + test "put the url advertised in the Activity in to the url attribute" do + id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810" + [activity] = Activity.search(nil, id) + + status = StatusView.render("status.json", %{activity: activity}) + + assert status.uri == id + assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" + end + + test "a reblog" do + user = insert(:user) + activity = insert(:note_activity) + + {:ok, reblog, _} = CommonAPI.repeat(activity.id, user) + + represented = StatusView.render("status.json", %{for: user, activity: reblog}) + + assert represented[:id] == to_string(reblog.id) + assert represented[:reblog][:id] == to_string(activity.id) + assert represented[:emojis] == [] + end + + test "a peertube video" do + user = insert(:user) + + {:ok, object} = + Pleroma.Object.Fetcher.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + represented = StatusView.render("status.json", %{for: user, activity: activity}) + + assert represented[:id] == to_string(activity.id) + assert length(represented[:media_attachments]) == 1 + end + + describe "build_tags/1" do + test "it returns a a dictionary tags" do + object_tags = [ + "fediverse", + "mastodon", + "nextcloud", + %{ + "href" => "https://kawen.space/users/lain", + "name" => "@lain@kawen.space", + "type" => "Mention" + } + ] + + assert StatusView.build_tags(object_tags) == [ + %{name: "fediverse", url: "/tag/fediverse"}, + %{name: "mastodon", url: "/tag/mastodon"}, + %{name: "nextcloud", url: "/tag/nextcloud"} + ] + end + end + + describe "rich media cards" do + test "a rich media card without a site name renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + image: page_url <> "/example.jpg", + title: "Example website" + } + + %{provider_name: "example.com"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + + test "a rich media card without a site name or image renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + title: "Example website" + } + + %{provider_name: "example.com"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + + test "a rich media card without an image renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + site_name: "Example site name", + title: "Example website" + } + + %{provider_name: "Example site name"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + + test "a rich media card with all relevant data renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + site_name: "Example site name", + title: "Example website", + image: page_url <> "/example.jpg", + description: "Example description" + } + + %{provider_name: "Example site name"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + end + + describe "poll view" do + test "renders a poll" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Is Tenshi eating a corndog cute?", + "poll" => %{ + "options" => ["absolutely!", "sure", "yes", "why are you even asking?"], + "expires_in" => 20 + } + }) + + object = Object.normalize(activity) + + expected = %{ + emojis: [], + expired: false, + id: to_string(object.id), + multiple: false, + options: [ + %{title: "absolutely!", votes_count: 0}, + %{title: "sure", votes_count: 0}, + %{title: "yes", votes_count: 0}, + %{title: "why are you even asking?", votes_count: 0} + ], + voted: false, + votes_count: 0 + } + + result = StatusView.render("poll.json", %{object: object}) + expires_at = result.expires_at + result = Map.delete(result, :expires_at) + + assert result == expected + + expires_at = NaiveDateTime.from_iso8601!(expires_at) + assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20 + end + + test "detects if it is multiple choice" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Which Mastodon developer is your favourite?", + "poll" => %{ + "options" => ["Gargron", "Eugen"], + "expires_in" => 20, + "multiple" => true + } + }) + + object = Object.normalize(activity) + + assert %{multiple: true} = StatusView.render("poll.json", %{object: object}) + end + + test "detects emoji" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "What's with the smug face?", + "poll" => %{ + "options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"], + "expires_in" => 20 + } + }) + + object = Object.normalize(activity) + + assert %{emojis: [%{shortcode: "blank"}]} = + StatusView.render("poll.json", %{object: object}) + end + + test "detects vote status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Which input devices do you use?", + "poll" => %{ + "options" => ["mouse", "trackball", "trackpoint"], + "multiple" => true, + "expires_in" => 20 + } + }) + + object = Object.normalize(activity) + + {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2]) + + result = StatusView.render("poll.json", %{object: object, for: other_user}) + + assert result[:voted] == true + assert Enum.at(result[:options], 1)[:votes_count] == 1 + assert Enum.at(result[:options], 2)[:votes_count] == 1 + end + end + + test "embeds a relationship in the account" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "drink more water" + }) + + result = StatusView.render("status.json", %{activity: activity, for: other_user}) + + assert result[:account][:pleroma][:relationship] == + AccountView.render("relationship.json", %{user: other_user, target: user}) + end + + test "embeds a relationship in the account in reposts" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "˙˙ɐʎns" + }) + + {:ok, activity, _object} = CommonAPI.repeat(activity.id, other_user) + + result = StatusView.render("status.json", %{activity: activity, for: user}) + + assert result[:account][:pleroma][:relationship] == + AccountView.render("relationship.json", %{user: user, target: other_user}) + + assert result[:reblog][:account][:pleroma][:relationship] == + AccountView.render("relationship.json", %{user: user, target: user}) + end + + test "visibility/list" do + user = insert(:user) + + {:ok, list} = Pleroma.List.create("foo", user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"}) + + status = StatusView.render("status.json", activity: activity) + + assert status.visibility == "list" + end +end -- cgit v1.2.3 From 66c1966688e9bb24ce1703217b89d8ec390b6095 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 26 Aug 2019 20:36:44 +0700 Subject: Disable rate limiter by default --- config/config.exs | 11 +---------- docs/config.md | 2 ++ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/config/config.exs b/config/config.exs index e58454d68..f630771a3 100644 --- a/config/config.exs +++ b/config/config.exs @@ -556,16 +556,7 @@ config :pleroma, :env, Mix.env() config :http_signatures, adapter: Pleroma.Signature -config :pleroma, :rate_limit, - search: [{1000, 10}, {1000, 30}], - app_account_creation: {1_800_000, 25}, - relations_actions: {10_000, 10}, - relation_id_action: {60_000, 2}, - statuses_actions: {10_000, 15}, - status_id_action: {60_000, 3}, - password_reset: {1_800_000, 5}, - account_confirmation_resend: {8_640_000, 5}, - ap_routes: {60_000, 15} +config :pleroma, :rate_limit, nil config :pleroma, Pleroma.ActivityExpiration, enabled: true diff --git a/docs/config.md b/docs/config.md index 414b54660..61aa7db9b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -671,6 +671,8 @@ This will probably take a long time. ## :rate_limit +This is an advanced feature and disabled by default. + A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where: * The first element: `scale` (Integer). The time scale in milliseconds. -- cgit v1.2.3 From c338224c93c3e8111cecdd3ef652016a574b55f4 Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Mon, 26 Aug 2019 17:24:22 +0300 Subject: Fix sporadic test --- test/web/admin_api/admin_api_controller_test.exs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 1afdb6a50..4e2c27431 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -64,8 +64,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do log_entry = Repo.one(ModerationLog) - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} created users: @lain2, @lain" + assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] end test "Cannot create user with exisiting email" do -- cgit v1.2.3 From fd076def0a2d42ca4b406cdde3fc54b665512362 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 27 Aug 2019 02:24:14 +0700 Subject: Fix typo --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 61aa7db9b..7a8819c91 100644 --- a/docs/config.md +++ b/docs/config.md @@ -8,7 +8,7 @@ If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherw * `filters`: List of `Pleroma.Upload.Filter` to use. * `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe` * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host. -* `proxy_remote`: If you\'re using a remote uploader, Pleroma will proxy media requests instead of redirecting to it. +* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it. * `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation. Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`. -- cgit v1.2.3 From 3da65292b389c1f1edeff03fd5097579721fb681 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 26 Aug 2019 14:34:52 -0500 Subject: Transmogrifier: Fix follow handling when the actor is an object. --- CHANGELOG.md | 1 + lib/pleroma/object.ex | 4 ++ lib/pleroma/web/activity_pub/transmogrifier.ex | 4 +- test/fixtures/osada-follow-activity.json | 56 ++++++++++++++++++++++ test/fixtures/tesla_mock/osada-user-indio.json | 1 + test/support/http_request_mock.ex | 5 ++ .../transmogrifier/follow_handling_test.exs | 19 ++++++++ 7 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/osada-follow-activity.json create mode 100644 test/fixtures/tesla_mock/osada-user-indio.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fdcb014a..20af9badc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Improve digest email template ### Fixed +- Following from Osada - Not being able to pin unlisted posts - Objects being re-embedded to activities after being updated (e.g faved/reposted). Running 'mix pleroma.database prune_objects' again is advised. - Favorites timeline doing database-intensive queries diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index c8d339c19..468549c87 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -230,4 +230,8 @@ defmodule Pleroma.Object do _ -> :noop end end + + def get_ap_id(%{"id" => id}), do: id + def get_ap_id(id) when is_binary(id), do: id + def get_ap_id(_), do: {:error, "Object is not a string and has no id."} end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 36340a3a1..6c4259c02 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -464,8 +464,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data, _options ) do - with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), - {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), + with %User{local: true} = followed <- User.get_cached_by_ap_id(Object.get_ap_id(followed)), + {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(Object.get_ap_id(follower)), {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, diff --git a/test/fixtures/osada-follow-activity.json b/test/fixtures/osada-follow-activity.json new file mode 100644 index 000000000..b991eea36 --- /dev/null +++ b/test/fixtures/osada-follow-activity.json @@ -0,0 +1,56 @@ +{ + "@context":[ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + "https://apfed.club/apschema/v1.4" + ], + "id":"https://apfed.club/follow/9", + "type":"Follow", + "actor":{ + "type":"Person", + "id":"https://apfed.club/channel/indio", + "preferredUsername":"indio", + "name":"Indio", + "updated":"2019-08-20T23:52:34Z", + "icon":{ + "type":"Image", + "mediaType":"image/jpeg", + "updated":"2019-08-20T23:53:37Z", + "url":"https://apfed.club/photo/profile/l/2", + "height":300, + "width":300 + }, + "url":"https://apfed.club/channel/indio", + "inbox":"https://apfed.club/inbox/indio", + "outbox":"https://apfed.club/outbox/indio", + "followers":"https://apfed.club/followers/indio", + "following":"https://apfed.club/following/indio", + "endpoints":{ + "sharedInbox":"https://apfed.club/inbox" + }, + "publicKey":{ + "id":"https://apfed.club/channel/indio", + "owner":"https://apfed.club/channel/indio", + "publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA77TIR1VuSYFnmDRFGHHb\n4vaGdx9ranzRX4bfOKAqa++Ch5L4EqJpPy08RuM+NrYCYiYl4QQFDSSDXAEgb5g9\nC1TgWTfI7q/E0UBX2Vr0mU6X4i1ztv0tuQvegRjcSJ7l1AvoBs8Ip4MEJ3OPEQhB\ngJqAACB3Gnps4zi2I0yavkxUfGVKr6zKT3BxWh5hTpKC7Do+ChIrVZC2EwxND9K6 +\nsAnQHThcb5EQuvuzUQZKeS7IEOsd0JpZDmJjbfMGrAWE81pLIfEeeA2joCJiBBTO\nglDsW+juvZ+lWqJpMr2hMWpvfrFjJeUawNJCIzsLdVIZR+aKj5yy6yqoS8hkN9Ha\n1MljZpsXl+EmwcwAIqim1YeLwERCEAQ/JWbSt8pQTQbzZ6ibwQ4mchCxacrRbIVR +\nnL59fWMBassJcbY0VwrTugm2SBsYbDjESd55UZV03Rwr8qseGTyi+hH8O7w2SIaY\nzjN6AdZiPmsh00YflzlCk8MSLOHMol1vqIUzXxU8CdXn9+KsuQdZGrTz0YKN/db4\naVwUGJatz2Tsvf7R1tJBjJfeQWOWbbn3pycLVH86LjZ83qngp9ZVnAveUnUqz0yS +\nhe+buZ6UMsfGzbIYon2bKNlz6gYTH0YPcr+cLe+29drtt0GZiXha1agbpo4RB8zE +\naNL2fucF5YT0yNpbd/5WoV0CAwEAAQ==\n-----END PUBLIC KEY-----\n" + } + }, + "object":"https://pleroma.site/users/kaniini", + "to":[ + "https://pleroma.site/users/kaniini" + ], + "signature":{ + "@context":[ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1" + ], + "type":"RsaSignature2017", + "nonce":"52c035e0a9e81dce8b486159204e97c22637e91f75cdfad5378de91de68e9117", + "creator":"https://apfed.club/channel/indio/public_key_pem", + "created":"2019-08-22T03:38:02Z", + "signatureValue":"oVliRCIqNIh6yUp851dYrF0y21aHp3Rz6VkIpW1pFMWfXuzExyWSfcELpyLseeRmsw5bUu9zJkH44B4G2LiJQKA9UoEQDjrDMZBmbeUpiQqq3DVUzkrBOI8bHZ7xyJ/CjSZcNHHh0MHhSKxswyxWMGi4zIqzkAZG3vRRgoPVHdjPm00sR3B8jBLw1cjoffv+KKeM/zEUpe13gqX9qHAWHHqZepxgSWmq+EKOkRvHUPBXiEJZfXzc5uW+vZ09F3WBYmaRoy8Y0e1P29fnRLqSy7EEINdrHaGclRqoUZyiawpkgy3lWWlynesV/HiLBR7EXT79eKstxf4wfTDaPKBCfTCsOWuMWHr7Genu37ew2/t7eiBGqCwwW12ylhml/OLHgNK3LOhmRABhtfpaFZSxfDVnlXfaLpY1xekVOj2oC0FpBtnoxVKLpIcyLw6dkfSil5ANd+hl59W/bpPA8KT90ii1fSNCo3+FcwQVx0YsPznJNA60XfFuVsme7zNcOst6393e1WriZxBanFpfB63zVQc9u1fjyfktx/yiUNxIlre+sz9OCc0AACn94iRhBYh4bbzdleUOTnM7lnD4Dj2FP+xeDIP8CA8wXUeq5+9kopSp2kAmlUEyFUdg4no7naIeu1SZnopfUg56PsVCp9JHiUK1SYAyWbdC+FbUECu5CvI=" + } +} diff --git a/test/fixtures/tesla_mock/osada-user-indio.json b/test/fixtures/tesla_mock/osada-user-indio.json new file mode 100644 index 000000000..c1d52c92a --- /dev/null +++ b/test/fixtures/tesla_mock/osada-user-indio.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"Person","id":"https://apfed.club/channel/indio","preferredUsername":"indio","name":"Indio","updated":"2019-08-20T23:52:34Z","icon":{"type":"Image","mediaType":"image/jpeg","updated":"2019-08-20T23:53:37Z","url":"https://apfed.club/photo/profile/l/2","height":300,"width":300},"url":"https://apfed.club/channel/indio","inbox":"https://apfed.club/inbox/indio","outbox":"https://apfed.club/outbox/indio","followers":"https://apfed.club/followers/indio","following":"https://apfed.club/following/indio","endpoints":{"sharedInbox":"https://apfed.club/inbox"},"publicKey":{"id":"https://apfed.club/channel/indio","owner":"https://apfed.club/channel/indio","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA77TIR1VuSYFnmDRFGHHb\n4vaGdx9ranzRX4bfOKAqa++Ch5L4EqJpPy08RuM+NrYCYiYl4QQFDSSDXAEgb5g9\nC1TgWTfI7q/E0UBX2Vr0mU6X4i1ztv0tuQvegRjcSJ7l1AvoBs8Ip4MEJ3OPEQhB\ngJqAACB3Gnps4zi2I0yavkxUfGVKr6zKT3BxWh5hTpKC7Do+ChIrVZC2EwxND9K6\nsAnQHThcb5EQuvuzUQZKeS7IEOsd0JpZDmJjbfMGrAWE81pLIfEeeA2joCJiBBTO\nglDsW+juvZ+lWqJpMr2hMWpvfrFjJeUawNJCIzsLdVIZR+aKj5yy6yqoS8hkN9Ha\n1MljZpsXl+EmwcwAIqim1YeLwERCEAQ/JWbSt8pQTQbzZ6ibwQ4mchCxacrRbIVR\nnL59fWMBassJcbY0VwrTugm2SBsYbDjESd55UZV03Rwr8qseGTyi+hH8O7w2SIaY\nzjN6AdZiPmsh00YflzlCk8MSLOHMol1vqIUzXxU8CdXn9+KsuQdZGrTz0YKN/db4\naVwUGJatz2Tsvf7R1tJBjJfeQWOWbbn3pycLVH86LjZ83qngp9ZVnAveUnUqz0yS\nhe+buZ6UMsfGzbIYon2bKNlz6gYTH0YPcr+cLe+29drtt0GZiXha1agbpo4RB8zE\naNL2fucF5YT0yNpbd/5WoV0CAwEAAQ==\n-----END PUBLIC KEY-----\n"},"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"c672a408d2e88b322b36a61bf0c25f586be9245d30293c55b8d653dcc867aaf7","creator":"https://apfed.club/channel/indio/public_key_pem","created":"2019-08-26T07:24:03Z","signatureValue":"MyAv5gnedu6L/DYFaE1TUYvp4LjI9ZUU0axwGYOhgD7qsjivMgwbOrjX/iH32xlcfF8nWOMh/ogu3+Qwr5sqLHkS2AimWmw1+Ubf2KccE58b8vI8zWfyu8QJnMuE92jtBPv8UTQUHw8ZebbExk3L99oXaeyVihKiMBmd63NpVTpGXZTg6m+H+KfWchVajPoyNKZtKMd3nH99x5j54Cqkz0BN5CSTwCSG0wP95G0VtZHtmhX+tsAPM3oAj0d+gtCZSCd8Nu8fvFAwCyTg1oKSfRqKb27EKHlskqK9X57x0jURH77CTAIQSejgGcKJ5GGLtvofubJkafadjagqrtqz6Mz6BZ642ssJ2KGkRAn79Q4F08goI6cfU5lLk2Tooe5A55XERnmE3SkYGyTvLpacZplxJdU0sa+deX9D7+alSGFJZSziaxpCxzrO6lEApe4b9kHXAzn9VaZt9trijkHq/kkq0i3NRcP7n8JG9q+Vv8jY9ddY6HcH89RNCBIA6MKLtAqc+vSc5G24qeZlw2MzlQWBp0KGuVG8DQR00AL6cXLBzF1WY8JZeEg6zqm+DMznbuNzgiS34BP+AehBSHlQ4MZebwDnK3ZPPqGSwioIWMxIFfZDaVDX9Pp1pXAARQMw0c/y4sDcf9FMzsr8jteEa7ZQcoqq5kXQTSCP56TEHnI="}} \ No newline at end of file diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 55b141dd8..05eebbe9b 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -775,6 +775,11 @@ defmodule HttpRequestMock do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.json")}} end + def get("https://apfed.club/channel/indio", _, _, _) do + {:ok, + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/osada-user-indio.json")}} + end + def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/activity+json") do {:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)} end diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 857d65564..fe89f7cb0 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -19,6 +19,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do end describe "handle_incoming" do + test "it works for osada follow request" do + user = insert(:user) + + data = + File.read!("test/fixtures/osada-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "https://apfed.club/channel/indio" + assert data["type"] == "Follow" + assert data["id"] == "https://apfed.club/follow/9" + + activity = Repo.get(Activity, activity.id) + assert activity.data["state"] == "accept" + assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) + end + test "it works for incoming follow requests" do user = insert(:user) -- cgit v1.2.3 From 00abe099cd85b03b880908eef1e469e656d56365 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 27 Aug 2019 16:21:03 +0300 Subject: added tests for ActivityPub.like\unlike --- lib/pleroma/activity/queries.ex | 49 +++++++ lib/pleroma/object.ex | 2 - lib/pleroma/web/activity_pub/activity_pub.ex | 9 +- .../web/activity_pub/activity_pub_controller.ex | 59 ++++----- lib/pleroma/web/activity_pub/utils.ex | 144 ++++++++++----------- test/support/factory.ex | 16 ++- test/web/activity_pub/activity_pub_test.exs | 44 +++++++ test/web/activity_pub/utils_test.exs | 102 +++++++++++++++ 8 files changed, 301 insertions(+), 124 deletions(-) create mode 100644 lib/pleroma/activity/queries.ex diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex new file mode 100644 index 000000000..aa5b29566 --- /dev/null +++ b/lib/pleroma/activity/queries.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Activity.Queries do + @moduledoc """ + Contains queries for Activity. + """ + + import Ecto.Query, only: [from: 2] + + @type query :: Ecto.Queryable.t() | Activity.t() + + alias Pleroma.Activity + + @spec by_actor(query, String.t()) :: query + def by_actor(query \\ Activity, actor) do + from( + activity in query, + where: fragment("(?)->>'actor' = ?", activity.data, ^actor) + ) + end + + @spec by_object_id(query, String.t()) :: query + def by_object_id(query \\ Activity, object_id) do + from(activity in query, + where: + fragment( + "coalesce((?)->'object'->>'id', (?)->>'object') = ?", + activity.data, + activity.data, + ^object_id + ) + ) + end + + @spec by_type(query, String.t()) :: query + def by_type(query \\ Activity, activity_type) do + from( + activity in query, + where: fragment("(?)->>'type' = ?", activity.data, ^activity_type) + ) + end + + @spec limit(query, pos_integer()) :: query + def limit(query \\ Activity, limit) do + from(activity in query, limit: ^limit) + end +end diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index c8d339c19..d58eb7f7d 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -150,8 +150,6 @@ defmodule Pleroma.Object do def update_and_set_cache(changeset) do with {:ok, object} <- Repo.update(changeset) do set_cache(object) - else - e -> e end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 172c952d4..eeb826814 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -139,7 +139,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do # Splice in the child object if we have one. activity = - if !is_nil(object) do + if not is_nil(object) do Map.put(activity, :object, object) else activity @@ -331,12 +331,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end - def unlike( - %User{} = actor, - %Object{} = object, - activity_id \\ nil, - local \\ true - ) do + def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object), unlike_data <- make_unlike_data(actor, like_activity, activity_id), {:ok, unlike_activity} <- insert(unlike_data, local), diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index ed801a7ae..5c73fc9f3 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -309,42 +309,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end def update_outbox( - %{assigns: %{user: user}} = conn, + %{assigns: %{user: %User{nickname: user_nickname} = user}} = conn, %{"nickname" => nickname} = params - ) do - if nickname == user.nickname do - actor = user.ap_id() - - params = - params - |> Map.drop(["id"]) - |> Map.put("actor", actor) - |> Transmogrifier.fix_addressing() + ) + when user_nickname == nickname do + actor = user.ap_id() - with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do - conn - |> put_status(:created) - |> put_resp_header("location", activity.data["id"]) - |> json(activity.data) - else - {:error, message} -> - conn - |> put_status(:bad_request) - |> json(message) - end - else - err = - dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}", - nickname: nickname, - as_nickname: user.nickname - ) + params = + params + |> Map.drop(["id"]) + |> Map.put("actor", actor) + |> Transmogrifier.fix_addressing() + with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do conn - |> put_status(:forbidden) - |> json(err) + |> put_status(:created) + |> put_resp_header("location", activity.data["id"]) + |> json(activity.data) + else + {:error, message} -> + conn + |> put_status(:bad_request) + |> json(message) end end + def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do + err = + dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}", + nickname: nickname, + as_nickname: user.nickname + ) + + conn + |> put_status(:forbidden) + |> json(err) + end + def errors(conn, {:error, :not_found}) do conn |> put_status(:not_found) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 1c3058658..c9c0c3763 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -166,6 +166,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do @doc """ Enqueues an activity for federation if it's local """ + @spec maybe_federate(any()) :: :ok def maybe_federate(%Activity{local: true} = activity) do if Pleroma.Config.get!([:instance, :federating]) do priority = @@ -256,46 +257,27 @@ defmodule Pleroma.Web.ActivityPub.Utils do @doc """ Returns an existing like if a user already liked an object """ + @spec get_existing_like(String.t(), map()) :: Activity.t() | nil def get_existing_like(actor, %{data: %{"id" => id}}) do - query = - from( - activity in Activity, - where: fragment("(?)->>'actor' = ?", activity.data, ^actor), - # this is to use the index - where: - fragment( - "coalesce((?)->'object'->>'id', (?)->>'object') = ?", - activity.data, - activity.data, - ^id - ), - where: fragment("(?)->>'type' = 'Like'", activity.data) - ) - - Repo.one(query) + actor + |> Activity.Queries.by_actor() + |> Activity.Queries.by_object_id(id) + |> Activity.Queries.by_type("Like") + |> Activity.Queries.limit(1) + |> Repo.one() end @doc """ Returns like activities targeting an object """ def get_object_likes(%{data: %{"id" => id}}) do - query = - from( - activity in Activity, - # this is to use the index - where: - fragment( - "coalesce((?)->'object'->>'id', (?)->>'object') = ?", - activity.data, - activity.data, - ^id - ), - where: fragment("(?)->>'type' = 'Like'", activity.data) - ) - - Repo.all(query) + id + |> Activity.Queries.by_object_id() + |> Activity.Queries.by_type("Like") + |> Repo.all() end + @spec make_like_data(User.t(), map(), String.t()) :: map() def make_like_data( %User{ap_id: ap_id} = actor, %{data: %{"actor" => object_actor_id, "id" => id}} = object, @@ -315,7 +297,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do |> List.delete(actor.ap_id) |> List.delete(object_actor.follower_address) - data = %{ + %{ "type" => "Like", "actor" => ap_id, "object" => id, @@ -323,38 +305,49 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => cc, "context" => object.data["context"] } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end + @spec update_element_in_object(String.t(), list(any), Object.t()) :: + {:ok, Object.t()} | {:error, Ecto.Changeset.t()} def update_element_in_object(property, element, object) do - with new_data <- - object.data - |> Map.put("#{property}_count", length(element)) - |> Map.put("#{property}s", element), - changeset <- Changeset.change(object, data: new_data), - {:ok, object} <- Object.update_and_set_cache(changeset) do - {:ok, object} - end - end + data = + Map.merge( + object.data, + %{"#{property}_count" => length(element), "#{property}s" => element} + ) - def update_likes_in_object(likes, object) do - update_element_in_object("like", likes, object) + object + |> Changeset.change(data: data) + |> Object.update_and_set_cache() end + @spec add_like_to_object(Activity.t(), Object.t()) :: + {:ok, Object.t()} | {:error, Ecto.Changeset.t()} def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do - likes = if is_list(object.data["likes"]), do: object.data["likes"], else: [] - - with likes <- [actor | likes] |> Enum.uniq() do - update_likes_in_object(likes, object) - end + [actor | fetch_likes(object)] + |> Enum.uniq() + |> update_likes_in_object(object) end + @spec remove_like_from_object(Activity.t(), Object.t()) :: + {:ok, Object.t()} | {:error, Ecto.Changeset.t()} def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do - likes = if is_list(object.data["likes"]), do: object.data["likes"], else: [] + object + |> fetch_likes() + |> List.delete(actor) + |> update_likes_in_object(object) + end - with likes <- likes |> List.delete(actor) do - update_likes_in_object(likes, object) + defp update_likes_in_object(likes, object) do + update_element_in_object("like", likes, object) + end + + defp fetch_likes(object) do + if is_list(object.data["likes"]) do + object.data["likes"] + else + [] end end @@ -405,7 +398,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do %User{ap_id: followed_id} = _followed, activity_id ) do - data = %{ + %{ "type" => "Follow", "actor" => follower_id, "to" => [followed_id], @@ -413,10 +406,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "object" => followed_id, "state" => "pending" } - - data = if activity_id, do: Map.put(data, "id", activity_id), else: data - - data + |> maybe_put("id", activity_id) end def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do @@ -478,7 +468,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do activity_id, false ) do - data = %{ + %{ "type" => "Announce", "actor" => ap_id, "object" => id, @@ -486,8 +476,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => [], "context" => object.data["context"] } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end def make_announce_data( @@ -496,7 +485,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do activity_id, true ) do - data = %{ + %{ "type" => "Announce", "actor" => ap_id, "object" => id, @@ -504,8 +493,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => [Pleroma.Constants.as_public()], "context" => object.data["context"] } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end @doc """ @@ -516,7 +504,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do %Activity{data: %{"context" => context}} = activity, activity_id ) do - data = %{ + %{ "type" => "Undo", "actor" => ap_id, "object" => activity.data, @@ -524,8 +512,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => [Pleroma.Constants.as_public()], "context" => context } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end def make_unlike_data( @@ -533,7 +520,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do %Activity{data: %{"context" => context}} = activity, activity_id ) do - data = %{ + %{ "type" => "Undo", "actor" => ap_id, "object" => activity.data, @@ -541,8 +528,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => [Pleroma.Constants.as_public()], "context" => context } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end def add_announce_to_object( @@ -573,14 +559,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do #### Unfollow-related helpers def make_unfollow_data(follower, followed, follow_activity, activity_id) do - data = %{ + %{ "type" => "Undo", "actor" => follower.ap_id, "to" => [followed.ap_id], "object" => follow_activity.data } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end #### Block-related helpers @@ -610,25 +595,23 @@ defmodule Pleroma.Web.ActivityPub.Utils do end def make_block_data(blocker, blocked, activity_id) do - data = %{ + %{ "type" => "Block", "actor" => blocker.ap_id, "to" => [blocked.ap_id], "object" => blocked.ap_id } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end def make_unblock_data(blocker, blocked, block_activity, activity_id) do - data = %{ + %{ "type" => "Undo", "actor" => blocker.ap_id, "to" => [blocked.ap_id], "object" => block_activity.data } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end #### Create-related helpers @@ -799,4 +782,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do Repo.all(query) end + + defp maybe_put(map, _key, nil), do: map + defp maybe_put(map, key, value), do: Map.put(map, key, value) end diff --git a/test/support/factory.ex b/test/support/factory.ex index 62d1de717..719115003 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -207,13 +207,15 @@ defmodule Pleroma.Factory do object = Object.normalize(note_activity) user = insert(:user) - data = %{ - "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), - "actor" => user.ap_id, - "type" => "Like", - "object" => object.data["id"], - "published_at" => DateTime.utc_now() |> DateTime.to_iso8601() - } + data = + %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "actor" => user.ap_id, + "type" => "Like", + "object" => object.data["id"], + "published_at" => DateTime.utc_now() |> DateTime.to_iso8601() + } + |> Map.merge(attrs[:data_attrs] || %{}) %Pleroma.Activity{ data: data diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 1515f4eb6..f72b44aed 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -21,6 +21,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do :ok end + clear_config([:instance, :federating]) + describe "streaming out participations" do test "it streams them out" do user = insert(:user) @@ -676,6 +678,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do end describe "like an object" do + test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do + Pleroma.Config.put([:instance, :federating], true) + note_activity = insert(:note_activity) + assert object_activity = Object.normalize(note_activity) + + user = insert(:user) + + {:ok, like_activity, _object} = ActivityPub.like(user, object_activity) + assert called(Pleroma.Web.Federator.publish(like_activity, 5)) + end + + test "returns exist activity if object already liked" do + note_activity = insert(:note_activity) + assert object_activity = Object.normalize(note_activity) + + user = insert(:user) + + {:ok, like_activity, _object} = ActivityPub.like(user, object_activity) + + {:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity) + assert like_activity == like_activity_exist + end + test "adds a like activity to the db" do note_activity = insert(:note_activity) assert object = Object.normalize(note_activity) @@ -706,6 +731,25 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do end describe "unliking" do + test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do + Pleroma.Config.put([:instance, :federating], true) + + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + user = insert(:user) + + {:ok, object} = ActivityPub.unlike(user, object) + refute called(Pleroma.Web.Federator.publish()) + + {:ok, _like_activity, object} = ActivityPub.like(user, object) + assert object.data["like_count"] == 1 + + {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object) + assert object.data["like_count"] == 0 + + assert called(Pleroma.Web.Federator.publish(unlike_activity, 5)) + end + test "unliking a previously liked object" do note_activity = insert(:note_activity) object = Object.normalize(note_activity) diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs index ca5f057a7..eb429b2c4 100644 --- a/test/web/activity_pub/utils_test.exs +++ b/test/web/activity_pub/utils_test.exs @@ -14,6 +14,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do import Pleroma.Factory + require Pleroma.Constants + describe "fetch the latest Follow" do test "fetches the latest Follow activity" do %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) @@ -87,6 +89,32 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do end end + describe "make_unlike_data/3" do + test "returns data for unlike activity" do + user = insert(:user) + like_activity = insert(:like_activity, data_attrs: %{"context" => "test context"}) + + assert Utils.make_unlike_data(user, like_activity, nil) == %{ + "type" => "Undo", + "actor" => user.ap_id, + "object" => like_activity.data, + "to" => [user.follower_address, like_activity.data["actor"]], + "cc" => [Pleroma.Constants.as_public()], + "context" => like_activity.data["context"] + } + + assert Utils.make_unlike_data(user, like_activity, "9mJEZK0tky1w2xD2vY") == %{ + "type" => "Undo", + "actor" => user.ap_id, + "object" => like_activity.data, + "to" => [user.follower_address, like_activity.data["actor"]], + "cc" => [Pleroma.Constants.as_public()], + "context" => like_activity.data["context"], + "id" => "9mJEZK0tky1w2xD2vY" + } + end + end + describe "make_like_data" do setup do user = insert(:user) @@ -299,4 +327,78 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" end end + + describe "update_element_in_object/3" do + test "updates likes" do + user = insert(:user) + activity = insert(:note_activity) + object = Object.normalize(activity) + + assert {:ok, updated_object} = + Utils.update_element_in_object( + "like", + [user.ap_id], + object + ) + + assert updated_object.data["likes"] == [user.ap_id] + assert updated_object.data["like_count"] == 1 + end + end + + describe "add_like_to_object/2" do + test "add actor to likes" do + user = insert(:user) + user2 = insert(:user) + object = insert(:note) + + assert {:ok, updated_object} = + Utils.add_like_to_object( + %Activity{data: %{"actor" => user.ap_id}}, + object + ) + + assert updated_object.data["likes"] == [user.ap_id] + assert updated_object.data["like_count"] == 1 + + assert {:ok, updated_object2} = + Utils.add_like_to_object( + %Activity{data: %{"actor" => user2.ap_id}}, + updated_object + ) + + assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id] + assert updated_object2.data["like_count"] == 2 + end + end + + describe "remove_like_from_object/2" do + test "removes ap_id from likes" do + user = insert(:user) + user2 = insert(:user) + object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2}) + + assert {:ok, updated_object} = + Utils.remove_like_from_object( + %Activity{data: %{"actor" => user.ap_id}}, + object + ) + + assert updated_object.data["likes"] == [user2.ap_id] + assert updated_object.data["like_count"] == 1 + end + end + + describe "get_existing_like/2" do + test "fetches existing like" do + note_activity = insert(:note_activity) + assert object = Object.normalize(note_activity) + + user = insert(:user) + refute Utils.get_existing_like(user.ap_id, object) + {:ok, like_activity, _object} = ActivityPub.like(user, object) + + assert ^like_activity = Utils.get_existing_like(user.ap_id, object) + end + end end -- cgit v1.2.3 From c30cc039e423e8f31d0222747e301514b7d0dd9e Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 27 Aug 2019 12:22:30 -0500 Subject: Transmogrifier: Use Containment.get_actor to get actors. --- lib/pleroma/object.ex | 4 ---- lib/pleroma/web/activity_pub/transmogrifier.ex | 6 ++++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 468549c87..c8d339c19 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -230,8 +230,4 @@ defmodule Pleroma.Object do _ -> :noop end end - - def get_ap_id(%{"id" => id}), do: id - def get_ap_id(id) when is_binary(id), do: id - def get_ap_id(_), do: {:error, "Object is not a string and has no id."} end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 6c4259c02..468961bd0 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -464,8 +464,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data, _options ) do - with %User{local: true} = followed <- User.get_cached_by_ap_id(Object.get_ap_id(followed)), - {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(Object.get_ap_id(follower)), + with %User{local: true} = followed <- + User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})), + {:ok, %User{} = follower} <- + User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})), {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, -- cgit v1.2.3 From ffcd742aa0797b5bb872e58c1e605f22c8652250 Mon Sep 17 00:00:00 2001 From: Maksim Date: Tue, 27 Aug 2019 17:37:19 +0000 Subject: Apply suggestion to lib/pleroma/web/activity_pub/activity_pub_controller.ex --- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 5c73fc9f3..08bf1c752 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -309,10 +309,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end def update_outbox( - %{assigns: %{user: %User{nickname: user_nickname} = user}} = conn, + %{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{"nickname" => nickname} = params - ) - when user_nickname == nickname do + ) do actor = user.ap_id() params = -- cgit v1.2.3 From 7853b3f17d3b57d7ac91bc909a57143674f57272 Mon Sep 17 00:00:00 2001 From: feld Date: Fri, 30 Aug 2019 00:38:03 +0000 Subject: Fix AntiFollowbotPolicy when trying to follow a relay --- CHANGELOG.md | 1 + lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20af9badc..4acb749ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances - MRF: fix use of unserializable keyword lists in describe() implementations - ActivityPub: Deactivated user deletion +- MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled ### Added - Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically. diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex index de1eb4aa5..b3547ecd4 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -25,11 +25,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do defp score_displayname(_), do: 0.0 defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do - # nickname will always be a binary string because it's generated by Pleroma. + # nickname will be a binary string except when following a relay nick_score = - nickname - |> String.downcase() - |> score_nickname() + if is_binary(nickname) do + nickname + |> String.downcase() + |> score_nickname() + else + 0.0 + end # displayname will either be a binary string or nil, if a displayname isn't set. name_score = -- cgit v1.2.3 From 99b4847da3244a0d023ae25b2669afb07a4eda4f Mon Sep 17 00:00:00 2001 From: kPherox Date: Fri, 30 Aug 2019 21:00:50 +0900 Subject: Fix missing changes in pleroma/pleroma!1197 --- installation/pleroma.nginx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index e3c70de54..4da9918ca 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -71,26 +71,26 @@ server { proxy_set_header Connection "upgrade"; proxy_set_header Host $http_host; - # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only - # and `localhost.` resolves to [::0] on some systems: see issue #930 + # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only + # and `localhost.` resolves to [::0] on some systems: see issue #930 proxy_pass http://127.0.0.1:4000; client_max_body_size 16m; } location ~ ^/(media|proxy) { - proxy_cache pleroma_media_cache; + proxy_cache pleroma_media_cache; slice 1m; proxy_cache_key $host$uri$is_args$args$slice_range; proxy_set_header Range $slice_range; proxy_http_version 1.1; proxy_cache_valid 200 206 301 304 1h; - proxy_cache_lock on; + proxy_cache_lock on; proxy_ignore_client_abort on; - proxy_buffering on; + proxy_buffering on; chunked_transfer_encoding on; proxy_ignore_headers Cache-Control; - proxy_hide_header Cache-Control; - proxy_pass http://localhost:4000; + proxy_hide_header Cache-Control; + proxy_pass http://127.0.0.1:4000; } } -- cgit v1.2.3