aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/application.ex2
-rw-r--r--lib/pleroma/bbs/authenticator.ex3
-rw-r--r--lib/pleroma/bbs/handler.ex4
-rw-r--r--lib/pleroma/mfa.ex3
-rw-r--r--lib/pleroma/plugs/authentication_plug.ex7
-rw-r--r--lib/pleroma/scheduled_activity.ex2
-rw-r--r--lib/pleroma/user.ex22
-rw-r--r--lib/pleroma/user/welcome_message.ex4
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex4
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex10
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/delete_validator.ex1
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex21
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex8
-rw-r--r--lib/pleroma/web/admin_api/admin_api_controller.ex9
-rw-r--r--lib/pleroma/web/api_spec/operations/status_operation.ex499
-rw-r--r--lib/pleroma/web/api_spec/operations/timeline_operation.ex199
-rw-r--r--lib/pleroma/web/api_spec/schemas/status.ex198
-rw-r--r--lib/pleroma/web/api_spec/schemas/visibility_scope.ex2
-rw-r--r--lib/pleroma/web/auth/totp_authenticator.ex3
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex22
-rw-r--r--lib/pleroma/web/common_api/common_api.ex58
-rw-r--r--lib/pleroma/web/common_api/utils.ex18
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex75
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex21
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex18
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex11
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex95
-rw-r--r--lib/pleroma/web/mongooseim/mongoose_im_controller.ex3
-rw-r--r--lib/pleroma/web/streamer/streamer.ex73
-rw-r--r--lib/pleroma/workers/scheduled_activity_worker.ex2
30 files changed, 1173 insertions, 224 deletions
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index a00bc0624..9d3d92b38 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -56,7 +56,7 @@ defmodule Pleroma.Application do
if (major == 22 and minor < 2) or major < 22 do
raise "
!!!OTP VERSION WARNING!!!
- You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains.
+ You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2.
"
end
else
diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex
index e5b37f33e..d4494b003 100644
--- a/lib/pleroma/bbs/authenticator.ex
+++ b/lib/pleroma/bbs/authenticator.ex
@@ -4,7 +4,6 @@
defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator
- alias Comeonin.Pbkdf2
alias Pleroma.User
def authenticate(username, password) do
@@ -12,7 +11,7 @@ defmodule Pleroma.BBS.Authenticator do
password = to_string(password)
with %User{} = user <- User.get_by_nickname(username) do
- Pbkdf2.checkpw(password, user.password_hash)
+ Pbkdf2.verify_pass(password, user.password_hash)
else
_e -> false
end
diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex
index c7bc8ef6c..12d64c2fe 100644
--- a/lib/pleroma/bbs/handler.ex
+++ b/lib/pleroma/bbs/handler.ex
@@ -66,7 +66,7 @@ defmodule Pleroma.BBS.Handler do
with %Activity{} <- Activity.get_by_id(activity_id),
{:ok, _activity} <-
- CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do
+ CommonAPI.post(user, %{status: rest, in_reply_to_status_id: activity_id}) do
IO.puts("Replied!")
else
_e -> IO.puts("Could not reply...")
@@ -78,7 +78,7 @@ defmodule Pleroma.BBS.Handler do
def handle_command(%{user: user} = state, "p " <> text) do
text = String.trim(text)
- with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do
+ with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do
IO.puts("Posted!")
else
_e -> IO.puts("Could not post...")
diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex
index d353a4dad..2b77f5426 100644
--- a/lib/pleroma/mfa.ex
+++ b/lib/pleroma/mfa.ex
@@ -7,7 +7,6 @@ defmodule Pleroma.MFA do
The MFA context.
"""
- alias Comeonin.Pbkdf2
alias Pleroma.User
alias Pleroma.MFA.BackupCodes
@@ -72,7 +71,7 @@ defmodule Pleroma.MFA do
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
def generate_backup_codes(%User{} = user) do
with codes <- BackupCodes.generate(),
- hashed_codes <- Enum.map(codes, &Pbkdf2.hashpwsalt/1),
+ hashed_codes <- Enum.map(codes, &Pbkdf2.hash_pwd_salt/1),
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
{:ok, _} <- User.update_and_set_cache(changeset) do
{:ok, codes}
diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex
index 0061c69dc..ae4a235bd 100644
--- a/lib/pleroma/plugs/authentication_plug.ex
+++ b/lib/pleroma/plugs/authentication_plug.ex
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AuthenticationPlug do
- alias Comeonin.Pbkdf2
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
@@ -18,7 +17,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
end
def checkpw(password, "$pbkdf2" <> _ = password_hash) do
- Pbkdf2.checkpw(password, password_hash)
+ Pbkdf2.verify_pass(password, password_hash)
end
def checkpw(_password, _password_hash) do
@@ -37,7 +36,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
} = conn,
_
) do
- if Pbkdf2.checkpw(password, password_hash) do
+ if Pbkdf2.verify_pass(password, password_hash) do
conn
|> assign(:user, auth_user)
|> OAuthScopesPlug.skip_plug()
@@ -47,7 +46,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
end
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
- Pbkdf2.dummy_checkpw()
+ Pbkdf2.no_user_verify()
conn
end
diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex
index 8ff06a462..0937cb7db 100644
--- a/lib/pleroma/scheduled_activity.ex
+++ b/lib/pleroma/scheduled_activity.ex
@@ -40,7 +40,7 @@ defmodule Pleroma.ScheduledActivity do
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
)
when is_list(media_ids) do
- media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids})
+ media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids})
params =
params
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 2a6a23fec..cba391072 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -9,7 +9,6 @@ defmodule Pleroma.User do
import Ecto.Query
import Ecto, only: [assoc: 2]
- alias Comeonin.Pbkdf2
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Config
@@ -1554,10 +1553,23 @@ defmodule Pleroma.User do
|> Stream.run()
end
- defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do
- {:ok, delete_data, _} = Builder.delete(user, object)
+ defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
+ with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
+ {:ok, delete_data, _} <- Builder.delete(user, object) do
+ Pipeline.common_pipeline(delete_data, local: user.local)
+ else
+ {:find_object, nil} ->
+ # We have the create activity, but not the object, it was probably pruned.
+ # Insert a tombstone and try again
+ with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
+ {:ok, _tombstone} <- Object.create(tombstone_data) do
+ delete_activity(activity, user)
+ end
- Pipeline.common_pipeline(delete_data, local: user.local)
+ e ->
+ Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
+ Logger.error("Error: #{inspect(e)}")
+ end
end
defp delete_activity(%{data: %{"type" => type}} = activity, user)
@@ -1913,7 +1925,7 @@ defmodule Pleroma.User do
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
- change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
+ change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
end
defp put_password_hash(changeset), do: changeset
diff --git a/lib/pleroma/user/welcome_message.ex b/lib/pleroma/user/welcome_message.ex
index f0ac8ebae..f8f520285 100644
--- a/lib/pleroma/user/welcome_message.ex
+++ b/lib/pleroma/user/welcome_message.ex
@@ -10,8 +10,8 @@ defmodule Pleroma.User.WelcomeMessage do
with %User{} = sender_user <- welcome_user(),
message when is_binary(message) <- welcome_message() do
CommonAPI.post(sender_user, %{
- "visibility" => "direct",
- "status" => "@#{user.nickname}\n#{message}"
+ visibility: "direct",
+ status: "@#{user.nickname}\n#{message}"
})
else
_ -> {:ok, nil}
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 4955243ab..d752f4f04 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -439,7 +439,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp do_block(blocker, blocked, activity_id, local) do
- outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked do
@@ -447,8 +446,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if follow_activity, do: unfollow(blocker, blocked, nil, local)
end
- with true <- outgoing_blocks,
- block_data <- make_block_data(blocker, blocked, activity_id),
+ with block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 922a444a9..4a247ad0c 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -62,6 +62,16 @@ defmodule Pleroma.Web.ActivityPub.Builder do
}, []}
end
+ @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
+ def tombstone(actor, id) do
+ {:ok,
+ %{
+ "id" => id,
+ "actor" => actor,
+ "type" => "Tombstone"
+ }, []}
+ end
+
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
def like(actor, object) do
with {:ok, data, meta} <- object_action(actor, object) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
index e06de3dff..f42c03510 100644
--- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
@@ -51,6 +51,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
Page
Question
Video
+ Tombstone
}
def validate_data(cng) do
cng
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index be7b57f13..80701bb63 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -14,7 +14,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@@ -590,6 +592,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
+ User.update_follower_count(followed)
+ User.update_following_count(follower)
+
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
@@ -599,7 +604,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
activity_id: id
})
else
- _e -> :error
+ _e ->
+ :error
end
end
@@ -720,6 +726,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
) do
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
+ else
+ {:error, {:validate_object, _}} = e ->
+ # Check if we have a create activity for this
+ with {:ok, object_id} <- Types.ObjectID.cast(data["object"]),
+ %Activity{data: %{"actor" => actor}} <-
+ Activity.create_by_object_ap_id(object_id) |> Repo.one(),
+ # We have one, insert a tombstone and retry
+ {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
+ {:ok, _tombstone} <- Object.create(tombstone_data) do
+ handle_incoming(data)
+ else
+ _ -> e
+ end
end
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 09b80fa57..f2375bcc4 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.Changeset
alias Ecto.UUID
alias Pleroma.Activity
+ alias Pleroma.Config
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@@ -169,8 +170,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Enqueues an activity for federation if it's local
"""
@spec maybe_federate(any()) :: :ok
- def maybe_federate(%Activity{local: true} = activity) do
- if Pleroma.Config.get!([:instance, :federating]) do
+ def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
+ outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
+
+ with true <- Config.get!([:instance, :federating]),
+ true <- type != "Block" || outgoing_blocks do
Pleroma.Web.Federator.publish(activity)
end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 4db9f4cac..9175b1904 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -844,15 +844,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
+ params =
+ params
+ |> Map.take(["sensitive", "visibility"])
+ |> Map.new(fn {key, value} -> {String.to_existing_atom(key), value} end)
+
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
- {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
+ {:ok, sensitive} = Ecto.Type.cast(:boolean, params[:sensitive])
ModerationLog.insert_log(%{
action: "status_update",
actor: admin,
subject: activity,
sensitive: sensitive,
- visibility: params["visibility"]
+ visibility: params[:visibility]
})
conn
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
new file mode 100644
index 000000000..a6bb87560
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -0,0 +1,499 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.StatusOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.AccountOperation
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ 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: ["Statuses"],
+ summary: "Get multiple statuses by IDs",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ Operation.parameter(
+ :ids,
+ :query,
+ %Schema{type: :array, items: FlakeID},
+ "Array of status IDs"
+ )
+ ],
+ operationId: "StatusController.index",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Publish new status",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ description: "Post a new status",
+ operationId: "StatusController.create",
+ requestBody: request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Status. When `scheduled_at` is present, ScheduledStatus is returned instead",
+ "application/json",
+ %Schema{oneOf: [Status, ScheduledStatus]}
+ ),
+ 422 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "View specific status",
+ description: "View information about a status",
+ operationId: "StatusController.show",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Delete status",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ description: "Delete one of your own statuses",
+ operationId: "StatusController.delete",
+ parameters: [id_param()],
+ responses: %{
+ 200 => empty_object_response(),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def reblog_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Boost",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ description: "Share a status",
+ operationId: "StatusController.reblog",
+ parameters: [id_param()],
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ visibility: %Schema{allOf: [VisibilityScope], default: "public"}
+ }
+ }),
+ responses: %{
+ 200 => status_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unreblog_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Undo boost",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ description: "Undo a reshare of a status",
+ operationId: "StatusController.unreblog",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def favourite_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Favourite",
+ security: [%{"oAuth" => ["write:favourites"]}],
+ description: "Add a status to your favourites list",
+ operationId: "StatusController.favourite",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unfavourite_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Undo favourite",
+ security: [%{"oAuth" => ["write:favourites"]}],
+ description: "Remove a status from your favourites list",
+ operationId: "StatusController.unfavourite",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def pin_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Pin to profile",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ description: "Feature one of your own public statuses at the top of your profile",
+ operationId: "StatusController.pin",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unpin_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Unpin to profile",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ description: "Unfeature a status from the top of your profile",
+ operationId: "StatusController.unpin",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def bookmark_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Bookmark",
+ security: [%{"oAuth" => ["write:bookmarks"]}],
+ description: "Privately bookmark a status",
+ operationId: "StatusController.bookmark",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response()
+ }
+ }
+ end
+
+ def unbookmark_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Undo bookmark",
+ security: [%{"oAuth" => ["write:bookmarks"]}],
+ description: "Remove a status from your private bookmarks",
+ operationId: "StatusController.unbookmark",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response()
+ }
+ }
+ end
+
+ def mute_conversation_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Mute conversation",
+ security: [%{"oAuth" => ["write:mutes"]}],
+ description: "Do not receive notifications for the thread that this status is part of.",
+ operationId: "StatusController.mute_conversation",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unmute_conversation_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Unmute conversation",
+ security: [%{"oAuth" => ["write:mutes"]}],
+ description:
+ "Start receiving notifications again for the thread that this status is part of",
+ operationId: "StatusController.unmute_conversation",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def card_operation do
+ %Operation{
+ tags: ["Statuses"],
+ deprecated: true,
+ summary: "Preview card",
+ description: "Deprecated in favor of card property inlined on Status entity",
+ operationId: "StatusController.card",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["read:statuses"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Card", "application/json", %Schema{
+ type: :object,
+ nullable: true,
+ properties: %{
+ type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
+ provider_name: %Schema{type: :string, nullable: true},
+ provider_url: %Schema{type: :string, format: :uri},
+ url: %Schema{type: :string, format: :uri},
+ image: %Schema{type: :string, nullable: true, format: :uri},
+ title: %Schema{type: :string},
+ description: %Schema{type: :string}
+ }
+ })
+ }
+ }
+ end
+
+ def favourited_by_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Favourited by",
+ description: "View who favourited a given status",
+ operationId: "StatusController.favourited_by",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Accounts",
+ "application/json",
+ AccountOperation.array_of_accounts()
+ ),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def reblogged_by_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Boosted by",
+ description: "View who boosted a given status",
+ operationId: "StatusController.reblogged_by",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Accounts",
+ "application/json",
+ AccountOperation.array_of_accounts()
+ ),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def context_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Parent and child statuses",
+ description: "View statuses above and below this status in the thread",
+ operationId: "StatusController.context",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 => Operation.response("Context", "application/json", context())
+ }
+ }
+ end
+
+ def favourites_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Favourited statuses",
+ description: "Statuses the user has favourited",
+ operationId: "StatusController.favourites",
+ parameters: pagination_params(),
+ security: [%{"oAuth" => ["read:favourites"]}],
+ responses: %{
+ 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def bookmarks_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Bookmarked statuses",
+ description: "Statuses the user has bookmarked",
+ operationId: "StatusController.bookmarks",
+ parameters: [
+ Operation.parameter(:with_relationships, :query, BooleanLike, "Include relationships")
+ | pagination_params()
+ ],
+ security: [%{"oAuth" => ["read:bookmarks"]}],
+ responses: %{
+ 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ defp array_of_statuses do
+ %Schema{type: :array, items: Status, example: [Status.schema().example]}
+ end
+
+ defp create_request do
+ %Schema{
+ title: "StatusCreateRequest",
+ type: :object,
+ properties: %{
+ status: %Schema{
+ type: :string,
+ description:
+ "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
+ },
+ media_ids: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Array of Attachment ids to be attached as media."
+ },
+ poll: %Schema{
+ type: :object,
+ required: [:options],
+ properties: %{
+ options: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Array of possible answers. Must be provided with `poll[expires_in]`."
+ },
+ expires_in: %Schema{
+ type: :integer,
+ description:
+ "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
+ },
+ multiple: %Schema{type: :boolean, description: "Allow multiple choices?"},
+ hide_totals: %Schema{
+ type: :boolean,
+ description: "Hide vote counts until the poll ends?"
+ }
+ }
+ },
+ in_reply_to_id: %Schema{
+ allOf: [FlakeID],
+ description: "ID of the status being replied to, if status is a reply"
+ },
+ sensitive: %Schema{
+ type: :boolean,
+ description: "Mark status and attached media as sensitive?"
+ },
+ spoiler_text: %Schema{
+ type: :string,
+ description:
+ "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
+ },
+ scheduled_at: %Schema{
+ type: :string,
+ format: :"date-time",
+ nullable: true,
+ description:
+ "ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
+ },
+ language: %Schema{type: :string, description: "ISO 639 language code for this status."},
+ # Pleroma-specific properties:
+ preview: %Schema{
+ type: :boolean,
+ description:
+ "If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
+ },
+ content_type: %Schema{
+ type: :string,
+ description:
+ "The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
+ },
+ to: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description:
+ "A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
+ },
+ visibility: %Schema{
+ anyOf: [
+ VisibilityScope,
+ %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
+ ],
+ description:
+ "Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`"
+ },
+ expires_in: %Schema{
+ type: :integer,
+ description:
+ "The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour."
+ },
+ in_reply_to_conversation_id: %Schema{
+ type: :string,
+ description:
+ "Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
+ }
+ },
+ example: %{
+ "status" => "What time is it?",
+ "sensitive" => "false",
+ "poll" => %{
+ "options" => ["Cofe", "Adventure"],
+ "expires_in" => 420
+ }
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, FlakeID, "Status ID",
+ example: "9umDrYheeY451cQnEe",
+ required: true
+ )
+ end
+
+ defp status_response do
+ Operation.response("Status", "application/json", Status)
+ end
+
+ defp context do
+ %Schema{
+ title: "StatusContext",
+ description:
+ "Represents the tree around a given status. Used for reconstructing threads of statuses.",
+ type: :object,
+ required: [:ancestors, :descendants],
+ properties: %{
+ ancestors: array_of_statuses(),
+ descendants: array_of_statuses()
+ },
+ example: %{
+ "ancestors" => [Status.schema().example],
+ "descendants" => [Status.schema().example]
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
new file mode 100644
index 000000000..1b89035d4
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
@@ -0,0 +1,199 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.TimelineOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def home_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Home timeline",
+ description: "View statuses from followed users",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ local_param(),
+ with_muted_param(),
+ exclude_visibilities_param(),
+ reply_visibility_param(),
+ with_relationships_param() | pagination_params()
+ ],
+ operationId: "TimelineController.home",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def direct_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Direct timeline",
+ description:
+ "View statuses with a “direct” privacy, from your account or in your notifications",
+ deprecated: true,
+ parameters: pagination_params(),
+ security: [%{"oAuth" => ["read:statuses"]}],
+ operationId: "TimelineController.direct",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def public_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Public timeline",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ local_param(),
+ only_media_param(),
+ with_muted_param(),
+ exclude_visibilities_param(),
+ reply_visibility_param(),
+ with_relationships_param() | pagination_params()
+ ],
+ operationId: "TimelineController.public",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses()),
+ 401 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def hashtag_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Hashtag timeline",
+ description: "View public statuses containing the given hashtag",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ Operation.parameter(
+ :tag,
+ :path,
+ %Schema{type: :string},
+ "Content of a #hashtag, not including # symbol.",
+ required: true
+ ),
+ Operation.parameter(
+ :any,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Statuses that also includes any of these tags"
+ ),
+ Operation.parameter(
+ :all,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Statuses that also includes all of these tags"
+ ),
+ Operation.parameter(
+ :none,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Statuses that do not include these tags"
+ ),
+ local_param(),
+ only_media_param(),
+ with_muted_param(),
+ exclude_visibilities_param(),
+ with_relationships_param() | pagination_params()
+ ],
+ operationId: "TimelineController.hashtag",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def list_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "List timeline",
+ description: "View statuses in the given list timeline",
+ security: [%{"oAuth" => ["read:lists"]}],
+ parameters: [
+ Operation.parameter(
+ :list_id,
+ :path,
+ %Schema{type: :string},
+ "Local ID of the list in the database",
+ required: true
+ ),
+ with_muted_param(),
+ exclude_visibilities_param(),
+ with_relationships_param() | pagination_params()
+ ],
+ operationId: "TimelineController.list",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ defp array_of_statuses do
+ %Schema{
+ title: "ArrayOfStatuses",
+ type: :array,
+ items: Status,
+ example: [Status.schema().example]
+ }
+ end
+
+ defp with_relationships_param do
+ Operation.parameter(:with_relationships, :query, BooleanLike, "Include relationships")
+ end
+
+ defp local_param do
+ Operation.parameter(
+ :local,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Show only local statuses?"
+ )
+ end
+
+ defp with_muted_param do
+ Operation.parameter(:with_muted, :query, BooleanLike, "Includeactivities by muted users")
+ end
+
+ defp exclude_visibilities_param do
+ Operation.parameter(
+ :exclude_visibilities,
+ :query,
+ %Schema{type: :array, items: VisibilityScope},
+ "Exclude the statuses with the given visibilities"
+ )
+ end
+
+ defp reply_visibility_param do
+ Operation.parameter(
+ :reply_visibility,
+ :query,
+ %Schema{type: :string, enum: ["following", "self"]},
+ "Filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you."
+ )
+ end
+
+ defp only_media_param do
+ Operation.parameter(
+ :only_media,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Show only statuses with media attached?"
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index 2572c9641..8b87cb25b 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -19,60 +19,127 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
description: "Response schema for a status",
type: :object,
properties: %{
- account: Account,
+ account: %Schema{allOf: [Account], description: "The account that authored this status"},
application: %Schema{
+ description: "The application used to post this status",
type: :object,
properties: %{
name: %Schema{type: :string},
website: %Schema{type: :string, nullable: true, format: :uri}
}
},
- bookmarked: %Schema{type: :boolean},
+ bookmarked: %Schema{type: :boolean, description: "Have you bookmarked this status?"},
card: %Schema{
type: :object,
nullable: true,
+ description: "Preview card for links included within status content",
+ required: [:url, :title, :description, :type],
properties: %{
- type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
- provider_name: %Schema{type: :string, nullable: true},
- provider_url: %Schema{type: :string, format: :uri},
- url: %Schema{type: :string, format: :uri},
- image: %Schema{type: :string, nullable: true, format: :uri},
- title: %Schema{type: :string},
- description: %Schema{type: :string}
+ type: %Schema{
+ type: :string,
+ enum: ["link", "photo", "video", "rich"],
+ description: "The type of the preview card"
+ },
+ provider_name: %Schema{
+ type: :string,
+ nullable: true,
+ description: "The provider of the original resource"
+ },
+ provider_url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "A link to the provider of the original resource"
+ },
+ url: %Schema{type: :string, format: :uri, description: "Location of linked resource"},
+ image: %Schema{
+ type: :string,
+ nullable: true,
+ format: :uri,
+ description: "Preview thumbnail"
+ },
+ title: %Schema{type: :string, description: "Title of linked resource"},
+ description: %Schema{type: :string, description: "Description of preview"}
}
},
- content: %Schema{type: :string, format: :html},
- created_at: %Schema{type: :string, format: "date-time"},
- emojis: %Schema{type: :array, items: Emoji},
- favourited: %Schema{type: :boolean},
- favourites_count: %Schema{type: :integer},
+ content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"},
+ created_at: %Schema{
+ type: :string,
+ format: "date-time",
+ description: "The date when this status was created"
+ },
+ emojis: %Schema{
+ type: :array,
+ items: Emoji,
+ description: "Custom emoji to be used when rendering status content"
+ },
+ favourited: %Schema{type: :boolean, description: "Have you favourited this status?"},
+ favourites_count: %Schema{
+ type: :integer,
+ description: "How many favourites this status has received"
+ },
id: FlakeID,
- in_reply_to_account_id: %Schema{type: :string, nullable: true},
- in_reply_to_id: %Schema{type: :string, nullable: true},
- language: %Schema{type: :string, nullable: true},
+ in_reply_to_account_id: %Schema{
+ allOf: [FlakeID],
+ nullable: true,
+ description: "ID of the account being replied to"
+ },
+ in_reply_to_id: %Schema{
+ allOf: [FlakeID],
+ nullable: true,
+ description: "ID of the status being replied"
+ },
+ language: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Primary language of this status"
+ },
media_attachments: %Schema{
type: :array,
- items: Attachment
+ items: Attachment,
+ description: "Media that is attached to this status"
},
mentions: %Schema{
type: :array,
+ description: "Mentions of users within the status content",
items: %Schema{
type: :object,
properties: %{
- id: %Schema{type: :string},
- acct: %Schema{type: :string},
- username: %Schema{type: :string},
- url: %Schema{type: :string, format: :uri}
+ id: %Schema{allOf: [FlakeID], description: "The account id of the mentioned user"},
+ acct: %Schema{
+ type: :string,
+ description:
+ "The webfinger acct: URI of the mentioned user. Equivalent to `username` for local users, or `username@domain` for remote users."
+ },
+ username: %Schema{type: :string, description: "The username of the mentioned user"},
+ url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "The location of the mentioned user's profile"
+ }
}
}
},
- muted: %Schema{type: :boolean},
- pinned: %Schema{type: :boolean},
+ muted: %Schema{
+ type: :boolean,
+ description: "Have you muted notifications for this status's conversation?"
+ },
+ pinned: %Schema{
+ type: :boolean,
+ description: "Have you pinned this status? Only appears if the status is pinnable."
+ },
pleroma: %Schema{
type: :object,
properties: %{
- content: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
- conversation_id: %Schema{type: :integer},
+ content: %Schema{
+ type: :object,
+ additionalProperties: %Schema{type: :string},
+ description:
+ "A map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`"
+ },
+ conversation_id: %Schema{
+ type: :integer,
+ description: "The ID of the AP context the status is associated with (if any)"
+ },
direct_conversation_id: %Schema{
type: :integer,
nullable: true,
@@ -81,6 +148,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
},
emoji_reactions: %Schema{
type: :array,
+ description:
+ "A list with emoji / reaction maps. Contains no information about the reacting users, for that use the /statuses/:id/reactions endpoint.",
items: %Schema{
type: :object,
properties: %{
@@ -90,27 +159,74 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
}
}
},
- expires_at: %Schema{type: :string, format: "date-time", nullable: true},
- in_reply_to_account_acct: %Schema{type: :string, nullable: true},
- local: %Schema{type: :boolean},
- spoiler_text: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
- thread_muted: %Schema{type: :boolean}
+ expires_at: %Schema{
+ type: :string,
+ format: "date-time",
+ nullable: true,
+ description:
+ "A datetime (ISO 8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire"
+ },
+ in_reply_to_account_acct: %Schema{
+ type: :string,
+ nullable: true,
+ description: "The `acct` property of User entity for replied user (if any)"
+ },
+ local: %Schema{
+ type: :boolean,
+ description: "`true` if the post was made on the local instance"
+ },
+ spoiler_text: %Schema{
+ type: :object,
+ additionalProperties: %Schema{type: :string},
+ description:
+ "A map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`."
+ },
+ thread_muted: %Schema{
+ type: :boolean,
+ description: "`true` if the thread the post belongs to is muted"
+ }
}
},
- poll: %Schema{type: Poll, nullable: true},
+ poll: %Schema{allOf: [Poll], nullable: true, description: "The poll attached to the status"},
reblog: %Schema{
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
- nullable: true
+ nullable: true,
+ description: "The status being reblogged"
+ },
+ reblogged: %Schema{type: :boolean, description: "Have you boosted this status?"},
+ reblogs_count: %Schema{
+ type: :integer,
+ description: "How many boosts this status has received"
+ },
+ replies_count: %Schema{
+ type: :integer,
+ description: "How many replies this status has received"
+ },
+ sensitive: %Schema{
+ type: :boolean,
+ description: "Is this status marked as sensitive content?"
+ },
+ spoiler_text: %Schema{
+ type: :string,
+ description:
+ "Subject or summary line, below which status content is collapsed until expanded"
},
- reblogged: %Schema{type: :boolean},
- reblogs_count: %Schema{type: :integer},
- replies_count: %Schema{type: :integer},
- sensitive: %Schema{type: :boolean},
- spoiler_text: %Schema{type: :string},
tags: %Schema{type: :array, items: Tag},
- uri: %Schema{type: :string, format: :uri},
- url: %Schema{type: :string, nullable: true, format: :uri},
- visibility: VisibilityScope
+ uri: %Schema{
+ type: :string,
+ format: :uri,
+ description: "URI of the status used for federation"
+ },
+ url: %Schema{
+ type: :string,
+ nullable: true,
+ format: :uri,
+ description: "A link to the status's HTML representation"
+ },
+ visibility: %Schema{
+ allOf: [VisibilityScope],
+ description: "Visibility of this status"
+ }
},
example: %{
"account" => %{
diff --git a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
index 8c81a4d73..831734e27 100644
--- a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
+++ b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
@@ -9,6 +9,6 @@ defmodule Pleroma.Web.ApiSpec.Schemas.VisibilityScope do
title: "VisibilityScope",
description: "Status visibility",
type: :string,
- enum: ["public", "unlisted", "private", "direct"]
+ enum: ["public", "unlisted", "private", "direct", "list"]
})
end
diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex
index 98aca9a51..04e489c83 100644
--- a/lib/pleroma/web/auth/totp_authenticator.ex
+++ b/lib/pleroma/web/auth/totp_authenticator.ex
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.TOTPAuthenticator do
- alias Comeonin.Pbkdf2
alias Pleroma.MFA
alias Pleroma.MFA.TOTP
alias Pleroma.User
@@ -31,7 +30,7 @@ defmodule Pleroma.Web.Auth.TOTPAuthenticator do
code
)
when is_list(codes) and is_binary(code) do
- hash_code = Enum.find(codes, fn hash -> Pbkdf2.checkpw(code, hash) end)
+ hash_code = Enum.find(codes, fn hash -> Pbkdf2.verify_pass(code, hash) end)
if hash_code do
MFA.invalidate_backup_code(user, hash_code)
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 244cf2be5..3f1a50b96 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -58,16 +58,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp put_params(draft, params) do
- params = Map.put_new(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
%__MODULE__{draft | params: params}
end
- defp status(%{params: %{"status" => status}} = draft) do
+ defp status(%{params: %{status: status}} = draft) do
%__MODULE__{draft | status: String.trim(status)}
end
defp summary(%{params: params} = draft) do
- %__MODULE__{draft | summary: Map.get(params, "spoiler_text", "")}
+ %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")}
end
defp full_payload(%{status: status, summary: summary} = draft) do
@@ -84,20 +84,20 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
%__MODULE__{draft | attachments: attachments}
end
- defp in_reply_to(%{params: %{"in_reply_to_status_id" => ""}} = draft), do: draft
+ defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
- defp in_reply_to(%{params: %{"in_reply_to_status_id" => id}} = draft) when is_binary(id) do
+ defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
%__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
end
- defp in_reply_to(%{params: %{"in_reply_to_status_id" => %Activity{} = in_reply_to}} = draft) do
+ defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do
%__MODULE__{draft | in_reply_to: in_reply_to}
end
defp in_reply_to(draft), do: draft
defp in_reply_to_conversation(draft) do
- in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"])
+ in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
end
@@ -112,7 +112,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp expires_at(draft) do
- case CommonAPI.check_expiry_date(draft.params["expires_in"]) do
+ case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
{:error, message} -> add_error(draft, message)
end
@@ -144,7 +144,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
addressed_users =
draft.mentions
|> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
- |> Utils.get_addressed_users(draft.params["to"])
+ |> Utils.get_addressed_users(draft.params[:to])
{to, cc} =
Utils.get_to_and_cc(
@@ -164,7 +164,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp sensitive(draft) do
- sensitive = draft.params["sensitive"] || Enum.member?(draft.tags, {"#nsfw", "nsfw"})
+ sensitive = draft.params[:sensitive] || Enum.member?(draft.tags, {"#nsfw", "nsfw"})
%__MODULE__{draft | sensitive: sensitive}
end
@@ -191,7 +191,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp preview?(draft) do
- preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"])
+ preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params[:preview])
%__MODULE__{draft | preview?: preview?}
end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index c538a634f..601caeb46 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -83,33 +83,51 @@ defmodule Pleroma.Web.CommonAPI do
end
def delete(activity_id, user) do
- with {_, %Activity{data: %{"object" => _}} = activity} <-
- {:find_activity, Activity.get_by_id_with_object(activity_id)},
- %Object{} = object <- Object.normalize(activity),
+ with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
+ {:find_activity, Activity.get_by_id(activity_id)},
+ {_, %Object{} = object, _} <-
+ {:find_object, Object.normalize(activity, false), activity},
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
{:ok, delete}
else
- {:find_activity, _} -> {:error, :not_found}
- _ -> {:error, dgettext("errors", "Could not delete")}
+ {:find_activity, _} ->
+ {:error, :not_found}
+
+ {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
+ # We have the create activity, but not the object, it was probably pruned.
+ # Insert a tombstone and try again
+ with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
+ {:ok, _tombstone} <- Object.create(tombstone_data) do
+ delete(activity_id, user)
+ else
+ _ ->
+ Logger.error(
+ "Could not insert tombstone for missing object on deletion. Object is #{object}."
+ )
+
+ {:error, dgettext("errors", "Could not delete")}
+ end
+
+ _ ->
+ {:error, dgettext("errors", "Could not delete")}
end
end
def repeat(id, user, params \\ %{}) do
- with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
- {:find_activity, Activity.get_by_id(id)},
- object <- Object.normalize(activity),
- announce_activity <- Utils.get_existing_announce(user.ap_id, object),
- public <- public_announce?(object, params) do
+ with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id) do
+ object = Object.normalize(activity)
+ announce_activity = Utils.get_existing_announce(user.ap_id, object)
+ public = public_announce?(object, params)
+
if announce_activity do
{:ok, announce_activity, object}
else
ActivityPub.announce(user, object, nil, true, public)
end
else
- {:find_activity, _} -> {:error, :not_found}
- _ -> {:error, dgettext("errors", "Could not repeat")}
+ _ -> {:error, :not_found}
end
end
@@ -267,7 +285,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def public_announce?(_, %{"visibility" => visibility})
+ def public_announce?(_, %{visibility: visibility})
when visibility in ~w{public unlisted private direct},
do: visibility in ~w(public unlisted)
@@ -277,11 +295,11 @@ defmodule Pleroma.Web.CommonAPI do
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
- def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
+ def get_visibility(%{visibility: visibility}, in_reply_to, _)
when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
- def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
+ def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
visibility = {:list, String.to_integer(list_id)}
{visibility, get_replied_to_visibility(in_reply_to)}
end
@@ -339,7 +357,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def post(user, %{"status" => _} = data) do
+ def post(user, %{status: _} = data) do
with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
draft.changes
|> ActivityPub.create(draft.preview?)
@@ -448,11 +466,11 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
- toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
+ defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
+ toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
end
- defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
+ defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
when is_boolean(sensitive) do
new_data = Map.put(object.data, "sensitive", sensitive)
@@ -466,7 +484,7 @@ defmodule Pleroma.Web.CommonAPI do
defp toggle_sensitive(activity, _), do: {:ok, activity}
- defp set_visibility(activity, %{"visibility" => visibility}) do
+ defp set_visibility(activity, %{visibility: visibility}) do
Utils.update_activity_visibility(activity, visibility)
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 793f2e7f8..e8deee223 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -22,11 +22,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
require Logger
require Pleroma.Constants
- def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
+ def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do
attachments_from_ids_descs(ids, desc)
end
- def attachments_from_ids(%{"media_ids" => ids} = _) do
+ def attachments_from_ids(%{media_ids: ids}) do
attachments_from_ids_no_descs(ids)
end
@@ -37,11 +37,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def attachments_from_ids_no_descs(ids) do
Enum.map(ids, fn media_id ->
case Repo.get(Object, media_id) do
- %Object{data: data} = _ -> data
+ %Object{data: data} -> data
_ -> nil
end
end)
- |> Enum.filter(& &1)
+ |> Enum.reject(&is_nil/1)
end
def attachments_from_ids_descs([], _), do: []
@@ -51,14 +51,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
Enum.map(ids, fn media_id ->
case Repo.get(Object, media_id) do
- %Object{data: data} = _ ->
+ %Object{data: data} ->
Map.put(data, "name", descs[media_id])
_ ->
nil
end
end)
- |> Enum.filter(& &1)
+ |> Enum.reject(&is_nil/1)
end
@spec get_to_and_cc(
@@ -140,7 +140,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> make_poll_data()
end
- def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
+ def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)
when is_list(options) do
limits = Pleroma.Config.get([:instance, :poll_limits])
@@ -163,7 +163,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> DateTime.add(expires_in)
|> DateTime.to_iso8601()
- key = if truthy_param?(data["poll"]["multiple"]), do: "anyOf", else: "oneOf"
+ key = if truthy_param?(data.poll[:multiple]), do: "anyOf", else: "oneOf"
poll = %{"type" => "Question", key => option_notes, "closed" => end_time}
{:ok, {poll, emoji}}
@@ -213,7 +213,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Map.get("attachment_links", Config.get([:instance, :attachment_links]))
|> truthy_param?()
- content_type = get_content_type(data["content_type"])
+ content_type = get_content_type(data[:content_type])
options =
if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 12e3ba15e..25e499a77 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -24,6 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
@@ -97,12 +98,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
+
@doc """
GET `/api/v1/statuses?ids[]=1&ids[]=2`
`ids` query param is required
"""
- def index(%{assigns: %{user: user}} = conn, %{"ids" => ids} = params) do
+ def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
limit = 100
activities =
@@ -125,21 +128,29 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
Creates a scheduled status when `scheduled_at` param is present and it's far enough
"""
def create(
- %{assigns: %{user: user}} = conn,
- %{"status" => _, "scheduled_at" => scheduled_at} = params
+ %{
+ assigns: %{user: user},
+ body_params: %{status: _, scheduled_at: scheduled_at} = params
+ } = conn,
+ _
)
when not is_nil(scheduled_at) do
- params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
+
+ attrs = %{
+ params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
+ scheduled_at: scheduled_at
+ }
with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
- attrs <- %{"params" => params, "scheduled_at" => scheduled_at},
{:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", scheduled_activity: scheduled_activity)
else
{:far_enough, _} ->
- create(conn, Map.drop(params, ["scheduled_at"]))
+ params = Map.drop(params, [:scheduled_at])
+ create(%Plug.Conn{conn | body_params: params}, %{})
error ->
error
@@ -151,8 +162,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
Creates a regular status
"""
- def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
- params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
+ params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
with {:ok, activity} <- CommonAPI.post(user, params) do
try_render(conn, "show.json",
@@ -169,12 +180,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
end
- def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
- create(conn, Map.put(params, "status", ""))
+ def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
+ params = Map.put(params, :status, "")
+ create(%Plug.Conn{conn | body_params: params}, %{})
end
@doc "GET /api/v1/statuses/:id"
- def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json",
@@ -188,7 +200,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "DELETE /api/v1/statuses/:id"
- def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
else
@@ -198,7 +210,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/reblog"
- def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
+ def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
%Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
@@ -206,7 +218,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unreblog"
- def unreblog(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
+ def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
@@ -214,7 +226,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/favourite"
- def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
+ def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -222,7 +234,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unfavourite"
- def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
+ def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -230,21 +242,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/pin"
- def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unpin"
- def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/bookmark"
- def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
@@ -254,7 +266,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unbookmark"
- def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
@@ -264,7 +276,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/mute"
- def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.add_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -272,7 +284,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unmute"
- def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.remove_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -281,7 +293,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
@doc "GET /api/v1/statuses/:id/card"
@deprecated "https://github.com/tootsuite/mastodon/pull/11213"
- def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
+ def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
with %Activity{} = activity <- Activity.get_by_id(status_id),
true <- Visibility.visible_for_user?(activity, user) do
data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
@@ -292,7 +304,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/favourited_by"
- def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
@@ -312,7 +324,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/reblogged_by"
- def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces, "id" => ap_id}} <-
@@ -344,7 +356,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/context"
- def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def context(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
@@ -359,11 +371,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
@doc "GET /api/v1/favourites"
def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
- activities =
- ActivityPub.fetch_favourites(
- user,
- Map.take(params, Pleroma.Pagination.page_keys())
- )
+ params =
+ params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
+ |> Map.take(Pleroma.Pagination.page_keys())
+
+ activities = ActivityPub.fetch_favourites(user, params)
conn
|> add_link_headers(activities)
diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
index c93a43969..f91df9ab7 100644
--- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
@@ -5,11 +5,26 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
-
require Logger
- plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %OpenApiSpex.Operation{
+ tags: ["Suggestions"],
+ summary: "Follow suggestions (Not implemented)",
+ operationId: "SuggestionController.index",
+ responses: %{
+ 200 => Pleroma.Web.ApiSpec.Helpers.empty_array_response()
+ }
+ }
+ end
@doc "GET /api/v1/suggestions"
def index(conn, params),
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index 2d67e19da..bbd576ffd 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
- only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1]
+ only: [add_link_headers: 2, add_link_headers: 3, skip_relationships?: 1]
alias Pleroma.Pagination
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
@@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag])
# TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
@@ -37,10 +38,13 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation
+
# GET /api/v1/timelines/home
def home(%{assigns: %{user: user}} = conn, params) do
params =
params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
@@ -68,6 +72,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
def direct(%{assigns: %{user: user}} = conn, params) do
params =
params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
@@ -90,7 +95,9 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
# GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do
- local_only = truthy_param?(params["local"])
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+
+ local_only = params["local"]
cfg_key =
if local_only do
@@ -157,8 +164,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
# GET /api/v1/timelines/tag/:tag
def hashtag(%{assigns: %{user: user}} = conn, params) do
- local_only = truthy_param?(params["local"])
-
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+ local_only = params["local"]
activities = hashtag_fetching(params, user, local_only)
conn
@@ -172,10 +179,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
end
# GET /api/v1/timelines/list/:list_id
- def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
+ def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do
with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
params =
params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 420bd586f..835dfe9f4 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -261,7 +261,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp prepare_user_bio(%User{bio: ""}), do: ""
defp prepare_user_bio(%User{bio: bio}) when is_binary(bio) do
- bio |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags()
+ bio
+ |> String.replace(~r(<br */?>), "\n")
+ |> Pleroma.HTML.strip_tags()
+ |> HtmlEntities.decode()
end
defp prepare_user_bio(_), do: ""
@@ -334,7 +337,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_role(data, _, _), do: data
defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
- Kernel.put_in(data, [:pleroma, :notification_settings], user.notification_settings)
+ Kernel.put_in(
+ data,
+ [:pleroma, :notification_settings],
+ Map.from_struct(user.notification_settings)
+ )
end
defp maybe_put_notification_settings(data, _, _), do: data
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index e2ffd02d0..94e4595d8 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -12,31 +12,19 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
@behaviour :cowboy_websocket
+ # Client ping period.
+ @tick :timer.seconds(30)
# Cowboy timeout period.
- @timeout :timer.seconds(30)
+ @timeout :timer.seconds(60)
# Hibernate every X messages
@hibernate_every 100
- @streams [
- "public",
- "public:local",
- "public:media",
- "public:local:media",
- "user",
- "user:notification",
- "direct",
- "list",
- "hashtag"
- ]
- @anonymous_streams ["public", "public:local", "hashtag"]
-
def init(%{qs: qs} = req, state) do
- with params <- :cow_qs.parse_qs(qs),
+ with params <- Enum.into(:cow_qs.parse_qs(qs), %{}),
sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
- access_token <- List.keyfind(params, "access_token", 0),
- {_, stream} <- List.keyfind(params, "stream", 0),
- {:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
- topic when is_binary(topic) <- expand_topic(stream, params) do
+ access_token <- Map.get(params, "access_token"),
+ {:ok, user} <- authenticate_request(access_token, sec_websocket),
+ {:ok, topic} <- Streamer.get_topic(Map.get(params, "stream"), user, params) do
req =
if sec_websocket do
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
@@ -44,16 +32,17 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
req
end
- {:cowboy_websocket, req, %{user: user, topic: topic, count: 0}, %{idle_timeout: @timeout}}
+ {:cowboy_websocket, req, %{user: user, topic: topic, count: 0, timer: nil},
+ %{idle_timeout: @timeout}}
else
- {:error, code} ->
- Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
- {:ok, req} = :cowboy_req.reply(code, req)
+ {:error, :bad_topic} ->
+ Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
+ {:ok, req} = :cowboy_req.reply(404, req)
{:ok, req, state}
- error ->
- Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}")
- {:ok, req} = :cowboy_req.reply(400, req)
+ {:error, :unauthorized} ->
+ Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}")
+ {:ok, req} = :cowboy_req.reply(401, req)
{:ok, req, state}
end
end
@@ -66,11 +55,18 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
)
Streamer.add_socket(state.topic, state.user)
- {:ok, state}
+ {:ok, %{state | timer: timer()}}
+ end
+
+ # Client's Pong frame.
+ def websocket_handle(:pong, state) do
+ if state.timer, do: Process.cancel_timer(state.timer)
+ {:ok, %{state | timer: timer()}}
end
# We never receive messages.
- def websocket_handle(_frame, state) do
+ def websocket_handle(frame, state) do
+ Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
end
@@ -94,6 +90,14 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
end
+ # Ping tick. We don't re-queue a timer there, it is instead queued when :pong is received.
+ # As we hibernate there, reset the count to 0.
+ # If the client misses :pong, Cowboy will automatically timeout the connection after
+ # `@idle_timeout`.
+ def websocket_info(:tick, state) do
+ {:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}
+ end
+
def terminate(reason, _req, state) do
Logger.debug(
"#{__MODULE__} terminating websocket connection for user #{
@@ -106,47 +110,24 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
# Public streams without authentication.
- defp allow_request(stream, [nil, nil]) when stream in @anonymous_streams do
+ defp authenticate_request(nil, nil) do
{:ok, nil}
end
# Authenticated streams.
- defp allow_request(stream, [access_token, sec_websocket]) when stream in @streams do
- token =
- with {"access_token", token} <- access_token do
- token
- else
- _ -> sec_websocket
- end
+ defp authenticate_request(access_token, sec_websocket) do
+ token = access_token || sec_websocket
with true <- is_bitstring(token),
%Token{user_id: user_id} <- Repo.get_by(Token, token: token),
user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user}
else
- _ -> {:error, 403}
+ _ -> {:error, :unauthorized}
end
end
- # Not authenticated.
- defp allow_request(stream, _) when stream in @streams, do: {:error, 403}
-
- # No matching stream.
- defp allow_request(_, _), do: {:error, 404}
-
- defp expand_topic("hashtag", params) do
- case List.keyfind(params, "tag", 0) do
- {_, tag} -> "hashtag:#{tag}"
- _ -> nil
- end
- end
-
- defp expand_topic("list", params) do
- case List.keyfind(params, "list", 0) do
- {_, list} -> "list:#{list}"
- _ -> nil
- end
+ defp timer do
+ Process.send_after(self(), :tick, @tick)
end
-
- defp expand_topic(topic, _), do: topic
end
diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
index 1ed6ee521..0814b3bc3 100644
--- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
+++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
@@ -5,7 +5,6 @@
defmodule Pleroma.Web.MongooseIM.MongooseIMController do
use Pleroma.Web, :controller
- alias Comeonin.Pbkdf2
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.User
@@ -28,7 +27,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
def check_password(conn, %{"user" => username, "pass" => password}) do
with %User{password_hash: password_hash, deactivated: false} <-
Repo.get_by(User, nickname: username, local: true),
- true <- Pbkdf2.checkpw(password, password_hash) do
+ true <- Pbkdf2.verify_pass(password, password_hash) do
conn
|> json(true)
else
diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex
index 5ad4aa936..49a400df7 100644
--- a/lib/pleroma/web/streamer/streamer.ex
+++ b/lib/pleroma/web/streamer/streamer.ex
@@ -21,12 +21,68 @@ defmodule Pleroma.Web.Streamer do
def registry, do: @registry
- def add_socket(topic, %User{} = user) do
- if should_env_send?(), do: Registry.register(@registry, user_topic(topic, user), true)
+ @public_streams ["public", "public:local", "public:media", "public:local:media"]
+ @user_streams ["user", "user:notification", "direct"]
+
+ @doc "Expands and authorizes a stream, and registers the process for streaming."
+ @spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) ::
+ {:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}
+ def get_topic_and_add_socket(stream, user, params \\ %{}) do
+ case get_topic(stream, user, params) do
+ {:ok, topic} -> add_socket(topic, user)
+ error -> error
+ end
+ end
+
+ @doc "Expand and authorizes a stream"
+ @spec get_topic(stream :: String.t(), User.t() | nil, Map.t()) ::
+ {:ok, topic :: String.t()} | {:error, :bad_topic}
+ def get_topic(stream, user, params \\ %{})
+
+ # Allow all public steams.
+ def get_topic(stream, _, _) when stream in @public_streams do
+ {:ok, stream}
end
- def add_socket(topic, _) do
- if should_env_send?(), do: Registry.register(@registry, topic, false)
+ # Allow all hashtags streams.
+ def get_topic("hashtag", _, %{"tag" => tag}) do
+ {:ok, "hashtag:" <> tag}
+ end
+
+ # Expand user streams.
+ def get_topic(stream, %User{} = user, _) when stream in @user_streams do
+ {:ok, stream <> ":" <> to_string(user.id)}
+ end
+
+ def get_topic(stream, _, _) when stream in @user_streams do
+ {:error, :unauthorized}
+ end
+
+ # List streams.
+ def get_topic("list", %User{} = user, %{"list" => id}) do
+ if Pleroma.List.get(id, user) do
+ {:ok, "list:" <> to_string(id)}
+ else
+ {:error, :bad_topic}
+ end
+ end
+
+ def get_topic("list", _, _) do
+ {:error, :unauthorized}
+ end
+
+ def get_topic(_, _, _) do
+ {:error, :bad_topic}
+ end
+
+ @doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic."
+ def add_socket(topic, user) do
+ if should_env_send?() do
+ auth? = if user, do: true
+ Registry.register(@registry, topic, auth?)
+ end
+
+ {:ok, topic}
end
def remove_socket(topic) do
@@ -231,13 +287,4 @@ defmodule Pleroma.Web.Streamer do
true ->
def should_env_send?, do: true
end
-
- defp user_topic(topic, user)
- when topic in ~w[user user:notification direct] do
- "#{topic}:#{user.id}"
- end
-
- defp user_topic(topic, _) do
- topic
- end
end
diff --git a/lib/pleroma/workers/scheduled_activity_worker.ex b/lib/pleroma/workers/scheduled_activity_worker.ex
index 8905f4ad0..97d1efbfb 100644
--- a/lib/pleroma/workers/scheduled_activity_worker.ex
+++ b/lib/pleroma/workers/scheduled_activity_worker.ex
@@ -30,6 +30,8 @@ defmodule Pleroma.Workers.ScheduledActivityWorker do
end
defp post_activity(%ScheduledActivity{user_id: user_id, params: params} = scheduled_activity) do
+ params = Map.new(params, fn {key, value} -> {String.to_existing_atom(key), value} end)
+
with {:delete, {:ok, _}} <- {:delete, ScheduledActivity.delete(scheduled_activity)},
{:user, %User{} = user} <- {:user, User.get_cached_by_id(user_id)},
{:post, {:ok, _}} <- {:post, CommonAPI.post(user, params)} do