Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into finch

This commit is contained in:
Lain Soykaf 2021-12-26 16:41:17 +01:00
commit 6efbd08854
67 changed files with 1723 additions and 87 deletions

View file

@ -9,7 +9,8 @@ defenum(Pleroma.UserRelationship.Type,
mute: 2,
reblog_mute: 3,
notification_mute: 4,
inverse_subscription: 5
inverse_subscription: 5,
suggestion_dismiss: 6
)
defenum(Pleroma.FollowingRelationship.State,

View file

@ -103,6 +103,7 @@ defmodule Pleroma.Emoji.Loader do
pack_file = Path.join(pack_dir, "pack.json")
if File.exists?(pack_file) do
Logger.info("Loading emoji pack from JSON: #{pack_file}")
contents = Jason.decode!(File.read!(pack_file))
contents["files"]
@ -115,6 +116,7 @@ defmodule Pleroma.Emoji.Loader do
emoji_txt = Path.join(pack_dir, "emoji.txt")
if File.exists?(emoji_txt) do
Logger.info("Loading emoji pack from emoji.txt: #{emoji_txt}")
load_from_file(emoji_txt, emoji_groups)
else
extensions = Config.get([:emoji, :pack_extensions])

View file

@ -338,6 +338,26 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "add_suggestion",
"subject" => users
}
}) do
"@#{actor_nickname} added suggested users: #{users_to_nicknames_string(users)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "remove_suggestion",
"subject" => users
}
}) do
"@#{actor_nickname} removed suggested users: #{users_to_nicknames_string(users)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},

View file

@ -148,6 +148,8 @@ defmodule Pleroma.User do
field(:last_active_at, :naive_datetime)
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,
@ -1676,6 +1678,22 @@ defmodule Pleroma.User do
def confirm(%User{} = user), do: {:ok, user}
def set_suggestion(users, is_suggested) when is_list(users) do
Repo.transaction(fn ->
Enum.map(users, fn user ->
with {:ok, user} <- set_suggestion(user, is_suggested), do: user
end)
end)
end
def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
user
|> change(is_suggested: is_suggested)
|> update_and_set_cache()
end
def update_notification_settings(%User{} = user, settings) do
user
|> cast(%{notification_settings: settings}, [])
@ -2482,4 +2500,16 @@ defmodule Pleroma.User 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

View file

@ -46,6 +46,8 @@ defmodule Pleroma.User.Query do
unconfirmed: boolean(),
is_admin: boolean(),
is_moderator: boolean(),
is_suggested: boolean(),
is_discoverable: boolean(),
super_users: boolean(),
invisible: boolean(),
internal: boolean(),
@ -167,6 +169,14 @@ defmodule Pleroma.User.Query do
where(query, [u], u.is_confirmed == false)
end
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)

52
lib/pleroma/user_note.ex Normal file
View file

@ -0,0 +1,52 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserNote do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserNote
schema "user_notes" do
belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
field(:comment, :string)
timestamps()
end
def changeset(%UserNote{} = user_note, params \\ %{}) do
user_note
|> cast(params, [:source_id, :target_id, :comment])
|> validate_required([:source_id, :target_id])
end
def show(%User{} = source, %User{} = target) do
with %UserNote{} = note <-
UserNote
|> where(source_id: ^source.id, target_id: ^target.id)
|> Repo.one() do
note.comment
else
_ -> ""
end
end
def create(%User{} = source, %User{} = target, comment) do
%UserNote{}
|> changeset(%{
source_id: source.id,
target_id: target.id,
comment: comment
})
|> Repo.insert(
on_conflict: {:replace, [:comment]},
conflict_target: [:source_id, :target_id]
)
end
end

View file

@ -81,6 +81,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
_ <- 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

View file

@ -199,8 +199,9 @@ defmodule Pleroma.Web.ActivityPub.SideEffects 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["inReplyTo"] && object.data["type"] != "Answer" do
if in_reply_to = object.data["type"] != "Answer" && object.data["inReplyTo"] do
Object.increase_replies_count(in_reply_to)
end

View file

@ -446,7 +446,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Activity.Queries.by_type()
|> Activity.Queries.by_actor(actor)
|> Activity.Queries.by_object_id(object)
|> where(fragment("data->>'state' = 'pending'"))
|> where(fragment("data->>'state' = 'pending'") or fragment("data->>'state' = 'accept'"))
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])

View file

@ -35,7 +35,9 @@ defmodule Pleroma.Web.AdminAPI.UserController do
:toggle_activation,
:activate,
:deactivate,
:approve
:approve,
:suggest,
:unsuggest
]
)
@ -239,6 +241,32 @@ defmodule Pleroma.Web.AdminAPI.UserController do
render(conn, "index.json", users: updated_users)
end
def suggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_suggestion(users, true)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "add_suggestion"
})
render(conn, "index.json", users: updated_users)
end
def unsuggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_suggestion(users, false)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "remove_suggestion"
})
render(conn, "index.json", users: updated_users)
end
def index(conn, params) do
{page, page_size} = page_params(params)
filters = maybe_parse_filters(params[:filters])

View file

@ -80,6 +80,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
"tags" => user.tags || [],
"is_confirmed" => user.is_confirmed,
"is_approved" => user.is_approved,
"is_suggested" => user.is_suggested,
"url" => user.uri || user.ap_id,
"registration_reason" => user.registration_reason,
"actor_type" => user.actor_type,

View file

@ -226,6 +226,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
type: :boolean,
description: "Receive this account's reblogs in home timeline? Defaults to true.",
default: true
},
notify: %Schema{
type: :boolean,
description:
"Receive notifications for all statuses posted by the account? Defaults to false.",
default: false
}
}
},
@ -328,6 +334,29 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
def note_operation do
%Operation{
tags: ["Account actions"],
summary: "Set a private note about a user.",
operationId: "AccountController.note",
security: [%{"oAuth" => ["follow", "write:accounts"]}],
requestBody: request_body("Parameters", note_request()),
description: "Create a note for the given account.",
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
Operation.parameter(
:comment,
:query,
%Schema{type: :string},
"Account note body"
)
],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end
def follow_by_uri_operation do
%Operation{
tags: ["Account actions"],
@ -685,9 +714,11 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
"blocked_by" => true,
"muting" => false,
"muting_notifications" => false,
"note" => "",
"requested" => false,
"domain_blocking" => false,
"subscribing" => false,
"notifying" => false,
"endorsed" => true
},
%{
@ -699,9 +730,11 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
"blocked_by" => true,
"muting" => true,
"muting_notifications" => false,
"note" => "",
"requested" => true,
"domain_blocking" => false,
"subscribing" => false,
"notifying" => false,
"endorsed" => false
},
%{
@ -713,9 +746,11 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
"blocked_by" => false,
"muting" => true,
"muting_notifications" => false,
"note" => "",
"requested" => false,
"domain_blocking" => true,
"subscribing" => true,
"notifying" => true,
"endorsed" => false
}
]
@ -760,6 +795,23 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
defp note_request do
%Schema{
title: "AccountNoteRequest",
description: "POST body for adding a note for an account",
type: :object,
properties: %{
comment: %Schema{
type: :string,
description: "Account note body"
}
},
example: %{
"comment" => "Example note"
}
}
end
defp array_of_lists do
%Schema{
title: "ArrayOfLists",

View file

@ -216,7 +216,71 @@ defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do
request_body(
"Parameters",
%Schema{
description: "POST body for deleting multiple users",
description: "POST body for approving multiple users",
type: :object,
properties: %{
nicknames: %Schema{
type: :array,
items: %Schema{type: :string}
}
}
}
),
responses: %{
200 =>
Operation.response("Response", "application/json", %Schema{
type: :object,
properties: %{user: %Schema{type: :array, items: user()}}
}),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def suggest_operation do
%Operation{
tags: ["User administration"],
summary: "Suggest multiple users",
operationId: "AdminAPI.UserController.suggest",
security: [%{"oAuth" => ["admin:write:accounts"]}],
parameters: admin_api_params(),
requestBody:
request_body(
"Parameters",
%Schema{
description: "POST body for adding multiple suggested users",
type: :object,
properties: %{
nicknames: %Schema{
type: :array,
items: %Schema{type: :string}
}
}
}
),
responses: %{
200 =>
Operation.response("Response", "application/json", %Schema{
type: :object,
properties: %{user: %Schema{type: :array, items: user()}}
}),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def unsuggest_operation do
%Operation{
tags: ["User administration"],
summary: "Unsuggest multiple users",
operationId: "AdminAPI.UserController.unsuggest",
security: [%{"oAuth" => ["admin:write:accounts"]}],
parameters: admin_api_params(),
requestBody:
request_body(
"Parameters",
%Schema{
description: "POST body for removing multiple suggested users",
type: :object,
properties: %{
nicknames: %Schema{

View 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

View file

@ -191,6 +191,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
parameters: [
Operation.parameter(:password, :query, :string, "Password")
],
requestBody: request_body("Parameters", delete_account_request(), required: false),
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
@ -237,4 +238,48 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
}
end
def remote_interaction_operation do
%Operation{
tags: ["Accounts"],
summary: "Remote interaction",
operationId: "UtilController.remote_interaction",
requestBody: request_body("Parameters", remote_interaction_request(), required: true),
responses: %{
200 =>
Operation.response("Remote interaction URL", "application/json", %Schema{type: :object})
}
}
end
defp remote_interaction_request do
%Schema{
title: "RemoteInteractionRequest",
description: "POST body for remote interaction",
type: :object,
required: [:ap_id, :profile],
properties: %{
ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"},
profile: %Schema{type: :string, description: "Remote profile webfinger"}
}
}
end
defp delete_account_request do
%Schema{
title: "AccountDeleteRequest",
description: "POST body for deleting one's own account",
type: :object,
properties: %{
password: %Schema{
type: :string,
description: "The user's own password for confirmation.",
format: :password
}
},
example: %{
"password" => "prettyp0ony1313"
}
}
end
end

View file

@ -194,9 +194,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"id" => "9tKi3esbG7OQgZ2920",
"muting" => false,
"muting_notifications" => false,
"note" => "",
"requested" => false,
"showing_reblogs" => true,
"subscribing" => false
"subscribing" => false,
"notifying" => false
},
"settings_store" => %{
"pleroma-fe" => %{}

View file

@ -22,9 +22,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
id: FlakeID,
muting: %Schema{type: :boolean},
muting_notifications: %Schema{type: :boolean},
note: %Schema{type: :string},
requested: %Schema{type: :boolean},
showing_reblogs: %Schema{type: :boolean},
subscribing: %Schema{type: :boolean}
subscribing: %Schema{type: :boolean},
notifying: %Schema{type: :boolean}
},
example: %{
"blocked_by" => false,
@ -36,9 +38,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
"id" => "9tKi3esbG7OQgZ2920",
"muting" => false,
"muting_notifications" => false,
"note" => "",
"requested" => false,
"showing_reblogs" => true,
"subscribing" => false
"subscribing" => false,
"notifying" => false
}
})
end

View file

@ -282,9 +282,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"id" => "9toJCsKN7SmSf3aj5c",
"muting" => false,
"muting_notifications" => false,
"note" => "",
"requested" => false,
"showing_reblogs" => true,
"subscribing" => false
"subscribing" => false,
"notifying" => false
},
"skip_thread_containment" => false,
"tags" => []

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.Endpoint do
alias Pleroma.Config
socket("/socket", Pleroma.Web.UserSocket)
socket("/live", Phoenix.LiveView.Socket)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])

View file

@ -0,0 +1,14 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ManifestController do
use Pleroma.Web, :controller
plug(:skip_auth when action == :show)
@doc "GET /manifest.json"
def show(conn, _params) do
render(conn, "manifest.json")
end
end

View file

@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Maps
alias Pleroma.User
alias Pleroma.UserNote
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
@ -53,7 +54,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
when action in [:verify_credentials, :endorsements, :identity_proofs]
)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"]}
when action in [:update_credentials, :note]
)
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
@ -79,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
@relationship_actions [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock note)a
plug(
RateLimiter,
@ -435,6 +440,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
end
@doc "POST /api/v1/accounts/:id/note"
def note(
%{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
_params
) do
with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
render(conn, "relationship.json", user: noter, target: target)
end
end
@doc "POST /api/v1/follows"
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
case User.get_cached_by_nickname(uri) do

View file

@ -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

View file

@ -17,6 +17,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
require Logger
@search_limit 40
plug(Pleroma.Web.ApiSpec.CastAndValidate)
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
@ -77,7 +79,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
[
resolve: params[:resolve],
following: params[:following],
limit: params[:limit],
limit: min(params[:limit], @search_limit),
offset: params[:offset],
type: params[:type],
author: get_author(params),

View file

@ -4,11 +4,16 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller
import Ecto.Query
alias Pleroma.FollowingRelationship
alias Pleroma.User
alias Pleroma.UserRelationship
require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action in [:index, :index2])
plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["write"]} when action in [:dismiss])
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
@ -26,7 +31,90 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionController do
}
end
def index2_operation do
%OpenApiSpex.Operation{
tags: ["Suggestions"],
summary: "Follow suggestions",
operationId: "SuggestionController.index2",
responses: %{
200 => Pleroma.Web.ApiSpec.Helpers.empty_array_response()
}
}
end
def dismiss_operation do
%OpenApiSpex.Operation{
tags: ["Suggestions"],
summary: "Remove a suggestion",
operationId: "SuggestionController.dismiss",
parameters: [
OpenApiSpex.Operation.parameter(
:account_id,
:path,
%OpenApiSpex.Schema{type: :string},
"Account to dismiss",
required: true
)
],
responses: %{
200 => Pleroma.Web.ApiSpec.Helpers.empty_object_response()
}
}
end
@doc "GET /api/v1/suggestions"
def index(conn, params),
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
@doc "GET /api/v2/suggestions"
def index2(%{assigns: %{user: user}} = conn, params) do
limit = Map.get(params, :limit, 40) |> min(80)
users =
%{is_suggested: true, invisible: false, limit: limit}
|> User.Query.build()
|> exclude_user(user)
|> exclude_relationships(user, [:block, :mute, :suggestion_dismiss])
|> exclude_following(user)
|> Pleroma.Repo.all()
render(conn, "index.json", %{
users: users,
source: :staff,
for: user,
skip_visibility_check: true
})
end
defp exclude_user(query, %User{id: user_id}) do
where(query, [u], u.id != ^user_id)
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_following(query, %User{id: user_id}) do
query
|> join(:left, [u], r in FollowingRelationship,
as: :following_relationships,
on: r.following_id == u.id and r.follower_id == ^user_id and r.state == :follow_accept
)
|> where([following_relationships: r], is_nil(r.following_id))
end
@doc "DELETE /api/v1/suggestions/:account_id"
def dismiss(%{assigns: %{user: source}} = conn, %{account_id: user_id}) do
with %User{} = target <- User.get_cached_by_id(user_id),
{:ok, _} <- UserRelationship.create(:suggestion_dismiss, source, target) do
json(conn, %{})
end
end
end

View file

@ -24,6 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
with {:ok, follower, _followed, _} <- result do
options = cast_params(params)
set_reblogs_visibility(options[:reblogs], result)
set_subscription(options[:notify], result)
{:ok, follower}
end
end
@ -36,6 +37,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
CommonAPI.show_reblogs(follower, followed)
end
defp set_subscription(true, {:ok, follower, followed, _}) do
User.subscribe(follower, followed)
end
defp set_subscription(false, {:ok, follower, followed, _}) do
User.unsubscribe(follower, followed)
end
defp set_subscription(_, _), do: {:ok, nil}
@spec get_followers(User.t(), map()) :: list(User.t())
def get_followers(user, params \\ %{}) do
user
@ -73,7 +84,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
exclude_visibilities: {:array, :string},
reblogs: :boolean,
with_muted: :boolean,
account_ap_id: :string
account_ap_id: :string,
notify: :boolean
}
changeset = cast({%{}, param_types}, params, Map.keys(param_types))

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
alias Pleroma.FollowingRelationship
alias Pleroma.User
alias Pleroma.UserNote
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
@ -101,6 +102,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
User.following?(target, reading_user)
end
subscribing =
UserRelationship.exists?(
user_relationships,
:inverse_subscription,
target,
reading_user,
&User.subscribed_to?(&2, &1)
)
# NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{
id: to_string(target.id),
@ -138,14 +148,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
target,
&User.muted_notifications?(&1, &2)
),
subscribing:
UserRelationship.exists?(
user_relationships,
:inverse_subscription,
target,
reading_user,
&User.subscribed_to?(&2, &1)
),
subscribing: subscribing,
notifying: subscribing,
requested: follow_state == :follow_pending,
domain_blocking: User.blocks_domain?(reading_user, target),
showing_reblogs:
@ -156,7 +160,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
target,
&User.muting_reblogs?(&1, &2)
),
endorsed: false
endorsed: false,
note:
UserNote.show(
reading_user,
target
)
}
end
@ -261,6 +270,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView 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
@ -269,6 +279,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
ap_id: user.ap_id,
also_known_as: user.also_known_as,
is_confirmed: user.is_confirmed,
is_suggested: user.is_suggested,
tags: user.tags,
hide_followers_count: user.hide_followers_count,
hide_follows_count: user.hide_follows_count,

View file

@ -59,6 +59,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
"mastodon_api",
"mastodon_api_streaming",
"polls",
"v2_suggestions",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
"multifetch",
@ -83,7 +84,13 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
"safe_dm_mentions"
end,
"pleroma_emoji_reactions",
"pleroma_chat_messages"
"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)
end

View file

@ -0,0 +1,28 @@
# 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.SuggestionView do
use Pleroma.Web, :view
alias Pleroma.Web.MastodonAPI.AccountView
@source_types [:staff, :global, :past_interactions]
def render("index.json", %{users: users} = opts) do
Enum.map(users, fn user ->
opts =
opts
|> Map.put(:user, user)
|> Map.delete(:users)
render("show.json", opts)
end)
end
def render("show.json", %{source: source, user: _user} = opts) when source in @source_types do
%{
source: source,
account: AccountView.render("show.json", opts)
}
end
end

View file

@ -151,7 +151,9 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
index_query(user, params)
|> Pagination.fetch_paginated(params)
render(conn, "index.json", chats: chats)
conn
|> add_link_headers(chats)
|> render("index.json", chats: chats)
end
defp index_query(%{id: user_id} = user, params) do

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
import Phoenix.LiveDashboard.Router
pipeline :accepts_html do
plug(:accepts, ["html"])
@ -150,6 +151,7 @@ defmodule Pleroma.Web.Router do
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
post("/remote_interaction", UtilController, :remote_interaction)
end
scope "/api/v1/pleroma", Pleroma.Web do
@ -157,12 +159,11 @@ defmodule Pleroma.Web.Router do
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
# AdminAPI: only admins can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :require_admin])
put("/users/disable_mfa", AdminAPIController, :disable_mfa)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
@ -185,16 +186,61 @@ defmodule Pleroma.Web.Router do
post("/users/follow", UserController, :follow)
post("/users/unfollow", UserController, :unfollow)
delete("/users", UserController, :delete)
post("/users", UserController, :create)
patch("/users/suggest", UserController, :suggest)
patch("/users/unsuggest", UserController, :unsuggest)
get("/relay", RelayController, :index)
post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/instance_document/:name", InstanceDocumentController, :show)
patch("/instance_document/:name", InstanceDocumentController, :update)
delete("/instance_document/:name", InstanceDocumentController, :delete)
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
get("/config", ConfigController, :show)
post("/config", ConfigController, :update)
get("/config/descriptions", ConfigController, :descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/oauth_app", OAuthAppController, :index)
post("/oauth_app", OAuthAppController, :create)
patch("/oauth_app/:id", OAuthAppController, :update)
delete("/oauth_app/:id", OAuthAppController, :delete)
get("/media_proxy_caches", MediaProxyCacheController, :index)
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
get("/frontends", FrontendController, :index)
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
end
# AdminAPI: admins and mods (staff) can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
patch("/users/activate", UserController, :activate)
patch("/users/deactivate", UserController, :deactivate)
patch("/users/approve", UserController, :approve)
get("/relay", RelayController, :index)
post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow)
delete("/users", UserController, :delete)
post("/users/invite_token", InviteController, :create)
get("/users/invites", InviteController, :index)
@ -214,13 +260,6 @@ defmodule Pleroma.Web.Router do
get("/instances/:instance/statuses", InstanceController, :list_statuses)
delete("/instances/:instance", InstanceController, :delete)
get("/instance_document/:name", InstanceDocumentController, :show)
patch("/instance_document/:name", InstanceDocumentController, :update)
delete("/instance_document/:name", InstanceDocumentController, :delete)
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update)
@ -232,39 +271,19 @@ defmodule Pleroma.Web.Router do
delete("/statuses/:id", StatusController, :delete)
get("/statuses", StatusController, :index)
get("/config", ConfigController, :show)
post("/config", ConfigController, :update)
get("/config/descriptions", ConfigController, :descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)
get("/oauth_app", OAuthAppController, :index)
post("/oauth_app", OAuthAppController, :create)
patch("/oauth_app/:id", OAuthAppController, :update)
delete("/oauth_app/:id", OAuthAppController, :delete)
get("/media_proxy_caches", MediaProxyCacheController, :index)
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
get("/frontends", FrontendController, :index)
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
end
scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/pack" do
pipe_through([:admin_api, :require_admin])
pipe_through(:admin_api)
post("/", EmojiPackController, :create)
patch("/", EmojiPackController, :update)
@ -279,7 +298,7 @@ defmodule Pleroma.Web.Router do
# Modifying packs
scope "/packs" do
pipe_through([:admin_api, :require_admin])
pipe_through(:admin_api)
get("/import", EmojiPackController, :import_from_filesystem)
get("/remote", EmojiPackController, :remote)
@ -456,6 +475,7 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
post("/accounts/:id/note", AccountController, :note)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read)
@ -535,6 +555,7 @@ defmodule Pleroma.Web.Router do
delete("/push/subscription", SubscriptionController, :delete)
get("/suggestions", SuggestionController, :index)
delete("/suggestions/:account_id", SuggestionController, :dismiss)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
@ -579,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
@ -586,6 +609,8 @@ defmodule Pleroma.Web.Router do
get("/search", SearchController, :search2)
post("/media", MediaController, :create2)
get("/suggestions", SuggestionController, :index2)
end
scope "/api", Pleroma.Web do
@ -627,6 +652,11 @@ defmodule Pleroma.Web.Router do
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
# Notice compatibility routes for other frontends
get("/@:nickname/:id", OStatus.OStatusController, :notice)
get("/@:nickname/posts/:id", OStatus.OStatusController, :notice)
get("/:nickname/status/:id", OStatus.OStatusController, :notice)
# Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
@ -736,6 +766,12 @@ defmodule Pleroma.Web.Router do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/", Pleroma.Web do
pipe_through(:api)
get("/manifest.json", ManifestController, :show)
end
scope "/", Pleroma.Web do
pipe_through(:pleroma_html)
@ -757,6 +793,11 @@ defmodule Pleroma.Web.Router do
end
end
scope "/" do
pipe_through([:pleroma_html, :authenticate, :require_admin])
live_dashboard("/phoenix/live_dashboard")
end
# Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do
@test_actions [

View file

@ -167,6 +167,15 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id)
defp assign_id(%{path_info: ["@" <> _nickname, notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id)
defp assign_id(%{path_info: ["@" <> _nickname, "posts", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id)
defp assign_id(%{path_info: [_nickname, "status", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id)
defp assign_id(%{path_info: ["users", user_id]} = conn, _opts),
do: assign(conn, :username_or_id, user_id)

View file

@ -62,6 +62,15 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
def remote_interaction(%{body_params: %{ap_id: ap_id, profile: profile}} = conn, _params) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do
conn
|> json(%{url: String.replace(template, "{uri}", ap_id)})
else
_e -> json(conn, %{error: "Couldn't find user"})
end
end
def frontend_configurations(conn, _params) do
render(conn, "frontend_configurations.json")
end
@ -123,8 +132,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
def delete_account(%{assigns: %{user: user}} = conn, params) do
password = params[:password] || ""
def delete_account(%{assigns: %{user: user}, body_params: body_params} = conn, params) do
# This endpoint can accept a query param or JSON body for backwards-compatibility.
# Submitting a JSON body is recommended, so passwords don't end up in server logs.
password = body_params[:password] || params[:password] || ""
case CommonAPI.Utils.confirm_current_password(user, password) do
{:ok, user} ->

View file

@ -0,0 +1,28 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ManifestView do
use Pleroma.Web, :view
alias Pleroma.Config
alias Pleroma.Web.Endpoint
def render("manifest.json", _params) do
%{
name: Config.get([:instance, :name]),
description: Config.get([:instance, :description]),
icons: Config.get([:manifest, :icons]),
theme_color: Config.get([:manifest, :theme_color]),
background_color: Config.get([:manifest, :background_color]),
display: "standalone",
scope: Endpoint.url(),
start_url: "/",
categories: [
"social"
],
serviceworker: %{
src: "/sw.js"
}
}
end
end