Merge remote-tracking branch 'remotes/origin/develop' into follow-request-notifications

This commit is contained in:
Ivan Tashkinov 2020-05-02 07:39:19 +03:00
commit cf5ca7e45b
65 changed files with 1289 additions and 738 deletions

View file

@ -73,7 +73,6 @@ defmodule Pleroma.Application do
Pleroma.Repo,
Config.TransferTask,
Pleroma.Emoji,
Pleroma.Captcha,
Pleroma.Plugs.RateLimiter.Supervisor
] ++
cachex_children() ++

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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