MastoAPI: Profile directory
This commit is contained in:
parent
73609211a4
commit
de006443f0
|
@ -254,7 +254,8 @@
|
|||
]
|
||||
],
|
||||
show_reactions: true,
|
||||
password_reset_token_validity: 60 * 60 * 24
|
||||
password_reset_token_validity: 60 * 60 * 24,
|
||||
profile_directory: true
|
||||
|
||||
config :pleroma, :welcome,
|
||||
direct_message: [
|
||||
|
|
|
@ -936,6 +936,11 @@
|
|||
key: :show_reactions,
|
||||
type: :boolean,
|
||||
description: "Let favourites and emoji reactions be viewed through the API."
|
||||
},
|
||||
%{
|
||||
key: :profile_directory,
|
||||
type: :boolean,
|
||||
description: "Enable profile directory."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -383,12 +383,6 @@ Pleroma is generally compatible with the Mastodon 2.7.2 API, but some newer feat
|
|||
|
||||
- `GET /api/v1/endorsements`: Returns an empty array, `[]`
|
||||
|
||||
### Profile directory
|
||||
|
||||
*Added in Mastodon 3.0.0*
|
||||
|
||||
- `GET /api/v1/directory`: Returns HTTP 404
|
||||
|
||||
### Featured tags
|
||||
|
||||
*Added in Mastodon 3.0.0*
|
||||
|
|
|
@ -149,6 +149,7 @@ defmodule Pleroma.User do
|
|||
field(:disclose_client, :boolean, default: true)
|
||||
field(:pinned_objects, :map, default: %{})
|
||||
field(:is_suggested, :boolean, default: false)
|
||||
field(:last_status_at, :naive_datetime)
|
||||
|
||||
embeds_one(
|
||||
:notification_settings,
|
||||
|
@ -2499,4 +2500,16 @@ def active_user_count(days \\ 30) do
|
|||
|> where([u], u.local == true)
|
||||
|> Repo.aggregate(:count)
|
||||
end
|
||||
|
||||
def update_last_status_at(user) do
|
||||
User
|
||||
|> where(id: ^user.id)
|
||||
|> update([u], set: [last_status_at: fragment("NOW()")])
|
||||
|> select([u], u)
|
||||
|> Repo.update_all([])
|
||||
|> case do
|
||||
{1, [user]} -> set_cache(user)
|
||||
_ -> {:error, user}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -47,6 +47,7 @@ defmodule Pleroma.User.Query do
|
|||
is_admin: boolean(),
|
||||
is_moderator: boolean(),
|
||||
is_suggested: boolean(),
|
||||
is_discoverable: boolean(),
|
||||
super_users: boolean(),
|
||||
invisible: boolean(),
|
||||
internal: boolean(),
|
||||
|
@ -172,6 +173,10 @@ defp compose_query({:is_suggested, bool}, query) do
|
|||
where(query, [u], u.is_suggested == ^bool)
|
||||
end
|
||||
|
||||
defp compose_query({:is_discoverable, bool}, query) do
|
||||
where(query, [u], u.is_discoverable == ^bool)
|
||||
end
|
||||
|
||||
defp compose_query({:followers, %User{id: id}}, query) do
|
||||
query
|
||||
|> where([u], u.id != ^id)
|
||||
|
|
|
@ -81,6 +81,10 @@ def decrease_note_count_if_public(actor, object) do
|
|||
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
|
||||
end
|
||||
|
||||
def update_last_status_at_if_public(actor, object) do
|
||||
if is_public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
|
||||
end
|
||||
|
||||
defp increase_replies_count_if_reply(%{
|
||||
"object" => %{"inReplyTo" => reply_ap_id} = object,
|
||||
"type" => "Create"
|
||||
|
@ -288,6 +292,7 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param
|
|||
_ <- increase_replies_count_if_reply(create_data),
|
||||
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
|
||||
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
||||
{:ok, _actor} <- update_last_status_at_if_public(actor, activity),
|
||||
_ <- notify_and_stream(activity),
|
||||
:ok <- maybe_schedule_poll_notifications(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
|
|
|
@ -199,6 +199,7 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
|
|||
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
|
||||
{:ok, notifications} = Notification.create_notifications(activity, do_send: false)
|
||||
{:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
|
||||
{:ok, _user} = ActivityPub.update_last_status_at_if_public(user, object)
|
||||
|
||||
if in_reply_to = object.data["type"] != "Answer" && object.data["inReplyTo"] do
|
||||
Object.increase_replies_count(in_reply_to)
|
||||
|
|
41
lib/pleroma/web/api_spec/operations/directory_operation.ex
Normal file
41
lib/pleroma/web/api_spec/operations/directory_operation.ex
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.DirectoryOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias Pleroma.Web.ApiSpec.AccountOperation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Directory"],
|
||||
summary: "Profile directory",
|
||||
operationId: "DirectoryController.index",
|
||||
parameters:
|
||||
[
|
||||
Operation.parameter(
|
||||
:order,
|
||||
:query,
|
||||
:string,
|
||||
"Order by recent activity or account creation",
|
||||
required: nil
|
||||
),
|
||||
Operation.parameter(:local, :query, BooleanLike, "Include local users only")
|
||||
] ++ pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Accounts", "application/json", AccountOperation.array_of_accounts()),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,82 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.DirectoryController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
import Ecto.Query
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
|
||||
require Logger
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(:skip_auth when action == "index")
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DirectoryOperation
|
||||
|
||||
@doc "GET /api/v1/directory"
|
||||
def index(%{assigns: %{user: user}} = conn, params) do
|
||||
with true <- Pleroma.Config.get([:instance, :profile_directory]) do
|
||||
limit = Map.get(params, :limit, 20) |> min(80)
|
||||
|
||||
users =
|
||||
User.Query.build(%{is_discoverable: true, invisible: false, limit: limit})
|
||||
|> order_by_creation_date(params)
|
||||
|> exclude_remote(params)
|
||||
|> exclude_user(user)
|
||||
|> exclude_relationships(user, [:block, :mute])
|
||||
|> Pagination.fetch_paginated(params, :offset)
|
||||
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("index.json", for: user, users: users, as: :user)
|
||||
else
|
||||
_ -> json(conn, [])
|
||||
end
|
||||
end
|
||||
|
||||
defp order_by_creation_date(query, %{order: "new"}) do
|
||||
query
|
||||
end
|
||||
|
||||
defp order_by_creation_date(query, _params) do
|
||||
query
|
||||
|> order_by([u], desc_nulls_last: u.last_status_at)
|
||||
end
|
||||
|
||||
defp exclude_remote(query, %{local: true}) do
|
||||
where(query, [u], u.local == true)
|
||||
end
|
||||
|
||||
defp exclude_remote(query, _params) do
|
||||
query
|
||||
end
|
||||
|
||||
defp exclude_user(query, %User{id: user_id}) do
|
||||
where(query, [u], u.id != ^user_id)
|
||||
end
|
||||
|
||||
defp exclude_user(query, _user) do
|
||||
query
|
||||
end
|
||||
|
||||
defp exclude_relationships(query, %User{id: user_id}, relationship_types) do
|
||||
query
|
||||
|> join(:left, [u], r in UserRelationship,
|
||||
as: :user_relationships,
|
||||
on:
|
||||
r.target_id == u.id and r.source_id == ^user_id and
|
||||
r.relationship_type in ^relationship_types
|
||||
)
|
||||
|> where([user_relationships: r], is_nil(r.target_id))
|
||||
end
|
||||
|
||||
defp exclude_relationships(query, _user, _relationship_types) do
|
||||
query
|
||||
end
|
||||
end
|
|
@ -270,6 +270,7 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
actor_type: user.actor_type
|
||||
}
|
||||
},
|
||||
last_status_at: user.last_status_at,
|
||||
|
||||
# Pleroma extensions
|
||||
# Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub
|
||||
|
|
|
@ -87,6 +87,9 @@ def features do
|
|||
"pleroma_chat_messages",
|
||||
if Config.get([:instance, :show_reactions]) do
|
||||
"exposable_reactions"
|
||||
end,
|
||||
if Config.get([:instance, :profile_directory]) do
|
||||
"profile_directory"
|
||||
end
|
||||
]
|
||||
|> Enum.filter(& &1)
|
||||
|
|
|
@ -600,6 +600,8 @@ defmodule Pleroma.Web.Router do
|
|||
get("/timelines/tag/:tag", TimelineController, :hashtag)
|
||||
|
||||
get("/polls/:id", PollController, :show)
|
||||
|
||||
get("/directory", DirectoryController, :index)
|
||||
end
|
||||
|
||||
scope "/api/v2", Pleroma.Web.MastodonAPI do
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddLastStatusAtToUsers do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
add(:last_status_at, :naive_datetime)
|
||||
end
|
||||
|
||||
create_if_not_exists(index(:users, [:last_status_at]))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddIsDiscoverableIndexToUsers do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create(index(:users, [:is_discoverable]))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,46 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.DirectoryControllerTest do
|
||||
use Pleroma.Web.ConnCase, async: true
|
||||
alias Pleroma.Web.CommonAPI
|
||||
import Pleroma.Factory
|
||||
|
||||
test "GET /api/v1/directory with :profile_directory disabled returns empty array", %{conn: conn} do
|
||||
clear_config([:instance, :profile_directory], false)
|
||||
|
||||
insert(:user, is_discoverable: true)
|
||||
insert(:user, is_discoverable: true)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/directory")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert result == []
|
||||
end
|
||||
|
||||
test "GET /api/v1/directory returns discoverable users only", %{conn: conn} do
|
||||
%{id: user_id} = insert(:user, is_discoverable: true)
|
||||
insert(:user, is_discoverable: false)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/directory")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [%{"id" => ^user_id}] = result
|
||||
end
|
||||
|
||||
test "GET /api/v1/directory returns users sorted by most recent statuses", %{conn: conn} do
|
||||
insert(:user, is_discoverable: true)
|
||||
%{id: user_id} = user = insert(:user, is_discoverable: true)
|
||||
insert(:user, is_discoverable: true)
|
||||
|
||||
{:ok, _activity} = CommonAPI.post(user, %{status: "yay i'm discoverable"})
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/directory?order=active")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [%{"id" => ^user_id} | _tail] = result
|
||||
end
|
||||
end
|
|
@ -74,6 +74,7 @@ test "Represent a user account" do
|
|||
fields: []
|
||||
},
|
||||
fqn: "shp@shitposter.club",
|
||||
last_status_at: nil,
|
||||
pleroma: %{
|
||||
ap_id: user.ap_id,
|
||||
also_known_as: ["https://shitposter.zone/users/shp"],
|
||||
|
@ -175,6 +176,7 @@ test "Represent a Service(bot) account" do
|
|||
fields: []
|
||||
},
|
||||
fqn: "shp@shitposter.club",
|
||||
last_status_at: nil,
|
||||
pleroma: %{
|
||||
ap_id: user.ap_id,
|
||||
also_known_as: [],
|
||||
|
|
Loading…
Reference in a new issue