Merge branch 'profile-directory' into 'develop'
MastoAPI: Profile directory See merge request pleroma/pleroma!3573
This commit is contained in:
commit
913141379c
16 changed files with 226 additions and 7 deletions
|
@ -254,7 +254,8 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
show_reactions: true,
|
show_reactions: true,
|
||||||
password_reset_token_validity: 60 * 60 * 24
|
password_reset_token_validity: 60 * 60 * 24,
|
||||||
|
profile_directory: true
|
||||||
|
|
||||||
config :pleroma, :welcome,
|
config :pleroma, :welcome,
|
||||||
direct_message: [
|
direct_message: [
|
||||||
|
|
|
@ -936,6 +936,11 @@
|
||||||
key: :show_reactions,
|
key: :show_reactions,
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Let favourites and emoji reactions be viewed through the API."
|
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, `[]`
|
- `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
|
### Featured tags
|
||||||
|
|
||||||
*Added in Mastodon 3.0.0*
|
*Added in Mastodon 3.0.0*
|
||||||
|
|
|
@ -149,6 +149,7 @@ defmodule Pleroma.User do
|
||||||
field(:disclose_client, :boolean, default: true)
|
field(:disclose_client, :boolean, default: true)
|
||||||
field(:pinned_objects, :map, default: %{})
|
field(:pinned_objects, :map, default: %{})
|
||||||
field(:is_suggested, :boolean, default: false)
|
field(:is_suggested, :boolean, default: false)
|
||||||
|
field(:last_status_at, :naive_datetime)
|
||||||
|
|
||||||
embeds_one(
|
embeds_one(
|
||||||
:notification_settings,
|
:notification_settings,
|
||||||
|
@ -2499,4 +2500,16 @@ def active_user_count(days \\ 30) do
|
||||||
|> where([u], u.local == true)
|
|> where([u], u.local == true)
|
||||||
|> Repo.aggregate(:count)
|
|> Repo.aggregate(:count)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -47,6 +47,7 @@ defmodule Pleroma.User.Query do
|
||||||
is_admin: boolean(),
|
is_admin: boolean(),
|
||||||
is_moderator: boolean(),
|
is_moderator: boolean(),
|
||||||
is_suggested: boolean(),
|
is_suggested: boolean(),
|
||||||
|
is_discoverable: boolean(),
|
||||||
super_users: boolean(),
|
super_users: boolean(),
|
||||||
invisible: boolean(),
|
invisible: boolean(),
|
||||||
internal: boolean(),
|
internal: boolean(),
|
||||||
|
@ -172,6 +173,10 @@ defp compose_query({:is_suggested, bool}, query) do
|
||||||
where(query, [u], u.is_suggested == ^bool)
|
where(query, [u], u.is_suggested == ^bool)
|
||||||
end
|
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
|
defp compose_query({:followers, %User{id: id}}, query) do
|
||||||
query
|
query
|
||||||
|> where([u], u.id != ^id)
|
|> 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}
|
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
|
||||||
end
|
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(%{
|
defp increase_replies_count_if_reply(%{
|
||||||
"object" => %{"inReplyTo" => reply_ap_id} = object,
|
"object" => %{"inReplyTo" => reply_ap_id} = object,
|
||||||
"type" => "Create"
|
"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),
|
_ <- increase_replies_count_if_reply(create_data),
|
||||||
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
|
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
|
||||||
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
||||||
|
{:ok, _actor} <- update_last_status_at_if_public(actor, activity),
|
||||||
_ <- notify_and_stream(activity),
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_schedule_poll_notifications(activity),
|
:ok <- maybe_schedule_poll_notifications(activity),
|
||||||
:ok <- maybe_federate(activity) do
|
: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
|
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
|
||||||
{:ok, notifications} = Notification.create_notifications(activity, do_send: false)
|
{:ok, notifications} = Notification.create_notifications(activity, do_send: false)
|
||||||
{:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
|
{: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
|
if in_reply_to = object.data["type"] != "Answer" && object.data["inReplyTo"] do
|
||||||
Object.increase_replies_count(in_reply_to)
|
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
|
actor_type: user.actor_type
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
last_status_at: user.last_status_at,
|
||||||
|
|
||||||
# Pleroma extensions
|
# Pleroma extensions
|
||||||
# Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub
|
# 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",
|
"pleroma_chat_messages",
|
||||||
if Config.get([:instance, :show_reactions]) do
|
if Config.get([:instance, :show_reactions]) do
|
||||||
"exposable_reactions"
|
"exposable_reactions"
|
||||||
|
end,
|
||||||
|
if Config.get([:instance, :profile_directory]) do
|
||||||
|
"profile_directory"
|
||||||
end
|
end
|
||||||
]
|
]
|
||||||
|> Enum.filter(& &1)
|
|> Enum.filter(& &1)
|
||||||
|
|
|
@ -600,6 +600,8 @@ defmodule Pleroma.Web.Router do
|
||||||
get("/timelines/tag/:tag", TimelineController, :hashtag)
|
get("/timelines/tag/:tag", TimelineController, :hashtag)
|
||||||
|
|
||||||
get("/polls/:id", PollController, :show)
|
get("/polls/:id", PollController, :show)
|
||||||
|
|
||||||
|
get("/directory", DirectoryController, :index)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/api/v2", Pleroma.Web.MastodonAPI do
|
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: []
|
fields: []
|
||||||
},
|
},
|
||||||
fqn: "shp@shitposter.club",
|
fqn: "shp@shitposter.club",
|
||||||
|
last_status_at: nil,
|
||||||
pleroma: %{
|
pleroma: %{
|
||||||
ap_id: user.ap_id,
|
ap_id: user.ap_id,
|
||||||
also_known_as: ["https://shitposter.zone/users/shp"],
|
also_known_as: ["https://shitposter.zone/users/shp"],
|
||||||
|
@ -175,6 +176,7 @@ test "Represent a Service(bot) account" do
|
||||||
fields: []
|
fields: []
|
||||||
},
|
},
|
||||||
fqn: "shp@shitposter.club",
|
fqn: "shp@shitposter.club",
|
||||||
|
last_status_at: nil,
|
||||||
pleroma: %{
|
pleroma: %{
|
||||||
ap_id: user.ap_id,
|
ap_id: user.ap_id,
|
||||||
also_known_as: [],
|
also_known_as: [],
|
||||||
|
|
Loading…
Reference in a new issue