Merge branch 'develop' into feature/moderation-log-filters
authorMaxim Filippov <colixer@gmail.com>
Thu, 26 Sep 2019 16:01:54 +0000 (19:01 +0300)
committerMaxim Filippov <colixer@gmail.com>
Thu, 26 Sep 2019 16:01:54 +0000 (19:01 +0300)
1  2 
CHANGELOG.md
lib/pleroma/user/info.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
test/web/admin_api/admin_api_controller_test.exs

diff --combined CHANGELOG.md
index 1766b9e4cf26fd1b05be7437a78d8820f6634af4,1a76e6cf85692716a47987e2212d138fe3160d22..d9ddb5b03500e1a60aca9a69db0031cb9f3f8b18
@@@ -4,14 -4,26 +4,26 @@@ All notable changes to this project wil
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
  
  ## [Unreleased]
+ ### Added
+ - Refreshing poll results for remote polls
+ - Admin API: Add ability to require password reset
+ ### Changed
+ - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
+ - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
+ - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
+ - Admin API: Return `total` when querying for reports
+ - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
+ - Admin API: Return link alongside with token on password reset
+ ### Fixed
+ - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
+ ## [1.1.0] - 2019-??-??
  ### Security
- - OStatus: eliminate the possibility of a protocol downgrade attack.
- - OStatus: prevent following locked accounts, bypassing the approval process.
  - Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by`
  
  ### Removed
  - **Breaking:** GNU Social API with Qvitter extensions support
- - **Breaking:** ActivityPub: The `accept_blocks` configuration setting.
  - Emoji: Remove longfox emojis.
  - Remove `Reply-To` header from report emails for admins.
  
  - **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
  - **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired
  - **Breaking:** `/api/pleroma/notifications/read` is moved to `/api/v1/pleroma/notifications/read` and now supports `max_id` and responds with Mastodon API entities.
+ - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
+ - Configuration: added `config/description.exs`, from which `docs/config.md` is generated
  - Configuration: OpenGraph and TwitterCard providers enabled by default
  - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
- - Configuration: added `config/description.exs`, from which `docs/config.md` is generated
+ - Mastodon API: `pleroma.thread_muted` key in the Status entity
  - Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set
  - NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
  - NodeInfo: Return `mailerEnabled` in `metadata`
  - AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
  - Improve digest email template
  – Pagination: (optional) return `total` alongside with `items` when paginating
- - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
- - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
+ - Add `rel="ugc"` to all links in statuses, to prevent SEO spam
+ - ActivityPub: The first page in inboxes/outboxes is no longer embedded.
  
  ### 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
  - Metadata rendering errors resulting in the entire page being inaccessible
  - `federation_incoming_replies_max_depth` option being ignored in certain cases
- - Federation/MediaProxy not working with instances that have wrong certificate order
  - Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`)
  - Mastodon API: Misskey's endless polls being unable to render
  - Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity
  - Mastodon API: Notifications endpoint crashing if one notification failed to render
- - Mastodon API: follower/following counters not being nullified, when `hide_follows`/`hide_followers` is set
- - Mastodon API: `muted` in the Status entity, using author's account to determine if the tread was muted
  - Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`)
  - Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes
  - ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
  - Rich Media: Parser failing when no TTL can be found by image TTL setters
  - Rich Media: The crawled URL is now spliced into the rich media data.
  - ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification.
- - ActivityPub S2S: remote user deletions now work the same as local user deletions.
- - ActivityPub S2S: POST requests are now signed with `(request-target)` pseudo-header.
- - Not being able to access the Mastodon FE login page on private instances
- - Invalid SemVer version generation, when the current branch does not have commits ahead of tag/checked out on a tag
  - Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
  - Report email not being sent to admins when the reporter is a remote user
- - MRF: ensure that subdomain_match calls are case-insensitive
  - 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
  - ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user
  - MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled
  - 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.
-   Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules.
  - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
  - MRF: Support for excluding specific domains from Transparency.
  - MRF: Support for filtering posts based on who they mention (`Pleroma.Web.ActivityPub.MRF.MentionPolicy`)
- - MRF: Support for filtering posts based on ActivityStreams vocabulary (`Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`)
- - MRF (Simple Policy): Support for wildcard domains.
- - Support for wildcard domains in user domain blocks setting.
- - Configuration: `quarantined_instances` support wildcard domains.
- - Configuration: `federation_incoming_replies_max_depth` option
  - Mastodon API: Support for the [`tagged` filter](https://github.com/tootsuite/mastodon/pull/9755) in [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/api/rest/accounts/#get-api-v1-accounts-id-statuses)
  - Mastodon API, streaming: Add support for passing the token in the `Sec-WebSocket-Protocol` header
  - Mastodon API, extension: Ability to reset avatar, profile banner, and background
@@@ -94,6 -90,7 +90,7 @@@
  - Mastodon API: added `/auth/password` endpoint for password reset with rate limit.
  - Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id
  - Mastodon API: Improve support for the user profile custom fields
+ - Mastodon API: follower/following counters are nullified when `hide_follows`/`hide_followers` and `hide_follows_count`/`hide_followers_count` are set
  - Admin API: Return users' tags when querying reports
  - Admin API: Return avatar and display name when querying users
  - Admin API: Allow querying user by ID
  - Admin API: Endpoint for fetching latest user's statuses
  - Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=<email>` for resending account confirmation.
  - Pleroma API: Email change endpoint.
- - 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
  - 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
  - RichMedia: parsers and their order are configured in `rich_media` config.
  - RichMedia: add the rich media ttl based on image expiration time.
  
+ ## [1.0.6] - 2019-08-14
+ ### Fixed
+ - MRF: fix use of unserializable keyword lists in describe() implementations
+ - ActivityPub S2S: POST requests are now signed with `(request-target)` pseudo-header.
+ ## [1.0.5] - 2019-08-13
+ ### Fixed
+ - Mastodon API: follower/following counters not being nullified, when `hide_follows`/`hide_followers` is set
+ - Mastodon API: `muted` in the Status entity, using author's account to determine if the thread was muted
+ - Mastodon API: return the actual profile URL in the Account entity's `url` property when appropriate
+ - Templates: properly style anchor tags
+ - Objects being re-embedded to activities after being updated (e.g faved/reposted). Running 'mix pleroma.database prune_objects' again is advised.
+ - Not being able to access the Mastodon FE login page on private instances
+ - MRF: ensure that subdomain_match calls are case-insensitive
+ - Fix internal server error when using the healthcheck API.
+ ### Added
+ - **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo.
+   Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules.
+ - Relays: Added a task to list relay subscriptions.
+ - MRF: Support for filtering posts based on ActivityStreams vocabulary (`Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`)
+ - MRF (Simple Policy): Support for wildcard domains.
+ - Support for wildcard domains in user domain blocks setting.
+ - Configuration: `quarantined_instances` support wildcard domains.
+ - Mix Tasks: `mix pleroma.database fix_likes_collections`
+ - Configuration: `federation_incoming_replies_max_depth` option
+ ### Removed
+ - Federation: Remove `likes` from objects.
+ - **Breaking:** ActivityPub: The `accept_blocks` configuration setting.
+ ## [1.0.4] - 2019-08-01
+ ### Fixed
+ - Invalid SemVer version generation, when the current branch does not have commits ahead of tag/checked out on a tag
+ ## [1.0.3] - 2019-07-31
+ ### Security
+ - OStatus: eliminate the possibility of a protocol downgrade attack.
+ - OStatus: prevent following locked accounts, bypassing the approval process.
+ - TwitterAPI: use CommonAPI to handle remote follows instead of OStatus.
+ ## [1.0.2] - 2019-07-28
+ ### Fixed
+ - Not being able to pin unlisted posts
+ - Mastodon API: represent poll IDs as strings
+ - MediaProxy: fix matching filenames
+ - MediaProxy: fix filename encoding
+ - Migrations: fix a sporadic migration failure
+ - Metadata rendering errors resulting in the entire page being inaccessible
+ - Federation/MediaProxy not working with instances that have wrong certificate order
+ - ActivityPub S2S: remote user deletions now work the same as local user deletions.
+ ### Changed
+ - Configuration: OpenGraph and TwitterCard providers enabled by default
+ - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
  
  ## [1.0.1] - 2019-07-14
  ### Security
diff --combined lib/pleroma/user/info.ex
index 1b5951a0e2becb1f4763e029a20c06ffb3e1dc41,eef985d0d5fcc6d259fc070c23686936ea5f8106..ebd4ddebf2c68989ebb39b1b1aa3d5ad3fc982f3
@@@ -20,6 -20,7 +20,7 @@@ defmodule Pleroma.User.Info d
      field(:following_count, :integer, default: nil)
      field(:locked, :boolean, default: false)
      field(:confirmation_pending, :boolean, default: false)
+     field(:password_reset_pending, :boolean, default: false)
      field(:confirmation_token, :string, default: nil)
      field(:default_scope, :string, default: "public")
      field(:blocks, {:array, :string}, default: [])
@@@ -41,6 -42,8 +42,8 @@@
      field(:topic, :string, default: nil)
      field(:hub, :string, default: nil)
      field(:salmon, :string, default: nil)
+     field(:hide_followers_count, :boolean, default: false)
+     field(:hide_follows_count, :boolean, default: false)
      field(:hide_followers, :boolean, default: false)
      field(:hide_follows, :boolean, default: false)
      field(:hide_favorites, :boolean, default: true)
@@@ -51,6 -54,7 +54,7 @@@
      field(:pleroma_settings_store, :map, default: %{})
      field(:fields, {:array, :map}, default: nil)
      field(:raw_fields, {:array, :map}, default: [])
+     field(:discoverable, :boolean, default: false)
  
      field(:notification_settings, :map,
        default: %{
      |> validate_required([:deactivated])
    end
  
+   def set_password_reset_pending(info, pending) do
+     params = %{password_reset_pending: pending}
+     info
+     |> cast(params, [:password_reset_pending])
+     |> validate_required([:password_reset_pending])
+   end
    def update_notification_settings(info, settings) do
      settings =
        settings
      |> validate_required([:subscribers])
    end
  
-   @spec add_to_mutes(Info.t(), String.t()) :: Changeset.t()
-   def add_to_mutes(info, muted) do
-     set_mutes(info, Enum.uniq([muted | info.mutes]))
-   end
-   @spec add_to_muted_notifications(Changeset.t(), Info.t(), String.t(), boolean()) ::
-           Changeset.t()
-   def add_to_muted_notifications(changeset, info, muted, notifications?) do
-     set_notification_mutes(
-       changeset,
+   @spec add_to_mutes(Info.t(), String.t(), boolean()) :: Changeset.t()
+   def add_to_mutes(info, muted, notifications?) do
+     info
+     |> set_mutes(Enum.uniq([muted | info.mutes]))
+     |> set_notification_mutes(
        Enum.uniq([muted | info.muted_notifications]),
        notifications?
      )
  
    @spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t()
    def remove_from_mutes(info, muted) do
-     set_mutes(info, List.delete(info.mutes, muted))
-   end
-   @spec remove_from_muted_notifications(Changeset.t(), Info.t(), String.t()) :: Changeset.t()
-   def remove_from_muted_notifications(changeset, info, muted) do
-     set_notification_mutes(changeset, List.delete(info.muted_notifications, muted), true)
+     info
+     |> set_mutes(List.delete(info.mutes, muted))
+     |> set_notification_mutes(List.delete(info.muted_notifications, muted), true)
    end
  
    def add_to_block(info, blocked) do
        :salmon,
        :hide_followers,
        :hide_follows,
+       :hide_followers_count,
+       :hide_follows_count,
        :follower_count,
        :fields,
-       :following_count
+       :following_count,
+       :discoverable
      ])
      |> validate_fields(true)
    end
        :following_count,
        :hide_follows,
        :fields,
-       :hide_followers
+       :hide_followers,
+       :discoverable,
+       :hide_followers_count,
+       :hide_follows_count
      ])
      |> validate_fields(remote?)
    end
        :banner,
        :hide_follows,
        :hide_followers,
+       :hide_followers_count,
+       :hide_follows_count,
        :hide_favorites,
        :background,
        :show_role,
        :skip_thread_containment,
        :fields,
        :raw_fields,
-       :pleroma_settings_store
+       :pleroma_settings_store,
+       :discoverable
      ])
      |> validate_fields()
    end
      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
  
        :hide_followers,
        :hide_follows,
        :follower_count,
-       :following_count
+       :following_count,
+       :hide_followers_count,
+       :hide_follows_count
      ])
    end
  end
index 135c6ae87f7bdee264d31ce3aca329d5ff281187,e9a048b9b703517d6e297caf55637effd8ea3c69..90aef99f7857d921e92055f980adb5810fc08bef
@@@ -14,10 -14,13 +14,13 @@@ defmodule Pleroma.Web.AdminAPI.AdminAPI
    alias Pleroma.Web.AdminAPI.Config
    alias Pleroma.Web.AdminAPI.ConfigView
    alias Pleroma.Web.AdminAPI.ModerationLogView
+   alias Pleroma.Web.AdminAPI.Report
    alias Pleroma.Web.AdminAPI.ReportView
    alias Pleroma.Web.AdminAPI.Search
    alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.Endpoint
    alias Pleroma.Web.MastodonAPI.StatusView
+   alias Pleroma.Web.Router
  
    import Pleroma.Web.ControllerHelper, only: [json_response: 3]
  
    def user_show(conn, %{"nickname" => nickname}) do
      with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
        conn
-       |> json(AccountView.render("show.json", %{user: user}))
+       |> put_view(AccountView)
+       |> render("show.json", %{user: user})
      else
        _ -> {:error, :not_found}
      end
          })
  
        conn
-       |> json(StatusView.render("index.json", %{activities: activities, as: :activity}))
+       |> put_view(StatusView)
+       |> render("index.json", %{activities: activities, as: :activity})
      else
        _ -> {:error, :not_found}
      end
      })
  
      conn
-     |> json(AccountView.render("show.json", %{user: updated_user}))
+     |> put_view(AccountView)
+     |> render("show.json", %{user: updated_user})
    end
  
    def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
          "nickname" => nickname
        })
        when permission_group in ["moderator", "admin"] do
-     user = User.get_cached_by_nickname(nickname)
-     info =
-       %{}
-       |> Map.put("is_" <> permission_group, true)
+     info = Map.put(%{}, "is_" <> permission_group, true)
  
-     info_cng = User.Info.admin_api_update(user.info, info)
-     cng =
-       user
-       |> Ecto.Changeset.change()
-       |> Ecto.Changeset.put_embed(:info, info_cng)
+     {:ok, user} =
+       nickname
+       |> User.get_cached_by_nickname()
+       |> User.update_info(&User.Info.admin_api_update(&1, info))
  
      ModerationLog.insert_log(%{
        action: "grant",
        permission: permission_group
      })
  
-     {:ok, _user} = User.update_and_set_cache(cng)
      json(conn, info)
    end
  
      })
    end
  
+   def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
+     render_error(conn, :forbidden, "You can't revoke your own admin status.")
+   end
    def right_delete(
-         %{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn,
+         %{assigns: %{user: admin}} = conn,
          %{
            "permission_group" => permission_group,
            "nickname" => nickname
          }
        )
        when permission_group in ["moderator", "admin"] do
-     if admin_nickname == nickname do
-       render_error(conn, :forbidden, "You can't revoke your own admin status.")
-     else
-       user = User.get_cached_by_nickname(nickname)
-       info =
-         %{}
-         |> Map.put("is_" <> permission_group, false)
-       info_cng = User.Info.admin_api_update(user.info, info)
+     info = Map.put(%{}, "is_" <> permission_group, false)
  
-       cng =
-         Ecto.Changeset.change(user)
-         |> Ecto.Changeset.put_embed(:info, info_cng)
+     {:ok, user} =
+       nickname
+       |> User.get_cached_by_nickname()
+       |> User.update_info(&User.Info.admin_api_update(&1, info))
  
-       {:ok, _user} = User.update_and_set_cache(cng)
-       ModerationLog.insert_log(%{
-         action: "revoke",
-         actor: admin,
-         subject: user,
-         permission: permission_group
-       })
+     ModerationLog.insert_log(%{
+       action: "revoke",
+       actor: admin,
+       subject: user,
+       permission: permission_group
+     })
  
-       json(conn, info)
-     end
+     json(conn, info)
    end
  
    def right_delete(conn, _) do
      end
    end
  
-   @doc "Get a account registeration invite token (base64 string)"
-   def get_invite_token(conn, params) do
-     options = params["invite"] || %{}
-     {:ok, invite} = UserInviteToken.create_invite(options)
+   @doc "Create an account registration invite token"
+   def create_invite_token(conn, params) do
+     opts = %{}
  
-     conn
-     |> json(invite.token)
+     opts =
+       if params["max_use"],
+         do: Map.put(opts, :max_use, params["max_use"]),
+         else: opts
+     opts =
+       if params["expires_at"],
+         do: Map.put(opts, :expires_at, params["expires_at"]),
+         else: opts
+     {:ok, invite} = UserInviteToken.create_invite(opts)
+     json(conn, AccountView.render("invite.json", %{invite: invite}))
    end
  
    @doc "Get list of created invites"
      invites = UserInviteToken.list_invites()
  
      conn
-     |> json(AccountView.render("invites.json", %{invites: invites}))
+     |> put_view(AccountView)
+     |> render("invites.json", %{invites: invites})
    end
  
    @doc "Revokes invite by token"
      with {:ok, invite} <- UserInviteToken.find_by_token(token),
           {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
        conn
-       |> json(AccountView.render("invite.json", %{invite: updated_invite}))
+       |> put_view(AccountView)
+       |> render("invite.json", %{invite: updated_invite})
      else
        nil -> {:error, :not_found}
      end
      {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
  
      conn
-     |> json(token.token)
+     |> json(%{
+       token: token.token,
+       link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
+     })
+   end
+   @doc "Force password reset for a given user"
+   def force_password_reset(conn, %{"nickname" => nickname}) do
+     (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
+     User.force_password_reset_async(user)
+     json_response(conn, :no_content, "")
    end
  
    def list_reports(conn, params) do
+     {page, page_size} = page_params(params)
      params =
        params
        |> Map.put("type", "Flag")
        |> Map.put("skip_preload", true)
+       |> Map.put("total", true)
+       |> Map.put("limit", page_size)
+       |> Map.put("offset", (page - 1) * page_size)
  
-     reports =
-       []
-       |> ActivityPub.fetch_activities(params)
-       |> Enum.reverse()
+     reports = ActivityPub.fetch_activities([], params, :offset)
  
      conn
      |> put_view(ReportView)
      with %Activity{} = report <- Activity.get_by_id(id) do
        conn
        |> put_view(ReportView)
-       |> render("show.json", %{report: report})
+       |> render("show.json", Report.extract_report_info(report))
      else
        _ -> {:error, :not_found}
      end
  
        conn
        |> put_view(ReportView)
-       |> render("show.json", %{report: report})
+       |> render("show.json", Report.extract_report_info(report))
      end
    end
  
    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)
      |> render("index.json", %{configs: updated})
    end
  
+   def reload_emoji(conn, _params) do
+     Pleroma.Emoji.reload()
+     conn |> json("ok")
+   end
    def errors(conn, {:error, :not_found}) do
      conn
      |> put_status(:not_found)
index 66804faacb03e238d09642107c03f94e84ae8b32,00e64692aa298664e6fe54d939956683433cd33b..b5c355e66f3eb9c36f6ad41bf676306f8f518c26
@@@ -1,14 -1,16 +1,16 @@@
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
    use Pleroma.Web.ConnCase
+   use Oban.Testing, repo: Pleroma.Repo
  
    alias Pleroma.Activity
    alias Pleroma.HTML
    alias Pleroma.ModerationLog
    alias Pleroma.Repo
+   alias Pleroma.Tests.ObanHelpers
    alias Pleroma.User
    alias Pleroma.UserInviteToken
    alias Pleroma.Web.CommonAPI
      end
    end
  
-   test "/api/pleroma/admin/users/invite_token" do
-     admin = insert(:user, info: %{is_admin: true})
-     conn =
-       build_conn()
-       |> assign(:user, admin)
-       |> put_req_header("accept", "application/json")
-       |> get("/api/pleroma/admin/users/invite_token")
-     assert conn.status == 200
-   end
    test "/api/pleroma/admin/users/:nickname/password_reset" do
      admin = insert(:user, info: %{is_admin: true})
      user = insert(:user)
        |> put_req_header("accept", "application/json")
        |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
  
-     assert conn.status == 200
+     resp = json_response(conn, 200)
+     assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"])
    end
  
    describe "GET /api/pleroma/admin/users" do
               "@#{admin.nickname} deactivated user @#{user.nickname}"
    end
  
-   describe "GET /api/pleroma/admin/users/invite_token" do
+   describe "POST /api/pleroma/admin/users/invite_token" do
      setup do
        admin = insert(:user, info: %{is_admin: true})
  
      end
  
      test "without options", %{conn: conn} do
-       conn = get(conn, "/api/pleroma/admin/users/invite_token")
+       conn = post(conn, "/api/pleroma/admin/users/invite_token")
  
-       token = json_response(conn, 200)
-       invite = UserInviteToken.find_by_token!(token)
+       invite_json = json_response(conn, 200)
+       invite = UserInviteToken.find_by_token!(invite_json["token"])
        refute invite.used
        refute invite.expires_at
        refute invite.max_use
  
      test "with expires_at", %{conn: conn} do
        conn =
-         get(conn, "/api/pleroma/admin/users/invite_token", %{
-           "invite" => %{"expires_at" => Date.to_string(Date.utc_today())}
+         post(conn, "/api/pleroma/admin/users/invite_token", %{
+           "expires_at" => Date.to_string(Date.utc_today())
          })
  
-       token = json_response(conn, 200)
-       invite = UserInviteToken.find_by_token!(token)
+       invite_json = json_response(conn, 200)
+       invite = UserInviteToken.find_by_token!(invite_json["token"])
  
        refute invite.used
        assert invite.expires_at == Date.utc_today()
      end
  
      test "with max_use", %{conn: conn} do
-       conn =
-         get(conn, "/api/pleroma/admin/users/invite_token", %{
-           "invite" => %{"max_use" => 150}
-         })
+       conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150})
  
-       token = json_response(conn, 200)
-       invite = UserInviteToken.find_by_token!(token)
+       invite_json = json_response(conn, 200)
+       invite = UserInviteToken.find_by_token!(invite_json["token"])
        refute invite.used
        refute invite.expires_at
        assert invite.max_use == 150
  
      test "with max use and expires_at", %{conn: conn} do
        conn =
-         get(conn, "/api/pleroma/admin/users/invite_token", %{
-           "invite" => %{"max_use" => 150, "expires_at" => Date.to_string(Date.utc_today())}
+         post(conn, "/api/pleroma/admin/users/invite_token", %{
+           "max_use" => 150,
+           "expires_at" => Date.to_string(Date.utc_today())
          })
  
-       token = json_response(conn, 200)
-       invite = UserInviteToken.find_by_token!(token)
+       invite_json = json_response(conn, 200)
+       invite = UserInviteToken.find_by_token!(invite_json["token"])
        refute invite.used
        assert invite.expires_at == Date.utc_today()
        assert invite.max_use == 150
          |> json_response(:ok)
  
        assert Enum.empty?(response["reports"])
+       assert response["total"] == 0
      end
  
      test "returns reports", %{conn: conn} do
  
        assert length(response["reports"]) == 1
        assert report["id"] == report_id
+       assert response["total"] == 1
      end
  
      test "returns reports with specified state", %{conn: conn} do
        assert length(response["reports"]) == 1
        assert open_report["id"] == first_report_id
  
+       assert response["total"] == 1
        response =
          conn
          |> get("/api/pleroma/admin/reports", %{
        assert length(response["reports"]) == 1
        assert closed_report["id"] == second_report_id
  
+       assert response["total"] == 1
        response =
          conn
          |> get("/api/pleroma/admin/reports", %{
          |> json_response(:ok)
  
        assert Enum.empty?(response["reports"])
+       assert response["total"] == 0
      end
  
      test "returns 403 when requested by a non-admin" 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
        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"] ==
        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"] ==
        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
+     setup %{conn: conn} do
+       admin = insert(:user, info: %{is_admin: true})
+       user = insert(:user)
+       %{conn: assign(conn, :user, admin), admin: admin, user: user}
+     end
+     test "sets password_reset_pending to true", %{admin: admin, user: user} do
+       assert user.info.password_reset_pending == false
+       conn =
+         build_conn()
+         |> assign(:user, admin)
+         |> patch("/api/pleroma/admin/users/#{user.nickname}/force_password_reset")
+       assert json_response(conn, 204) == ""
+       ObanHelpers.perform_all()
+       assert User.get_by_id(user.id).info.password_reset_pending == true
+     end
+   end
  end
  
  # Needed for testing