Merge remote-tracking branch 'remotes/origin/develop' into follow-request-notifications
This commit is contained in:
commit
cf5ca7e45b
65 changed files with 1289 additions and 738 deletions
|
|
@ -73,7 +73,6 @@ defmodule Pleroma.Application do
|
|||
Pleroma.Repo,
|
||||
Config.TransferTask,
|
||||
Pleroma.Emoji,
|
||||
Pleroma.Captcha,
|
||||
Pleroma.Plugs.RateLimiter.Supervisor
|
||||
] ++
|
||||
cachex_children() ++
|
||||
|
|
|
|||
|
|
@ -3,53 +3,22 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Captcha do
|
||||
import Pleroma.Web.Gettext
|
||||
|
||||
alias Calendar.DateTime
|
||||
alias Plug.Crypto.KeyGenerator
|
||||
alias Plug.Crypto.MessageEncryptor
|
||||
|
||||
use GenServer
|
||||
|
||||
@doc false
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def init(_) do
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ask the configured captcha service for a new captcha
|
||||
"""
|
||||
def new do
|
||||
GenServer.call(__MODULE__, :new)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ask the configured captcha service to validate the captcha
|
||||
"""
|
||||
def validate(token, captcha, answer_data) do
|
||||
GenServer.call(__MODULE__, {:validate, token, captcha, answer_data})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call(:new, _from, state) do
|
||||
enabled = Pleroma.Config.get([__MODULE__, :enabled])
|
||||
|
||||
if !enabled do
|
||||
{:reply, %{type: :none}, state}
|
||||
if not enabled?() do
|
||||
%{type: :none}
|
||||
else
|
||||
new_captcha = method().new()
|
||||
|
||||
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
|
||||
|
||||
# This make salt a little different for two keys
|
||||
token = new_captcha[:token]
|
||||
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
|
||||
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
|
||||
{secret, sign_secret} = secret_pair(new_captcha[:token])
|
||||
|
||||
# Basically copy what Phoenix.Token does here, add the time to
|
||||
# the actual data and make it a binary to then encrypt it
|
||||
encrypted_captcha_answer =
|
||||
|
|
@ -60,55 +29,73 @@ defmodule Pleroma.Captcha do
|
|||
|> :erlang.term_to_binary()
|
||||
|> MessageEncryptor.encrypt(secret, sign_secret)
|
||||
|
||||
{
|
||||
:reply,
|
||||
# Replace the answer with the encrypted answer
|
||||
%{new_captcha | answer_data: encrypted_captcha_answer},
|
||||
state
|
||||
}
|
||||
# Replace the answer with the encrypted answer
|
||||
%{new_captcha | answer_data: encrypted_captcha_answer}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call({:validate, token, captcha, answer_data}, _from, state) do
|
||||
@doc """
|
||||
Ask the configured captcha service to validate the captcha
|
||||
"""
|
||||
def validate(token, captcha, answer_data) do
|
||||
with {:ok, %{at: at, answer_data: answer_md5}} <- validate_answer_data(token, answer_data),
|
||||
:ok <- validate_expiration(at),
|
||||
:ok <- validate_usage(token),
|
||||
:ok <- method().validate(token, captcha, answer_md5),
|
||||
{:ok, _} <- mark_captcha_as_used(token) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled], false)
|
||||
|
||||
defp seconds_valid, do: Pleroma.Config.get!([__MODULE__, :seconds_valid])
|
||||
|
||||
defp secret_pair(token) do
|
||||
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
|
||||
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
|
||||
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
|
||||
|
||||
{secret, sign_secret}
|
||||
end
|
||||
|
||||
defp validate_answer_data(token, answer_data) do
|
||||
{secret, sign_secret} = secret_pair(token)
|
||||
|
||||
with false <- is_nil(answer_data),
|
||||
{:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
|
||||
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
|
||||
{:ok, %{at: at, answer_data: answer_md5}}
|
||||
else
|
||||
_ -> {:error, :invalid_answer_data}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_expiration(created_at) do
|
||||
# If the time found is less than (current_time-seconds_valid) then the time has already passed
|
||||
# Later we check that the time found is more than the presumed invalidatation time, that means
|
||||
# that the data is still valid and the captcha can be checked
|
||||
seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid])
|
||||
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid)
|
||||
|
||||
result =
|
||||
with false <- is_nil(answer_data),
|
||||
{:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
|
||||
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
|
||||
try do
|
||||
if DateTime.before?(at, valid_if_after),
|
||||
do: throw({:error, dgettext("errors", "CAPTCHA expired")})
|
||||
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid())
|
||||
|
||||
if not is_nil(Cachex.get!(:used_captcha_cache, token)),
|
||||
do: throw({:error, dgettext("errors", "CAPTCHA already used")})
|
||||
if DateTime.before?(created_at, valid_if_after) do
|
||||
{:error, :expired}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
res = method().validate(token, captcha, answer_md5)
|
||||
# Throw if an error occurs
|
||||
if res != :ok, do: throw(res)
|
||||
defp validate_usage(token) do
|
||||
if is_nil(Cachex.get!(:used_captcha_cache, token)) do
|
||||
:ok
|
||||
else
|
||||
{:error, :already_used}
|
||||
end
|
||||
end
|
||||
|
||||
# Mark this captcha as used
|
||||
{:ok, _} =
|
||||
Cachex.put(:used_captcha_cache, token, true, ttl: :timer.seconds(seconds_valid))
|
||||
|
||||
:ok
|
||||
catch
|
||||
:throw, e -> e
|
||||
end
|
||||
else
|
||||
_ -> {:error, dgettext("errors", "Invalid answer data")}
|
||||
end
|
||||
|
||||
{:reply, result, state}
|
||||
defp mark_captcha_as_used(token) do
|
||||
ttl = seconds_valid() |> :timer.seconds()
|
||||
Cachex.put(:used_captcha_cache, token, true, ttl: ttl)
|
||||
end
|
||||
|
||||
defp method, do: Pleroma.Config.get!([__MODULE__, :method])
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Captcha.Kocaptcha do
|
||||
import Pleroma.Web.Gettext
|
||||
alias Pleroma.Captcha.Service
|
||||
@behaviour Service
|
||||
|
||||
|
|
@ -13,7 +12,7 @@ defmodule Pleroma.Captcha.Kocaptcha do
|
|||
|
||||
case Tesla.get(endpoint <> "/new") do
|
||||
{:error, _} ->
|
||||
%{error: dgettext("errors", "Kocaptcha service unavailable")}
|
||||
%{error: :kocaptcha_service_unavailable}
|
||||
|
||||
{:ok, res} ->
|
||||
json_resp = Jason.decode!(res.body)
|
||||
|
|
@ -33,6 +32,6 @@ defmodule Pleroma.Captcha.Kocaptcha do
|
|||
if not is_nil(captcha) and
|
||||
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
|
||||
do: :ok,
|
||||
else: {:error, dgettext("errors", "Invalid CAPTCHA")}
|
||||
else: {:error, :invalid}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Captcha.Native do
|
||||
import Pleroma.Web.Gettext
|
||||
alias Pleroma.Captcha.Service
|
||||
@behaviour Service
|
||||
|
||||
|
|
@ -11,7 +10,7 @@ defmodule Pleroma.Captcha.Native do
|
|||
def new do
|
||||
case Captcha.get() do
|
||||
:error ->
|
||||
%{error: dgettext("errors", "Captcha error")}
|
||||
%{error: :captcha_error}
|
||||
|
||||
{:ok, answer_data, img_binary} ->
|
||||
%{
|
||||
|
|
@ -25,7 +24,7 @@ defmodule Pleroma.Captcha.Native do
|
|||
|
||||
@impl Service
|
||||
def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok
|
||||
def validate(_token, _captcha, _answer), do: {:error, dgettext("errors", "Invalid CAPTCHA")}
|
||||
def validate(_token, _captcha, _answer), do: {:error, :invalid}
|
||||
|
||||
defp token do
|
||||
10
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
|||
def init(options), do: options
|
||||
|
||||
defp key_id_from_conn(conn) do
|
||||
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn) do
|
||||
Signature.key_id_to_actor_id(key_id)
|
||||
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
|
||||
ap_id
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Signature do
|
|||
alias Pleroma.Keys
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
def key_id_to_actor_id(key_id) do
|
||||
uri =
|
||||
|
|
@ -21,12 +22,23 @@ defmodule Pleroma.Signature do
|
|||
uri
|
||||
end
|
||||
|
||||
URI.to_string(uri)
|
||||
maybe_ap_id = URI.to_string(uri)
|
||||
|
||||
case Types.ObjectID.cast(maybe_ap_id) do
|
||||
{:ok, ap_id} ->
|
||||
{:ok, ap_id}
|
||||
|
||||
_ ->
|
||||
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
|
||||
%{"ap_id" => ap_id} -> {:ok, ap_id}
|
||||
_ -> {:error, maybe_ap_id}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
actor_id <- key_id_to_actor_id(kid),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
|
|
@ -37,7 +49,7 @@ defmodule Pleroma.Signature do
|
|||
|
||||
def refetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
actor_id <- key_id_to_actor_id(kid),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||
{:ok, public_key}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
A module to handle coding from internal to wire ActivityPub and back.
|
||||
"""
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.EarmarkRenderer
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
|
|
@ -43,6 +44,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> fix_addressing
|
||||
|> fix_summary
|
||||
|> fix_type(options)
|
||||
|> fix_content
|
||||
end
|
||||
|
||||
def fix_summary(%{"summary" => nil} = object) do
|
||||
|
|
@ -357,6 +359,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def fix_type(object, _), do: object
|
||||
|
||||
defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
|
||||
when is_binary(content) do
|
||||
html_content =
|
||||
content
|
||||
|> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
|
||||
|> Pleroma.HTML.filter_tags()
|
||||
|
||||
Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
|
||||
end
|
||||
|
||||
defp fix_content(object), do: object
|
||||
|
||||
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
|
||||
with true <- id =~ "follows",
|
||||
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
|
||||
|
|
@ -1207,18 +1221,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def prepare_attachments(object) do
|
||||
attachments =
|
||||
(object["attachment"] || [])
|
||||
object
|
||||
|> Map.get("attachment", [])
|
||||
|> Enum.map(fn data ->
|
||||
[%{"mediaType" => media_type, "href" => href} | _] = data["url"]
|
||||
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
|
||||
|
||||
%{
|
||||
"url" => href,
|
||||
"mediaType" => media_type,
|
||||
"name" => data["name"],
|
||||
"type" => "Document"
|
||||
}
|
||||
end)
|
||||
|
||||
Map.put(object, "attachment", attachments)
|
||||
end
|
||||
|
||||
def strip_internal_fields(object) do
|
||||
object
|
||||
|> Map.drop(Pleroma.Constants.object_internal_fields())
|
||||
Map.drop(object, Pleroma.Constants.object_internal_fields())
|
||||
end
|
||||
|
||||
defp strip_internal_tags(%{"tag" => tags} = object) do
|
||||
|
|
|
|||
|
|
@ -41,9 +41,17 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
|
|||
Operation.parameter(
|
||||
:limit,
|
||||
:query,
|
||||
%Schema{type: :integer, default: 20, maximum: 40},
|
||||
"Limit"
|
||||
%Schema{type: :integer, default: 20},
|
||||
"Maximum number of items to return. Will be ignored if it's more than 40"
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
def empty_object_response do
|
||||
Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}})
|
||||
end
|
||||
|
||||
def empty_array_response do
|
||||
Operation.response("Empty array", "application/json", %Schema{type: :array, example: []})
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
description: "Not implemented",
|
||||
security: [%{"oAuth" => ["read:accounts"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empry array", "application/json", %Schema{type: :array})
|
||||
200 => empty_array_response()
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -356,7 +356,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
operationId: "AccountController.identity_proofs",
|
||||
description: "Not implemented",
|
||||
responses: %{
|
||||
200 => Operation.response("Empry array", "application/json", %Schema{type: :array})
|
||||
200 => empty_array_response()
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
|
|
@ -46,9 +46,7 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
|
|||
operationId: "DomainBlockController.create",
|
||||
requestBody: domain_block_request(),
|
||||
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||
}
|
||||
responses: %{200 => empty_object_response()}
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -67,7 +65,7 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
|
|||
end
|
||||
|
||||
defp domain_block_request do
|
||||
Helpers.request_body(
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
type: :object,
|
||||
|
|
|
|||
211
lib/pleroma/web/api_spec/operations/notification_operation.ex
Normal file
211
lib/pleroma/web/api_spec/operations/notification_operation.ex
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
# 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.NotificationOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
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 index_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Get all notifications",
|
||||
description:
|
||||
"Notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values.",
|
||||
operationId: "NotificationController.index",
|
||||
security: [%{"oAuth" => ["read:notifications"]}],
|
||||
parameters:
|
||||
[
|
||||
Operation.parameter(
|
||||
:exclude_types,
|
||||
:query,
|
||||
%Schema{type: :array, items: notification_type()},
|
||||
"Array of types to exclude"
|
||||
),
|
||||
Operation.parameter(
|
||||
:account_id,
|
||||
:query,
|
||||
%Schema{type: :string},
|
||||
"Return only notifications received from this account"
|
||||
),
|
||||
Operation.parameter(
|
||||
:exclude_visibilities,
|
||||
:query,
|
||||
%Schema{type: :array, items: VisibilityScope},
|
||||
"Exclude the notifications for activities with the given visibilities"
|
||||
),
|
||||
Operation.parameter(
|
||||
:include_types,
|
||||
:query,
|
||||
%Schema{type: :array, items: notification_type()},
|
||||
"Include the notifications for activities with the given types"
|
||||
),
|
||||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike,
|
||||
"Include the notifications from muted users"
|
||||
)
|
||||
] ++ pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of notifications", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: notification()
|
||||
}),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Get a single notification",
|
||||
description: "View information about a notification with a given ID.",
|
||||
operationId: "NotificationController.show",
|
||||
security: [%{"oAuth" => ["read:notifications"]}],
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Notification", "application/json", notification())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def clear_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Dismiss all notifications",
|
||||
description: "Clear all notifications from the server.",
|
||||
operationId: "NotificationController.clear",
|
||||
security: [%{"oAuth" => ["write:notifications"]}],
|
||||
responses: %{200 => empty_object_response()}
|
||||
}
|
||||
end
|
||||
|
||||
def dismiss_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Dismiss a single notification",
|
||||
description: "Clear a single notification from the server.",
|
||||
operationId: "NotificationController.dismiss",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["write:notifications"]}],
|
||||
responses: %{200 => empty_object_response()}
|
||||
}
|
||||
end
|
||||
|
||||
def dismiss_via_body_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Dismiss a single notification",
|
||||
deprecated: true,
|
||||
description: "Clear a single notification from the server.",
|
||||
operationId: "NotificationController.dismiss_via_body",
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{type: :object, properties: %{id: %Schema{type: :string}}},
|
||||
required: true
|
||||
),
|
||||
security: [%{"oAuth" => ["write:notifications"]}],
|
||||
responses: %{200 => empty_object_response()}
|
||||
}
|
||||
end
|
||||
|
||||
def destroy_multiple_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Dismiss multiple notifications",
|
||||
operationId: "NotificationController.destroy_multiple",
|
||||
security: [%{"oAuth" => ["write:notifications"]}],
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:ids,
|
||||
:query,
|
||||
%Schema{type: :array, items: %Schema{type: :string}},
|
||||
"Array of notification IDs to dismiss",
|
||||
required: true
|
||||
)
|
||||
],
|
||||
responses: %{200 => empty_object_response()}
|
||||
}
|
||||
end
|
||||
|
||||
defp notification do
|
||||
%Schema{
|
||||
title: "Notification",
|
||||
description: "Response schema for a notification",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
type: notification_type(),
|
||||
created_at: %Schema{type: :string, format: :"date-time"},
|
||||
account: %Schema{
|
||||
allOf: [Account],
|
||||
description: "The account that performed the action that generated the notification."
|
||||
},
|
||||
status: %Schema{
|
||||
allOf: [Status],
|
||||
description:
|
||||
"Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.",
|
||||
nullable: true
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "34975861",
|
||||
"type" => "mention",
|
||||
"created_at" => "2019-11-23T07:49:02.064Z",
|
||||
"account" => Account.schema().example,
|
||||
"status" => Status.schema().example
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp notification_type do
|
||||
%Schema{
|
||||
type: :string,
|
||||
enum: [
|
||||
"follow",
|
||||
"favourite",
|
||||
"reblog",
|
||||
"mention",
|
||||
"poll",
|
||||
"pleroma:emoji_reaction",
|
||||
"move",
|
||||
"follow_request"
|
||||
],
|
||||
description: """
|
||||
The type of event that resulted in the notification.
|
||||
|
||||
- `follow` - Someone followed you
|
||||
- `mention` - Someone mentioned you in their status
|
||||
- `reblog` - Someone boosted one of your statuses
|
||||
- `favourite` - Someone favourited one of your statuses
|
||||
- `poll` - A poll you have voted in or created has ended
|
||||
- `move` - Someone moved their account
|
||||
- `pleroma:emoji_reaction` - Someone reacted with emoji to your status
|
||||
"""
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, :string, "Notification ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
||||
78
lib/pleroma/web/api_spec/operations/report_operation.ex
Normal file
78
lib/pleroma/web/api_spec/operations/report_operation.ex
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# 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.ReportOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["reports"],
|
||||
summary: "File a report",
|
||||
description: "Report problematic users to your moderators",
|
||||
operationId: "ReportController.create",
|
||||
security: [%{"oAuth" => ["follow", "write:reports"]}],
|
||||
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Report", "application/json", create_response()),
|
||||
400 => Operation.response("Report", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "ReportCreateRequest",
|
||||
description: "POST body for creating a report",
|
||||
type: :object,
|
||||
properties: %{
|
||||
account_id: %Schema{type: :string, description: "ID of the account to report"},
|
||||
status_ids: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Array of Statuses to attach to the report, for context"
|
||||
},
|
||||
comment: %Schema{
|
||||
type: :string,
|
||||
description: "Reason for the report"
|
||||
},
|
||||
forward: %Schema{
|
||||
type: :boolean,
|
||||
default: false,
|
||||
description:
|
||||
"If the account is remote, should the report be forwarded to the remote admin?"
|
||||
}
|
||||
},
|
||||
required: [:account_id],
|
||||
example: %{
|
||||
"account_id" => "123",
|
||||
"status_ids" => ["1337"],
|
||||
"comment" => "bad status!",
|
||||
"forward" => "false"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_response do
|
||||
%Schema{
|
||||
title: "ReportResponse",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "Report ID"},
|
||||
action_taken: %Schema{type: :boolean, description: "Is action taken?"}
|
||||
},
|
||||
example: %{
|
||||
"id" => "123",
|
||||
"action_taken" => false
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -382,9 +382,9 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
ThreadMute.exists?(user.id, activity.data["context"])
|
||||
end
|
||||
|
||||
def report(user, %{"account_id" => account_id} = data) do
|
||||
with {:ok, account} <- get_reported_account(account_id),
|
||||
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
|
||||
def report(user, data) do
|
||||
with {:ok, account} <- get_reported_account(data.account_id),
|
||||
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
|
||||
{:ok, statuses} <- get_report_statuses(account, data) do
|
||||
ActivityPub.flag(%{
|
||||
context: Utils.generate_context_id(),
|
||||
|
|
@ -392,13 +392,11 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
account: account,
|
||||
statuses: statuses,
|
||||
content: content_html,
|
||||
forward: data["forward"] || false
|
||||
forward: Map.get(data, :forward, false)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
|
||||
|
||||
defp get_reported_account(account_id) do
|
||||
case User.get_cached_by_id(account_id) do
|
||||
%User{} = account -> {:ok, account}
|
||||
|
|
|
|||
|
|
@ -504,7 +504,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
end
|
||||
end
|
||||
|
||||
def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
|
||||
def get_report_statuses(%User{ap_id: actor}, %{status_ids: status_ids})
|
||||
when is_list(status_ids) do
|
||||
{:ok, Activity.all_by_actor_and_id(actor, status_ids)}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -94,24 +94,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
@doc "POST /api/v1/accounts"
|
||||
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
||||
params =
|
||||
params
|
||||
|> Map.take([
|
||||
:email,
|
||||
:bio,
|
||||
:captcha_solution,
|
||||
:captcha_token,
|
||||
:captcha_answer_data,
|
||||
:token,
|
||||
:password,
|
||||
:fullname
|
||||
])
|
||||
|> Map.put(:nickname, params.username)
|
||||
|> Map.put(:fullname, Map.get(params, :fullname, params.username))
|
||||
|> Map.put(:confirm, params.password)
|
||||
|> Map.put(:trusted_app, app.trusted)
|
||||
|
||||
with :ok <- validate_email_param(params),
|
||||
:ok <- TwitterAPI.validate_captcha(app, params),
|
||||
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
|
||||
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
|
||||
json(conn, %{
|
||||
|
|
@ -121,7 +105,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
created_at: Token.Utils.format_created_at(token)
|
||||
})
|
||||
else
|
||||
{:error, errors} -> json_response(conn, :bad_request, errors)
|
||||
{:error, error} -> json_response(conn, :bad_request, %{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -133,11 +117,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
render_error(conn, :forbidden, "Invalid credentials")
|
||||
end
|
||||
|
||||
defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
|
||||
defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
|
||||
|
||||
defp validate_email_param(_) do
|
||||
case Pleroma.Config.get([:instance, :account_activation_required]) do
|
||||
true -> {:error, %{"error" => "Missing parameters"}}
|
||||
true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
|||
|
||||
@oauth_read_actions [:show, :index]
|
||||
|
||||
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:notifications"]} when action in @oauth_read_actions
|
||||
|
|
@ -20,14 +22,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
|||
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.NotificationOperation
|
||||
|
||||
# GET /api/v1/notifications
|
||||
def index(conn, %{"account_id" => account_id} = params) do
|
||||
def index(conn, %{account_id: account_id} = params) do
|
||||
case Pleroma.User.get_cached_by_id(account_id) do
|
||||
%{ap_id: account_ap_id} ->
|
||||
params =
|
||||
params
|
||||
|> Map.delete("account_id")
|
||||
|> Map.put("account_ap_id", account_ap_id)
|
||||
|> Map.delete(:account_id)
|
||||
|> Map.put(:account_ap_id, account_ap_id)
|
||||
|
||||
index(conn, params)
|
||||
|
||||
|
|
@ -39,6 +43,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
|||
end
|
||||
|
||||
def index(%{assigns: %{user: user}} = conn, params) do
|
||||
params = Map.new(params, fn {k, v} -> {to_string(k), v} end)
|
||||
notifications = MastodonAPI.get_notifications(user, params)
|
||||
|
||||
conn
|
||||
|
|
@ -51,7 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
|||
end
|
||||
|
||||
# GET /api/v1/notifications/:id
|
||||
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||
with {:ok, notification} <- Notification.get(user, id) do
|
||||
render(conn, "show.json", notification: notification, for: user)
|
||||
else
|
||||
|
|
@ -69,8 +74,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
|||
end
|
||||
|
||||
# POST /api/v1/notifications/:id/dismiss
|
||||
# POST /api/v1/notifications/dismiss (deprecated)
|
||||
def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
|
||||
|
||||
def dismiss(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
|
||||
with {:ok, _notif} <- Notification.dismiss(user, id) do
|
||||
json(conn, %{})
|
||||
else
|
||||
|
|
@ -81,8 +86,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
|||
end
|
||||
end
|
||||
|
||||
# POST /api/v1/notifications/dismiss (deprecated)
|
||||
def dismiss_via_body(%{body_params: params} = conn, _) do
|
||||
dismiss(conn, params)
|
||||
end
|
||||
|
||||
# DELETE /api/v1/notifications/destroy_multiple
|
||||
def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
|
||||
def destroy_multiple(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
|
||||
Notification.destroy_multiple(user, ids)
|
||||
json(conn, %{})
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
|
|||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
|
||||
|
||||
@doc "POST /api/v1/reports"
|
||||
def create(%{assigns: %{user: user}} = conn, params) do
|
||||
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
|
||||
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
|
||||
render(conn, "show.json", activity: activity)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -396,7 +396,7 @@ defmodule Pleroma.Web.Router do
|
|||
post("/notifications/clear", NotificationController, :clear)
|
||||
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
|
||||
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
|
||||
post("/notifications/dismiss", NotificationController, :dismiss)
|
||||
post("/notifications/dismiss", NotificationController, :dismiss_via_body)
|
||||
|
||||
post("/polls/:id/votes", PollController, :vote)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,54 +3,27 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|
||||
import Pleroma.Web.Gettext
|
||||
|
||||
alias Pleroma.Emails.Mailer
|
||||
alias Pleroma.Emails.UserEmail
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserInviteToken
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
def register_user(params, opts \\ []) do
|
||||
params =
|
||||
params
|
||||
|> Map.take([
|
||||
:nickname,
|
||||
:password,
|
||||
:captcha_solution,
|
||||
:captcha_token,
|
||||
:captcha_answer_data,
|
||||
:token,
|
||||
:email,
|
||||
:trusted_app
|
||||
])
|
||||
|> Map.put(:bio, User.parse_bio(params[:bio] || ""))
|
||||
|> Map.put(:name, params.fullname)
|
||||
|> Map.put(:password_confirmation, params[:confirm])
|
||||
|> Map.take([:email, :token, :password])
|
||||
|> Map.put(:bio, params |> Map.get(:bio, "") |> User.parse_bio())
|
||||
|> Map.put(:nickname, params[:username])
|
||||
|> Map.put(:name, Map.get(params, :fullname, params[:username]))
|
||||
|> Map.put(:password_confirmation, params[:password])
|
||||
|
||||
case validate_captcha(params) do
|
||||
:ok ->
|
||||
if Pleroma.Config.get([:instance, :registrations_open]) do
|
||||
create_user(params, opts)
|
||||
else
|
||||
create_user_with_invite(params, opts)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
# I have no idea how this error handling works
|
||||
{:error, %{error: Jason.encode!(%{captcha: [error]})}}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_captcha(params) do
|
||||
if params[:trusted_app] || not Pleroma.Config.get([Pleroma.Captcha, :enabled]) do
|
||||
:ok
|
||||
if Pleroma.Config.get([:instance, :registrations_open]) do
|
||||
create_user(params, opts)
|
||||
else
|
||||
Pleroma.Captcha.validate(
|
||||
params.captcha_token,
|
||||
params.captcha_solution,
|
||||
params.captcha_answer_data
|
||||
)
|
||||
create_user_with_invite(params, opts)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -75,16 +48,17 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|
|||
|
||||
{:error, changeset} ->
|
||||
errors =
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|
||||
changeset
|
||||
|> Ecto.Changeset.traverse_errors(fn {msg, _opts} -> msg end)
|
||||
|> Jason.encode!()
|
||||
|
||||
{:error, %{error: errors}}
|
||||
{:error, errors}
|
||||
end
|
||||
end
|
||||
|
||||
def password_reset(nickname_or_email) do
|
||||
with true <- is_binary(nickname_or_email),
|
||||
%User{local: true, email: email} = user when not is_nil(email) <-
|
||||
%User{local: true, email: email} = user when is_binary(email) <-
|
||||
User.get_by_nickname_or_email(nickname_or_email),
|
||||
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
|
||||
user
|
||||
|
|
@ -106,4 +80,58 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|
|||
{:error, "unknown user"}
|
||||
end
|
||||
end
|
||||
|
||||
def validate_captcha(app, params) do
|
||||
if app.trusted || not Pleroma.Captcha.enabled?() do
|
||||
:ok
|
||||
else
|
||||
do_validate_captcha(params)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_validate_captcha(params) do
|
||||
with :ok <- validate_captcha_presence(params),
|
||||
:ok <-
|
||||
Pleroma.Captcha.validate(
|
||||
params[:captcha_token],
|
||||
params[:captcha_solution],
|
||||
params[:captcha_answer_data]
|
||||
) do
|
||||
:ok
|
||||
else
|
||||
{:error, :captcha_error} ->
|
||||
captcha_error(dgettext("errors", "CAPTCHA Error"))
|
||||
|
||||
{:error, :invalid} ->
|
||||
captcha_error(dgettext("errors", "Invalid CAPTCHA"))
|
||||
|
||||
{:error, :kocaptcha_service_unavailable} ->
|
||||
captcha_error(dgettext("errors", "Kocaptcha service unavailable"))
|
||||
|
||||
{:error, :expired} ->
|
||||
captcha_error(dgettext("errors", "CAPTCHA expired"))
|
||||
|
||||
{:error, :already_used} ->
|
||||
captcha_error(dgettext("errors", "CAPTCHA already used"))
|
||||
|
||||
{:error, :invalid_answer_data} ->
|
||||
captcha_error(dgettext("errors", "Invalid answer data"))
|
||||
|
||||
{:error, error} ->
|
||||
captcha_error(error)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_captcha_presence(params) do
|
||||
[:captcha_solution, :captcha_token, :captcha_answer_data]
|
||||
|> Enum.find_value(:ok, fn key ->
|
||||
unless is_binary(params[key]) do
|
||||
error = dgettext("errors", "Invalid CAPTCHA (Missing parameter: %{name})", name: key)
|
||||
{:error, error}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# For some reason FE expects error message to be a serialized JSON
|
||||
defp captcha_error(error), do: {:error, Jason.encode!(%{captcha: [error]})}
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue