diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | docs/api/admin_api.md | 6 | ||||
| -rw-r--r-- | lib/pleroma/moderation_log.ex | 265 | ||||
| -rw-r--r-- | lib/pleroma/user/info.ex | 4 | ||||
| -rw-r--r-- | lib/pleroma/web/admin_api/admin_api_controller.ex | 10 | ||||
| -rw-r--r-- | lib/pleroma/web/admin_api/views/moderation_log_view.ex | 5 | ||||
| -rw-r--r-- | test/moderation_log_test.exs | 36 | ||||
| -rw-r--r-- | test/web/admin_api/admin_api_controller_test.exs | 121 | 
8 files changed, 343 insertions, 105 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a76e6cf8..d9ddb5b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).  - Web response cache (currently, enabled for ActivityPub)  - Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)  - ActivityPub: Add ActivityPub actor's `discoverable` parameter. +- Admin API: Added moderation log filters (user/start date/end date/search/pagination)  ### 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 d4e08f221..573111416 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -731,7 +731,11 @@ Compile time settings (need instance reboot):  - Method `GET`  - Params:    - *optional* `page`: **integer** page number -  - *optional* `page_size`: **integer** number of users per page (default is `50`) +  - *optional* `page_size`: **integer** number of log entries per page (default is `50`) +  - *optional* `start_date`: **datetime (ISO 8601)** filter logs by creation date, start from `start_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. `2005-08-09T18:31:42` +  - *optional* `end_date`: **datetime (ISO 8601)** filter logs by creation date, end by from `end_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. 2005-08-09T18:31:42 +  - *optional* `user_id`: **integer** filter logs by actor's id +  - *optional* `search`: **string** search logs by the log message  - Response:  ```json diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 1ef6fe67a..352cad433 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -14,61 +14,143 @@ defmodule Pleroma.ModerationLog do      timestamps()    end -  def get_all(page, page_size) do -    from(q in __MODULE__, -      order_by: [desc: q.inserted_at], +  def get_all(params) do +    base_query = +      get_all_query() +      |> maybe_filter_by_date(params) +      |> maybe_filter_by_user(params) +      |> maybe_filter_by_search(params) + +    query_with_pagination = base_query |> paginate_query(params) + +    %{ +      items: Repo.all(query_with_pagination), +      count: Repo.aggregate(base_query, :count, :id) +    } +  end + +  defp maybe_filter_by_date(query, %{start_date: nil, end_date: nil}), do: query + +  defp maybe_filter_by_date(query, %{start_date: start_date, end_date: nil}) do +    from(q in query, +      where: q.inserted_at >= ^parse_datetime(start_date) +    ) +  end + +  defp maybe_filter_by_date(query, %{start_date: nil, end_date: end_date}) do +    from(q in query, +      where: q.inserted_at <= ^parse_datetime(end_date) +    ) +  end + +  defp maybe_filter_by_date(query, %{start_date: start_date, end_date: end_date}) do +    from(q in query, +      where: q.inserted_at >= ^parse_datetime(start_date), +      where: q.inserted_at <= ^parse_datetime(end_date) +    ) +  end + +  defp maybe_filter_by_user(query, %{user_id: nil}), do: query + +  defp maybe_filter_by_user(query, %{user_id: user_id}) do +    from(q in query, +      where: fragment("(?)->'actor'->>'id' = ?", q.data, ^user_id) +    ) +  end + +  defp maybe_filter_by_search(query, %{search: search}) when is_nil(search) or search == "", +    do: query + +  defp maybe_filter_by_search(query, %{search: search}) do +    from(q in query, +      where: fragment("(?)->>'message' ILIKE ?", q.data, ^"%#{search}%") +    ) +  end + +  defp paginate_query(query, %{page: page, page_size: page_size}) do +    from(q in query,        limit: ^page_size,        offset: ^((page - 1) * page_size)      ) -    |> Repo.all()    end +  defp get_all_query do +    from(q in __MODULE__, +      order_by: [desc: q.inserted_at] +    ) +  end + +  defp parse_datetime(datetime) do +    {:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime) + +    parsed_datetime +  end + +  @spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          subject: %User{} = subject,          action: action,          permission: permission        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        subject: user_to_map(subject), -        action: action, -        permission: permission +        "actor" => user_to_map(actor), +        "subject" => user_to_map(subject), +        "action" => action, +        "permission" => permission, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, subject: User, action: String.t()}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          action: "report_update",          subject: %Activity{data: %{"type" => "Flag"}} = subject        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "report_update", -        subject: report_to_map(subject) +        "actor" => user_to_map(actor), +        "action" => "report_update", +        "subject" => report_to_map(subject), +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          action: "report_response",          subject: %Activity{} = subject,          text: text        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "report_response", -        subject: report_to_map(subject), -        text: text +        "actor" => user_to_map(actor), +        "action" => "report_response", +        "subject" => report_to_map(subject), +        "text" => text, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{ +          actor: User, +          subject: Activity, +          action: String.t(), +          sensitive: String.t(), +          visibility: String.t() +        }) :: {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          action: "status_update", @@ -76,41 +158,49 @@ defmodule Pleroma.ModerationLog do          sensitive: sensitive,          visibility: visibility        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "status_update", -        subject: status_to_map(subject), -        sensitive: sensitive, -        visibility: visibility +        "actor" => user_to_map(actor), +        "action" => "status_update", +        "subject" => status_to_map(subject), +        "sensitive" => sensitive, +        "visibility" => visibility, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          action: "status_delete",          subject_id: subject_id        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "status_delete", -        subject_id: subject_id +        "actor" => user_to_map(actor), +        "action" => "status_delete", +        "subject_id" => subject_id, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    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{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: action, -        subject: user_to_map(subject) +        "actor" => user_to_map(actor), +        "action" => action, +        "subject" => user_to_map(subject), +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end    @spec insert_log(%{actor: User, subjects: [User], action: String.t()}) :: @@ -118,97 +208,128 @@ defmodule Pleroma.ModerationLog do    def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do      subjects = Enum.map(subjects, &user_to_map/1) -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: action, -        subjects: subjects +        "actor" => user_to_map(actor), +        "action" => action, +        "subjects" => subjects, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          followed: %User{} = followed,          follower: %User{} = follower,          action: "follow"        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "follow", -        followed: user_to_map(followed), -        follower: user_to_map(follower) +        "actor" => user_to_map(actor), +        "action" => "follow", +        "followed" => user_to_map(followed), +        "follower" => user_to_map(follower), +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          followed: %User{} = followed,          follower: %User{} = follower,          action: "unfollow"        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "unfollow", -        followed: user_to_map(followed), -        follower: user_to_map(follower) +        "actor" => user_to_map(actor), +        "action" => "unfollow", +        "followed" => user_to_map(followed), +        "follower" => user_to_map(follower), +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{ +          actor: User, +          action: String.t(), +          nicknames: [String.t()], +          tags: [String.t()] +        }) :: {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          nicknames: nicknames,          tags: tags,          action: action        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        nicknames: nicknames, -        tags: tags, -        action: action +        "actor" => user_to_map(actor), +        "nicknames" => nicknames, +        "tags" => tags, +        "action" => action, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, action: String.t(), target: String.t()}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          action: action,          target: target        })        when action in ["relay_follow", "relay_unfollow"] do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: action, -        target: target +        "actor" => user_to_map(actor), +        "action" => action, +        "target" => target, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message() +  end + +  @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any} + +  defp insert_log_entry_with_message(entry) do +    entry.data["message"] +    |> put_in(get_log_entry_message(entry)) +    |> Repo.insert()    end    defp user_to_map(%User{} = user) do      user      |> Map.from_struct()      |> Map.take([:id, :nickname]) -    |> Map.put(:type, "user") +    |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end) +    |> Map.put("type", "user")    end    defp report_to_map(%Activity{} = report) do      %{ -      type: "report", -      id: report.id, -      state: report.data["state"] +      "type" => "report", +      "id" => report.id, +      "state" => report.data["state"]      }    end    defp status_to_map(%Activity{} = status) do      %{ -      type: "status", -      id: status.id +      "type" => "status", +      "id" => status.id      }    end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index eef985d0d..ebd4ddebf 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -338,9 +338,7 @@ defmodule Pleroma.User.Info do      name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)      value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) -    is_binary(name) && -      is_binary(value) && -      String.length(name) <= name_limit && +    is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&        String.length(value) <= value_limit    end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index e9a048b9b..90aef99f7 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -556,7 +556,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    def list_log(conn, params) do      {page, page_size} = page_params(params) -    log = ModerationLog.get_all(page, page_size) +    log = +      ModerationLog.get_all(%{ +        page: page, +        page_size: page_size, +        start_date: params["start_date"], +        end_date: params["end_date"], +        user_id: params["user_id"], +        search: params["search"] +      })      conn      |> put_view(ModerationLogView) diff --git a/lib/pleroma/web/admin_api/views/moderation_log_view.ex b/lib/pleroma/web/admin_api/views/moderation_log_view.ex index b3fc7cfe5..e7752d1f3 100644 --- a/lib/pleroma/web/admin_api/views/moderation_log_view.ex +++ b/lib/pleroma/web/admin_api/views/moderation_log_view.ex @@ -8,7 +8,10 @@ defmodule Pleroma.Web.AdminAPI.ModerationLogView do    alias Pleroma.ModerationLog    def render("index.json", %{log: log}) do -    render_many(log, __MODULE__, "show.json", as: :log_entry) +    %{ +      items: render_many(log.items, __MODULE__, "show.json", as: :log_entry), +      total: log.count +    }    end    def render("show.json", %{log_entry: log_entry}) do diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs index c78708471..a39a00e02 100644 --- a/test/moderation_log_test.exs +++ b/test/moderation_log_test.exs @@ -30,8 +30,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == -               "@#{moderator.nickname} deleted user @#{subject1.nickname}" +      assert log.data["message"] == "@#{moderator.nickname} deleted user @#{subject1.nickname}"      end      test "logging user creation by moderator", %{ @@ -48,7 +47,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}"      end @@ -63,7 +62,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}"      end @@ -78,7 +77,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}"      end @@ -100,8 +99,7 @@ defmodule Pleroma.ModerationLogTest do        tags = ["foo", "bar"] |> Enum.join(", ") -      assert ModerationLog.get_log_entry_message(log) == -               "@#{admin.nickname} added tags: #{tags} to users: #{users}" +      assert log.data["message"] == "@#{admin.nickname} added tags: #{tags} to users: #{users}"      end      test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do @@ -122,7 +120,7 @@ defmodule Pleroma.ModerationLogTest do        tags = ["foo", "bar"] |> Enum.join(", ") -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{admin.nickname} removed tags: #{tags} from users: #{users}"      end @@ -137,8 +135,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == -               "@#{moderator.nickname} made @#{subject1.nickname} moderator" +      assert log.data["message"] == "@#{moderator.nickname} made @#{subject1.nickname} moderator"      end      test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do @@ -152,7 +149,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}"      end @@ -166,7 +163,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} followed relay: https://example.org/relay"      end @@ -180,7 +177,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} unfollowed relay: https://example.org/relay"      end @@ -202,7 +199,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state"      end @@ -224,7 +221,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} responded with 'look at this' to report ##{report.id}"      end @@ -242,7 +239,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'"      end @@ -260,7 +257,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'"      end @@ -278,7 +275,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'"      end @@ -294,8 +291,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == -               "@#{moderator.nickname} deleted status ##{note.id}" +      assert log.data["message"] == "@#{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 00e64692a..b5c355e66 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -2257,8 +2257,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do    describe "GET /api/pleroma/admin/moderation_log" do      setup %{conn: conn} do        admin = insert(:user, info: %{is_admin: true}) +      moderator = insert(:user, info: %{is_moderator: true}) -      %{conn: assign(conn, :user, admin), admin: admin} +      %{conn: assign(conn, :user, admin), admin: admin, moderator: moderator}      end      test "returns the log", %{conn: conn, admin: admin} do @@ -2291,9 +2292,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do        conn = get(conn, "/api/pleroma/admin/moderation_log")        response = json_response(conn, 200) -      [first_entry, second_entry] = response +      [first_entry, second_entry] = response["items"] -      assert response |> length() == 2 +      assert response["total"] == 2        assert first_entry["data"]["action"] == "relay_unfollow"        assert first_entry["message"] == @@ -2335,9 +2336,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do        conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1")        response1 = json_response(conn1, 200) -      [first_entry] = response1 +      [first_entry] = response1["items"] -      assert response1 |> length() == 1 +      assert response1["total"] == 2 +      assert response1["items"] |> length() == 1        assert first_entry["data"]["action"] == "relay_unfollow"        assert first_entry["message"] == @@ -2346,14 +2348,119 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do        conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2")        response2 = json_response(conn2, 200) -      [second_entry] = response2 +      [second_entry] = response2["items"] -      assert response2 |> length() == 1 +      assert response2["total"] == 2 +      assert response2["items"] |> length() == 1        assert second_entry["data"]["action"] == "relay_follow"        assert second_entry["message"] ==                 "@#{admin.nickname} followed relay: https://example.org/relay"      end + +    test "filters log by date", %{conn: conn, admin: admin} do +      first_date = "2017-08-15T15:47:06Z" +      second_date = "2017-08-20T15:47:06Z" + +      Repo.insert(%ModerationLog{ +        data: %{ +          actor: %{ +            "id" => admin.id, +            "nickname" => admin.nickname, +            "type" => "user" +          }, +          action: "relay_follow", +          target: "https://example.org/relay" +        }, +        inserted_at: NaiveDateTime.from_iso8601!(first_date) +      }) + +      Repo.insert(%ModerationLog{ +        data: %{ +          actor: %{ +            "id" => admin.id, +            "nickname" => admin.nickname, +            "type" => "user" +          }, +          action: "relay_unfollow", +          target: "https://example.org/relay" +        }, +        inserted_at: NaiveDateTime.from_iso8601!(second_date) +      }) + +      conn1 = +        get( +          conn, +          "/api/pleroma/admin/moderation_log?start_date=#{second_date}" +        ) + +      response1 = json_response(conn1, 200) +      [first_entry] = response1["items"] + +      assert response1["total"] == 1 +      assert first_entry["data"]["action"] == "relay_unfollow" + +      assert first_entry["message"] == +               "@#{admin.nickname} unfollowed relay: https://example.org/relay" +    end + +    test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do +      Repo.insert(%ModerationLog{ +        data: %{ +          actor: %{ +            "id" => admin.id, +            "nickname" => admin.nickname, +            "type" => "user" +          }, +          action: "relay_follow", +          target: "https://example.org/relay" +        } +      }) + +      Repo.insert(%ModerationLog{ +        data: %{ +          actor: %{ +            "id" => moderator.id, +            "nickname" => moderator.nickname, +            "type" => "user" +          }, +          action: "relay_unfollow", +          target: "https://example.org/relay" +        } +      }) + +      conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}") + +      response1 = json_response(conn1, 200) +      [first_entry] = response1["items"] + +      assert response1["total"] == 1 +      assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id +    end + +    test "returns log filtered by search", %{conn: conn, moderator: moderator} do +      ModerationLog.insert_log(%{ +        actor: moderator, +        action: "relay_follow", +        target: "https://example.org/relay" +      }) + +      ModerationLog.insert_log(%{ +        actor: moderator, +        action: "relay_unfollow", +        target: "https://example.org/relay" +      }) + +      conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo") + +      response1 = json_response(conn1, 200) +      [first_entry] = response1["items"] + +      assert response1["total"] == 1 + +      assert get_in(first_entry, ["data", "message"]) == +               "@#{moderator.nickname} unfollowed relay: https://example.org/relay" +    end    end    describe "PATCH /users/:nickname/force_password_reset" do  | 
