Merge branch 'develop' into issue/1276-2

This commit is contained in:
Maksim Pechnikov 2020-05-04 06:08:49 +03:00
commit fe7a0d660e
87 changed files with 1383 additions and 474 deletions

View File

@ -15,15 +15,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking:** removed `with_move` parameter from notifications timeline. - **Breaking:** removed `with_move` parameter from notifications timeline.
### Added ### Added
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list. - NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
- NodeInfo: `pleroma_emoji_reactions` to the `features` list. - NodeInfo: `pleroma_emoji_reactions` to the `features` list.
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses. - Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required. - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required.
- Mix task to create trusted OAuth App. - Mix task to create trusted OAuth App.
- Notifications: Added `follow_request` notification type (configurable, see `[:notifications, :enable_follow_request_notifications]` setting). - Notifications: Added `follow_request` notification type.
- Added `:reject_deletes` group to SimplePolicy - Added `:reject_deletes` group to SimplePolicy
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
- Mastodon API: Extended `/api/v1/instance`.
- Mastodon API: Support for `include_types` in `/api/v1/notifications`. - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint. - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Mastodon API: Add support for filtering replies in public and home timelines - Mastodon API: Add support for filtering replies in public and home timelines
@ -37,6 +39,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Filtering of push notifications on activities from blocked domains - Filtering of push notifications on activities from blocked domains
## [unreleased-patch] ## [unreleased-patch]
### Security
- Disallow re-registration of previously deleted users, which allowed viewing direct messages addressed to them
- Mastodon API: Fix `POST /api/v1/follow_requests/:id/authorize` allowing to force a follow from a local user even if they didn't request to follow
### Fixed ### Fixed
- Logger configuration through AdminFE - Logger configuration through AdminFE
- HTTP Basic Authentication permissions issue - HTTP Basic Authentication permissions issue

View File

@ -562,8 +562,6 @@
inactivity_threshold: 7 inactivity_threshold: 7
} }
config :pleroma, :notifications, enable_follow_request_notifications: false
config :pleroma, :oauth2, config :pleroma, :oauth2,
token_expires_in: 600, token_expires_in: 600,
issue_new_refresh_token: true, issue_new_refresh_token: true,

View File

@ -2273,20 +2273,6 @@
} }
] ]
}, },
%{
group: :pleroma,
key: :notifications,
type: :group,
description: "Notification settings",
children: [
%{
key: :enable_follow_request_notifications,
type: :boolean,
description:
"Enables notifications on new follow requests (causes issues with older PleromaFE versions)."
}
]
},
%{ %{
group: :pleroma, group: :pleroma,
key: Pleroma.Emails.UserEmail, key: Pleroma.Emails.UserEmail,

View File

@ -202,8 +202,22 @@ Has theses additional parameters (which are the same as in Pleroma-API):
- `bio`: optional - `bio`: optional
- `captcha_solution`: optional, contains provider-specific captcha solution, - `captcha_solution`: optional, contains provider-specific captcha solution,
- `captcha_token`: optional, contains provider-specific captcha token - `captcha_token`: optional, contains provider-specific captcha token
- `captcha_answer_data`: optional, contains provider-specific captcha data
- `token`: invite token required when the registrations aren't public. - `token`: invite token required when the registrations aren't public.
## Instance
`GET /api/v1/instance` has additional fields
- `max_toot_chars`: The maximum characters per post
- `poll_limits`: The limits of polls
- `upload_limit`: The maximum upload file size
- `avatar_upload_limit`: The same for avatars
- `background_upload_limit`: The same for backgrounds
- `banner_upload_limit`: The same for banners
- `pleroma.metadata.features`: A list of supported features
- `pleroma.metadata.federation`: The federation restrictions of this instance
- `vapid_public_key`: The public key needed for push messages
## Markers ## Markers

View File

@ -49,11 +49,11 @@ Feel free to contact us to be added to this list!
- Platforms: Android - Platforms: Android
- Features: Streaming Ready - Features: Streaming Ready
### Roma ### Fedi
- Homepage: <https://www.pleroma.com/#mobileApps> - Homepage: <https://www.fediapp.com/>
- Source Code: [iOS](https://github.com/roma-apps/roma-ios), [Android](https://github.com/roma-apps/roma-android) - Source Code: Proprietary, but free
- Platforms: iOS, Android - Platforms: iOS, Android
- Features: No Streaming - Features: Pleroma-specific features like Reactions
### Tusky ### Tusky
- Homepage: <https://tuskyapp.github.io/> - Homepage: <https://tuskyapp.github.io/>

View File

@ -73,7 +73,6 @@ def start(_type, _args) do
Pleroma.Repo, Pleroma.Repo,
Config.TransferTask, Config.TransferTask,
Pleroma.Emoji, Pleroma.Emoji,
Pleroma.Captcha,
Pleroma.Plugs.RateLimiter.Supervisor Pleroma.Plugs.RateLimiter.Supervisor
] ++ ] ++
cachex_children() ++ cachex_children() ++

View File

@ -3,53 +3,22 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha do defmodule Pleroma.Captcha do
import Pleroma.Web.Gettext
alias Calendar.DateTime alias Calendar.DateTime
alias Plug.Crypto.KeyGenerator alias Plug.Crypto.KeyGenerator
alias Plug.Crypto.MessageEncryptor 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 """ @doc """
Ask the configured captcha service for a new captcha Ask the configured captcha service for a new captcha
""" """
def new do def new do
GenServer.call(__MODULE__, :new) if not enabled?() do
end %{type: :none}
@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}
else else
new_captcha = method().new() 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 # This make salt a little different for two keys
token = new_captcha[:token] {secret, sign_secret} = secret_pair(new_captcha[:token])
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
# Basically copy what Phoenix.Token does here, add the time to # Basically copy what Phoenix.Token does here, add the time to
# the actual data and make it a binary to then encrypt it # the actual data and make it a binary to then encrypt it
encrypted_captcha_answer = encrypted_captcha_answer =
@ -60,55 +29,73 @@ def handle_call(:new, _from, state) do
|> :erlang.term_to_binary() |> :erlang.term_to_binary()
|> MessageEncryptor.encrypt(secret, sign_secret) |> MessageEncryptor.encrypt(secret, sign_secret)
{
:reply,
# Replace the answer with the encrypted answer # Replace the answer with the encrypted answer
%{new_captcha | answer_data: encrypted_captcha_answer}, %{new_captcha | answer_data: encrypted_captcha_answer}
state
}
end end
end end
@doc false @doc """
def handle_call({:validate, token, captcha, answer_data}, _from, state) do 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_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
# If the time found is less than (current_time-seconds_valid) then the time has already passed {secret, sign_secret}
# Later we check that the time found is more than the presumed invalidatation time, that means end
# that the data is still valid and the captcha can be checked
seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]) defp validate_answer_data(token, answer_data) do
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid) {secret, sign_secret} = secret_pair(token)
result =
with false <- is_nil(answer_data), with false <- is_nil(answer_data),
{:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret), {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do %{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
try do {:ok, %{at: at, answer_data: answer_md5}}
if DateTime.before?(at, valid_if_after),
do: throw({:error, dgettext("errors", "CAPTCHA expired")})
if not is_nil(Cachex.get!(:used_captcha_cache, token)),
do: throw({:error, dgettext("errors", "CAPTCHA already used")})
res = method().validate(token, captcha, answer_md5)
# Throw if an error occurs
if res != :ok, do: throw(res)
# 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 else
_ -> {:error, dgettext("errors", "Invalid answer data")} _ -> {:error, :invalid_answer_data}
end
end end
{:reply, result, state} 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
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid())
if DateTime.before?(created_at, valid_if_after) do
{:error, :expired}
else
:ok
end
end
defp validate_usage(token) do
if is_nil(Cachex.get!(:used_captcha_cache, token)) do
:ok
else
{:error, :already_used}
end
end
defp mark_captcha_as_used(token) do
ttl = seconds_valid() |> :timer.seconds()
Cachex.put(:used_captcha_cache, token, true, ttl: ttl)
end end
defp method, do: Pleroma.Config.get!([__MODULE__, :method]) defp method, do: Pleroma.Config.get!([__MODULE__, :method])

View File

@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Kocaptcha do defmodule Pleroma.Captcha.Kocaptcha do
import Pleroma.Web.Gettext
alias Pleroma.Captcha.Service alias Pleroma.Captcha.Service
@behaviour Service @behaviour Service
@ -13,7 +12,7 @@ def new do
case Tesla.get(endpoint <> "/new") do case Tesla.get(endpoint <> "/new") do
{:error, _} -> {:error, _} ->
%{error: dgettext("errors", "Kocaptcha service unavailable")} %{error: :kocaptcha_service_unavailable}
{:ok, res} -> {:ok, res} ->
json_resp = Jason.decode!(res.body) json_resp = Jason.decode!(res.body)
@ -33,6 +32,6 @@ def validate(_token, captcha, answer_data) do
if not is_nil(captcha) and if not is_nil(captcha) and
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data), :crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
do: :ok, do: :ok,
else: {:error, dgettext("errors", "Invalid CAPTCHA")} else: {:error, :invalid}
end end
end end

View File

@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Native do defmodule Pleroma.Captcha.Native do
import Pleroma.Web.Gettext
alias Pleroma.Captcha.Service alias Pleroma.Captcha.Service
@behaviour Service @behaviour Service
@ -11,7 +10,7 @@ defmodule Pleroma.Captcha.Native do
def new do def new do
case Captcha.get() do case Captcha.get() do
:error -> :error ->
%{error: dgettext("errors", "Captcha error")} %{error: :captcha_error}
{:ok, answer_data, img_binary} -> {:ok, answer_data, img_binary} ->
%{ %{
@ -25,7 +24,7 @@ def new do
@impl Service @impl Service
def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok 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 defp token do
10 10

View File

@ -20,4 +20,9 @@ defmodule Pleroma.Constants do
"deleted_activity_id" "deleted_activity_id"
] ]
) )
const(static_only_files,
do:
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
)
end end

View File

@ -311,17 +311,8 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
end end
end end
def create_notifications(%Activity{data: %{"type" => "Follow"}} = activity) do
if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
Activity.follow_accepted?(activity) do
do_create_notifications(activity)
else
{:ok, []}
end
end
def create_notifications(%Activity{data: %{"type" => type}} = activity) def create_notifications(%Activity{data: %{"type" => type}} = activity)
when type in ["Like", "Announce", "Move", "EmojiReact"] do when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
do_create_notifications(activity) do_create_notifications(activity)
end end

View File

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.InstanceStatic do defmodule Pleroma.Plugs.InstanceStatic do
require Pleroma.Constants
@moduledoc """ @moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration. This is a shim to call `Plug.Static` but with runtime `from` configuration.
@ -21,9 +23,6 @@ def file_path(path) do
end end
end end
@only ~w(index.html robots.txt static emoji packs sounds images instance favicon.png sw.js
sw-pleroma.js)
def init(opts) do def init(opts) do
opts opts
|> Keyword.put(:from, "__unconfigured_instance_static_plug") |> Keyword.put(:from, "__unconfigured_instance_static_plug")
@ -31,7 +30,7 @@ def init(opts) do
|> Plug.Static.init() |> Plug.Static.init()
end end
for only <- @only do for only <- Pleroma.Constants.static_only_files() do
at = Plug.Router.Utils.split("/") at = Plug.Router.Utils.split("/")
def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do

View File

@ -13,8 +13,9 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
def init(options), do: options def init(options), do: options
defp key_id_from_conn(conn) do defp key_id_from_conn(conn) do
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn) do with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
Signature.key_id_to_actor_id(key_id) {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
ap_id
else else
_ -> _ ->
nil nil

View File

@ -8,6 +8,7 @@ defmodule Pleroma.Signature do
alias Pleroma.Keys alias Pleroma.Keys
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
def key_id_to_actor_id(key_id) do def key_id_to_actor_id(key_id) do
uri = uri =
@ -21,12 +22,23 @@ def key_id_to_actor_id(key_id) do
uri uri
end 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 end
def fetch_public_key(conn) do def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), 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} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key} {:ok, public_key}
else else
@ -37,7 +49,7 @@ def fetch_public_key(conn) do
def refetch_public_key(conn) do def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), 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, _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} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key} {:ok, public_key}

View File

@ -1445,9 +1445,16 @@ def perform(:delete, %User{} = user) do
end) end)
delete_user_activities(user) delete_user_activities(user)
if user.local do
user
|> change(%{deactivated: true, email: nil})
|> update_and_set_cache()
else
invalidate_cache(user) invalidate_cache(user)
Repo.delete(user) Repo.delete(user)
end end
end
def perform(:deactivate_async, user, status), do: deactivate(user, status) def perform(:deactivate_async, user, status), do: deactivate(user, status)

View File

@ -37,9 +37,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
[unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions [unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions
) )
# Note: :following and :followers must be served even without authentication (as via :api)
plug( plug(
EnsureAuthenticatedPlug EnsureAuthenticatedPlug
when action in [:read_inbox, :update_outbox, :whoami, :upload_media, :following, :followers] when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
) )
plug( plug(

View File

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
A module to handle coding from internal to wire ActivityPub and back. A module to handle coding from internal to wire ActivityPub and back.
""" """
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.EarmarkRenderer
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
@ -43,6 +44,7 @@ def fix_object(object, options \\ []) do
|> fix_addressing |> fix_addressing
|> fix_summary |> fix_summary
|> fix_type(options) |> fix_type(options)
|> fix_content
end end
def fix_summary(%{"summary" => nil} = object) do def fix_summary(%{"summary" => nil} = object) do
@ -357,6 +359,18 @@ def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
def fix_type(object, _), do: object 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 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows", with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
@ -1207,18 +1221,24 @@ def add_attributed_to(object) do
def prepare_attachments(object) do def prepare_attachments(object) do
attachments = attachments =
(object["attachment"] || []) object
|> Map.get("attachment", [])
|> Enum.map(fn data -> |> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} | _] = data["url"] [%{"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) end)
Map.put(object, "attachment", attachments) Map.put(object, "attachment", attachments)
end end
def strip_internal_fields(object) do def strip_internal_fields(object) do
object Map.drop(object, Pleroma.Constants.object_internal_fields())
|> Map.drop(Pleroma.Constants.object_internal_fields())
end end
defp strip_internal_tags(%{"tag" => tags} = object) do defp strip_internal_tags(%{"tag" => tags} = object) do

View File

@ -41,9 +41,17 @@ def pagination_params do
Operation.parameter( Operation.parameter(
:limit, :limit,
:query, :query,
%Schema{type: :integer, default: 20, maximum: 40}, %Schema{type: :integer, default: 20},
"Limit" "Maximum number of items to return. Will be ignored if it's more than 40"
) )
] ]
end 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 end

View File

@ -344,7 +344,7 @@ def endorsements_operation do
description: "Not implemented", description: "Not implemented",
security: [%{"oAuth" => ["read:accounts"]}], security: [%{"oAuth" => ["read:accounts"]}],
responses: %{ responses: %{
200 => Operation.response("Empry array", "application/json", %Schema{type: :array}) 200 => empty_array_response()
} }
} }
end end
@ -356,7 +356,7 @@ def identity_proofs_operation do
operationId: "AccountController.identity_proofs", operationId: "AccountController.identity_proofs",
description: "Not implemented", description: "Not implemented",
responses: %{ responses: %{
200 => Operation.response("Empry array", "application/json", %Schema{type: :array}) 200 => empty_array_response()
} }
} }
end end

View File

@ -5,7 +5,7 @@
defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias OpenApiSpex.Schema alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation") operation = String.to_existing_atom("#{action}_operation")
@ -46,9 +46,7 @@ def create_operation do
operationId: "DomainBlockController.create", operationId: "DomainBlockController.create",
requestBody: domain_block_request(), requestBody: domain_block_request(),
security: [%{"oAuth" => ["follow", "write:blocks"]}], security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{ responses: %{200 => empty_object_response()}
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
}
} }
end end
@ -67,7 +65,7 @@ def delete_operation do
end end
defp domain_block_request do defp domain_block_request do
Helpers.request_body( request_body(
"Parameters", "Parameters",
%Schema{ %Schema{
type: :object, 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

@ -43,8 +43,8 @@ def unfollow(follower, unfollowed) do
end end
def accept_follow_request(follower, followed) do def accept_follow_request(follower, followed) do
with {:ok, follower} <- User.follow(follower, followed), with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follower} <- User.follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept), {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
{:ok, _activity} <- {:ok, _activity} <-
@ -382,9 +382,9 @@ def thread_muted?(user, activity) do
ThreadMute.exists?(user.id, activity.data["context"]) ThreadMute.exists?(user.id, activity.data["context"])
end end
def report(user, %{"account_id" => account_id} = data) do def report(user, data) do
with {:ok, account} <- get_reported_account(account_id), with {:ok, account} <- get_reported_account(data.account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]), {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
{:ok, statuses} <- get_report_statuses(account, data) do {:ok, statuses} <- get_report_statuses(account, data) do
ActivityPub.flag(%{ ActivityPub.flag(%{
context: Utils.generate_context_id(), context: Utils.generate_context_id(),
@ -392,13 +392,11 @@ def report(user, %{"account_id" => account_id} = data) do
account: account, account: account,
statuses: statuses, statuses: statuses,
content: content_html, content: content_html,
forward: data["forward"] || false forward: Map.get(data, :forward, false)
}) })
end end
end end
def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
defp get_reported_account(account_id) do defp get_reported_account(account_id) do
case User.get_cached_by_id(account_id) do case User.get_cached_by_id(account_id) do
%User{} = account -> {:ok, account} %User{} = account -> {:ok, account}

View File

@ -504,7 +504,8 @@ def make_report_content_html(comment) do
end end
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)} {:ok, Activity.all_by_actor_and_id(actor, status_ids)}
end end

View File

@ -5,6 +5,8 @@
defmodule Pleroma.Web.Endpoint do defmodule Pleroma.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :pleroma use Phoenix.Endpoint, otp_app: :pleroma
require Pleroma.Constants
socket("/socket", Pleroma.Web.UserSocket) socket("/socket", Pleroma.Web.UserSocket)
plug(Pleroma.Plugs.SetLocalePlug) plug(Pleroma.Plugs.SetLocalePlug)
@ -34,8 +36,7 @@ defmodule Pleroma.Web.Endpoint do
Plug.Static, Plug.Static,
at: "/", at: "/",
from: :pleroma, from: :pleroma,
only: only: Pleroma.Constants.static_only_files(),
~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc),
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
gzip: true, gzip: true,
cache_control_for_etags: @static_cache_control, cache_control_for_etags: @static_cache_control,

View File

@ -94,24 +94,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts" @doc "POST /api/v1/accounts"
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do 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), with :ok <- validate_email_param(params),
:ok <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
json(conn, %{ json(conn, %{
@ -121,7 +105,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
created_at: Token.Utils.format_created_at(token) created_at: Token.Utils.format_created_at(token)
}) })
else else
{:error, errors} -> json_response(conn, :bad_request, errors) {:error, error} -> json_response(conn, :bad_request, %{error: error})
end end
end end
@ -133,11 +117,11 @@ def create(conn, _) do
render_error(conn, :forbidden, "Invalid credentials") render_error(conn, :forbidden, "Invalid credentials")
end 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 defp validate_email_param(_) do
case Pleroma.Config.get([:instance, :account_activation_required]) 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 _ -> :ok
end end
end end

View File

@ -13,6 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
@oauth_read_actions [:show, :index] @oauth_read_actions [:show, :index]
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:notifications"]} when action in @oauth_read_actions %{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) 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 # 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 case Pleroma.User.get_cached_by_id(account_id) do
%{ap_id: account_ap_id} -> %{ap_id: account_ap_id} ->
params = params =
params params
|> Map.delete("account_id") |> Map.delete(:account_id)
|> Map.put("account_ap_id", account_ap_id) |> Map.put(:account_ap_id, account_ap_id)
index(conn, params) index(conn, params)
@ -39,6 +43,7 @@ def index(conn, %{"account_id" => account_id} = params) do
end end
def index(%{assigns: %{user: user}} = conn, params) do 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) notifications = MastodonAPI.get_notifications(user, params)
conn conn
@ -51,7 +56,7 @@ def index(%{assigns: %{user: user}} = conn, params) do
end end
# GET /api/v1/notifications/:id # 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 with {:ok, notification} <- Notification.get(user, id) do
render(conn, "show.json", notification: notification, for: user) render(conn, "show.json", notification: notification, for: user)
else else
@ -69,8 +74,8 @@ def clear(%{assigns: %{user: user}} = conn, _params) do
end end
# POST /api/v1/notifications/:id/dismiss # 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 with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{}) json(conn, %{})
else else
@ -81,8 +86,13 @@ def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
end end
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 # 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) Notification.destroy_multiple(user, ids)
json(conn, %{}) json(conn, %{})
end end

View File

@ -9,10 +9,13 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create) plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
@doc "POST /api/v1/reports" @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 with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
render(conn, "show.json", activity: activity) render(conn, "show.json", activity: activity)
end end

View File

@ -5,10 +5,13 @@
defmodule Pleroma.Web.MastodonAPI.InstanceView do defmodule Pleroma.Web.MastodonAPI.InstanceView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
@mastodon_api_level "2.7.2" @mastodon_api_level "2.7.2"
def render("show.json", _) do def render("show.json", _) do
instance = Pleroma.Config.get(:instance) instance = Config.get(:instance)
%{ %{
uri: Pleroma.Web.base_url(), uri: Pleroma.Web.base_url(),
@ -29,7 +32,58 @@ def render("show.json", _) do
upload_limit: Keyword.get(instance, :upload_limit), upload_limit: Keyword.get(instance, :upload_limit),
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit), avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
background_upload_limit: Keyword.get(instance, :background_upload_limit), background_upload_limit: Keyword.get(instance, :background_upload_limit),
banner_upload_limit: Keyword.get(instance, :banner_upload_limit) banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
pleroma: %{
metadata: %{
features: features(),
federation: federation()
},
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
}
} }
end end
def features do
[
"pleroma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
if Config.get([:media_proxy, :enabled]) do
"media_proxy"
end,
if Config.get([:gopher, :enabled]) do
"gopher"
end,
if Config.get([:chat, :enabled]) do
"chat"
end,
if Config.get([:instance, :allow_relay]) do
"relay"
end,
if Config.get([:instance, :safe_dm_mentions]) do
"safe_dm_mentions"
end,
"pleroma_emoji_reactions"
]
|> Enum.filter(& &1)
end
def federation do
quarantined = Config.get([:instance, :quarantined_instances], [])
if Config.get([:instance, :mrf_transparency]) do
{:ok, data} = MRF.describe()
data
|> Map.merge(%{quarantined_instances: quarantined})
else
%{}
end
|> Map.put(:enabled, Config.get([:instance, :federating]))
end
end end

View File

@ -9,8 +9,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.Stats alias Pleroma.Stats
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.MastodonAPI.InstanceView
def schemas(conn, _params) do def schemas(conn, _params) do
response = %{ response = %{
@ -34,51 +34,12 @@ def schemas(conn, _params) do
def raw_nodeinfo do def raw_nodeinfo do
stats = Stats.get_stats() stats = Stats.get_stats()
quarantined = Config.get([:instance, :quarantined_instances], [])
staff_accounts = staff_accounts =
User.all_superusers() User.all_superusers()
|> Enum.map(fn u -> u.ap_id end) |> Enum.map(fn u -> u.ap_id end)
federation_response = features = InstanceView.features()
if Config.get([:instance, :mrf_transparency]) do federation = InstanceView.federation()
{:ok, data} = MRF.describe()
data
|> Map.merge(%{quarantined_instances: quarantined})
else
%{}
end
|> Map.put(:enabled, Config.get([:instance, :federating]))
features =
[
"pleroma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
if Config.get([:media_proxy, :enabled]) do
"media_proxy"
end,
if Config.get([:gopher, :enabled]) do
"gopher"
end,
if Config.get([:chat, :enabled]) do
"chat"
end,
if Config.get([:instance, :allow_relay]) do
"relay"
end,
if Config.get([:instance, :safe_dm_mentions]) do
"safe_dm_mentions"
end,
"pleroma_emoji_reactions"
]
|> Enum.filter(& &1)
%{ %{
version: "2.0", version: "2.0",
@ -106,7 +67,7 @@ def raw_nodeinfo do
enabled: false enabled: false
}, },
staffAccounts: staff_accounts, staffAccounts: staff_accounts,
federation: federation_response, federation: federation,
pollLimits: Config.get([:instance, :poll_limits]), pollLimits: Config.get([:instance, :poll_limits]),
postFormats: Config.get([:instance, :allowed_post_formats]), postFormats: Config.get([:instance, :allowed_post_formats]),
uploadLimits: %{ uploadLimits: %{

View File

@ -61,7 +61,10 @@ def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}
else else
users = users =
Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1) Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1)
|> Enum.filter(& &1) |> Enum.filter(fn
%{deactivated: false} -> true
_ -> false
end)
%{ %{
name: emoji, name: emoji,

View File

@ -396,7 +396,7 @@ defmodule Pleroma.Web.Router do
post("/notifications/clear", NotificationController, :clear) post("/notifications/clear", NotificationController, :clear)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple) delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead # 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) post("/polls/:id/votes", PollController, :vote)
@ -585,6 +585,7 @@ defmodule Pleroma.Web.Router do
post("/users/:nickname/outbox", ActivityPubController, :update_outbox) post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media) post("/api/ap/upload_media", ActivityPubController, :upload_media)
# The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
get("/users/:nickname/followers", ActivityPubController, :followers) get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following) get("/users/:nickname/following", ActivityPubController, :following)
end end

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" /> <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
<title><%= Pleroma.Config.get([:instance, :name]) %></title> <title><%= Pleroma.Config.get([:instance, :name]) %></title>
<%= Phoenix.HTML.raw(assigns[:meta] || "") %> <%= Phoenix.HTML.raw(assigns[:meta] || "") %>
<link rel="stylesheet" href="/static/static-fe.css"> <link rel="stylesheet" href="/static-fe/static-fe.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">

View File

@ -3,55 +3,28 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
import Pleroma.Web.Gettext
alias Pleroma.Emails.Mailer alias Pleroma.Emails.Mailer
alias Pleroma.Emails.UserEmail alias Pleroma.Emails.UserEmail
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
require Pleroma.Constants
def register_user(params, opts \\ []) do def register_user(params, opts \\ []) do
params = params =
params params
|> Map.take([ |> Map.take([:email, :token, :password])
:nickname, |> Map.put(:bio, params |> Map.get(:bio, "") |> User.parse_bio())
:password, |> Map.put(:nickname, params[:username])
:captcha_solution, |> Map.put(:name, Map.get(params, :fullname, params[:username]))
:captcha_token, |> Map.put(:password_confirmation, params[:password])
: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])
case validate_captcha(params) do
:ok ->
if Pleroma.Config.get([:instance, :registrations_open]) do if Pleroma.Config.get([:instance, :registrations_open]) do
create_user(params, opts) create_user(params, opts)
else else
create_user_with_invite(params, opts) create_user_with_invite(params, opts)
end 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
else
Pleroma.Captcha.validate(
params.captcha_token,
params.captcha_solution,
params.captcha_answer_data
)
end
end end
defp create_user_with_invite(params, opts) do defp create_user_with_invite(params, opts) do
@ -75,16 +48,17 @@ defp create_user(params, opts) do
{:error, changeset} -> {:error, changeset} ->
errors = errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) changeset
|> Ecto.Changeset.traverse_errors(fn {msg, _opts} -> msg end)
|> Jason.encode!() |> Jason.encode!()
{:error, %{error: errors}} {:error, errors}
end end
end end
def password_reset(nickname_or_email) do def password_reset(nickname_or_email) do
with true <- is_binary(nickname_or_email), 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), User.get_by_nickname_or_email(nickname_or_email),
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do {:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user user
@ -106,4 +80,58 @@ def password_reset(nickname_or_email) do
{:error, "unknown user"} {:error, "unknown user"}
end end
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 end

View File

@ -0,0 +1,45 @@
defmodule Pleroma.Repo.Migrations.InsertSkeletonsForDeletedUsers do
use Ecto.Migration
alias Pleroma.User
alias Pleroma.Repo
import Ecto.Query
def change do
Application.ensure_all_started(:flake_id)
local_ap_id =
User.Query.build(%{local: true})
|> select([u], u.ap_id)
|> limit(1)
|> Repo.one()
unless local_ap_id == nil do
# Hack to get instance base url because getting it from Phoenix
# would require starting the whole application
instance_uri =
local_ap_id
|> URI.parse()
|> Map.put(:query, nil)
|> Map.put(:path, nil)
|> URI.to_string()
{:ok, %{rows: ap_ids}} =
Ecto.Adapters.SQL.query(
Repo,
"select distinct unnest(nonexistent_locals.recipients) from activities, lateral (select array_agg(recipient) as recipients from unnest(activities.recipients) as recipient where recipient similar to '#{
instance_uri
}/users/[A-Za-z0-9]*' and not(recipient in (select ap_id from users where local = true))) nonexistent_locals;",
[],
timeout: :infinity
)
ap_ids
|> Enum.each(fn [ap_id] ->
Ecto.Changeset.change(%User{}, deactivated: true, ap_id: ap_id)
|> Repo.insert()
end)
end
end
end

View File

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.1055039ce3f2fe4dd110.css rel=stylesheet><link href=/static/fontello.1587147224637.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.c5bbd3734647f0cc7eef.js></script><script type=text/javascript src=/static/js/app.def6476e8bc9b214218b.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/vendors~app.18fea621d430000acc27.css rel=stylesheet><link href=/static/css/app.613cef07981cd95ccceb.css rel=stylesheet><link href=/static/fontello.1588419330867.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.8aa781e6dd81307f544b.js></script><script type=text/javascript src=/static/js/app.fa89b90e606f4facd209.js></script></body></html>

View File

@ -1 +0,0 @@
{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACxFA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.1055039ce3f2fe4dd110.css","sourcesContent":[".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}",".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n overflow-y: auto;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher .tabs .tab-wrapper {\n height: 28px;\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tabs .tab-wrapper .tab {\n width: 100%;\n min-width: 1px;\n position: relative;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 1em;\n padding-bottom: 99px;\n margin-bottom: -93px;\n white-space: nowrap;\n color: #b9b9ba;\n color: var(--tabText, #b9b9ba);\n background-color: #182230;\n background-color: var(--tab, #182230);\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tabs .tab-wrapper .tab.active {\n background: transparent;\n z-index: 5;\n color: #b9b9ba;\n color: var(--tabActiveText, #b9b9ba);\n}\n.tab-switcher .tabs .tab-wrapper .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 7;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}",".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}

Binary file not shown.

View File

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA,uBAAuB,aAAa,kBAAkB,qBAAqB,sBAAsB,qCAAqC,8BAA8B,e;ACApK,cAAc,oBAAoB,aAAa,0BAA0B,sBAAsB,wBAAwB,kBAAkB,cAAc,eAAe,gCAAgC,aAAa,wCAAwC,0BAA0B,aAAa,gBAAgB,oBAAoB,oBAAoB,aAAa,kBAAkB,WAAW,kBAAkB,gBAAgB,gBAAgB,sBAAsB,uDAAuD,cAAc,WAAW,kBAAkB,cAAc,wBAAwB,yBAAyB,wCAAwC,iCAAiC,YAAY,kBAAkB,oBAAoB,aAAa,kBAAkB,cAAc,sCAAsC,WAAW,cAAc,kBAAkB,4BAA4B,6BAA6B,gBAAgB,oBAAoB,oBAAoB,mBAAmB,cAAc,8BAA8B,yBAAyB,qCAAqC,mDAAmD,UAAU,yDAAyD,UAAU,6CAA6C,uBAAuB,UAAU,cAAc,oCAAoC,0CAA0C,gBAAgB,mBAAmB,gBAAgB,qDAAqD,WAAW,kBAAkB,OAAO,QAAQ,SAAS,UAAU,wBAAwB,yBAAyB,wC;ACAtlD,2BAA2B,aAAa,kBAAkB,kCAAkC,e","file":"static/css/app.613cef07981cd95ccceb.css","sourcesContent":[".with-load-more-footer{padding:10px;text-align:center;border-top:1px solid;border-top-color:#222;border-top-color:var(--border, #222)}.with-load-more-footer .error{font-size:14px}",".tab-switcher{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.tab-switcher .contents{-ms-flex:1 0 auto;flex:1 0 auto;min-height:0px}.tab-switcher .contents .hidden{display:none}.tab-switcher .contents.scrollable-tabs{-ms-flex-preferred-size:0;flex-basis:0;overflow-y:auto}.tab-switcher .tabs{display:-ms-flexbox;display:flex;position:relative;width:100%;overflow-y:hidden;overflow-x:auto;padding-top:5px;box-sizing:border-box}.tab-switcher .tabs::after,.tab-switcher .tabs::before{display:block;content:\"\";-ms-flex:1 1 auto;flex:1 1 auto;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}.tab-switcher .tabs .tab-wrapper{height:28px;position:relative;display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto}.tab-switcher .tabs .tab-wrapper .tab{width:100%;min-width:1px;position:relative;border-bottom-left-radius:0;border-bottom-right-radius:0;padding:6px 1em;padding-bottom:99px;margin-bottom:-93px;white-space:nowrap;color:#b9b9ba;color:var(--tabText, #b9b9ba);background-color:#182230;background-color:var(--tab, #182230)}.tab-switcher .tabs .tab-wrapper .tab:not(.active){z-index:4}.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover{z-index:6}.tab-switcher .tabs .tab-wrapper .tab.active{background:transparent;z-index:5;color:#b9b9ba;color:var(--tabActiveText, #b9b9ba)}.tab-switcher .tabs .tab-wrapper .tab img{max-height:26px;vertical-align:top;margin-top:-5px}.tab-switcher .tabs .tab-wrapper:not(.active)::after{content:\"\";position:absolute;left:0;right:0;bottom:0;z-index:7;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}",".with-subscription-loading{padding:10px;text-align:center}.with-subscription-loading .error{font-size:14px}"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -78,6 +78,10 @@
<glyph glyph-name="link" unicode="&#xe823;" d="M813 178q0 23-16 38l-116 116q-16 16-38 16-24 0-40-18 1-1 10-10t12-12 9-11 7-14 2-15q0-23-16-38t-38-16q-8 0-15 2t-14 7-11 9-12 12-10 10q-19-17-19-40 0-23 16-38l115-116q15-15 38-15 22 0 38 15l82 81q16 16 16 37z m-393 394q0 22-15 38l-115 115q-16 16-38 16-22 0-38-15l-82-82q-16-15-16-37 0-22 16-38l116-116q15-15 38-15 23 0 40 17-2 2-11 11t-12 12-8 10-7 14-2 16q0 22 15 38t38 15q9 0 16-2t14-7 11-8 12-12 10-11q18 17 18 41z m500-394q0-66-48-113l-82-81q-46-47-113-47-68 0-114 48l-115 115q-46 47-46 114 0 68 49 116l-49 49q-48-49-116-49-67 0-114 47l-116 116q-47 47-47 114t47 113l82 82q47 46 114 46 67 0 114-47l115-116q46-46 46-113 0-69-49-117l49-49q48 49 116 49 67 0 114-47l116-116q47-47 47-114z" horiz-adv-x="928.6" /> <glyph glyph-name="link" unicode="&#xe823;" d="M813 178q0 23-16 38l-116 116q-16 16-38 16-24 0-40-18 1-1 10-10t12-12 9-11 7-14 2-15q0-23-16-38t-38-16q-8 0-15 2t-14 7-11 9-12 12-10 10q-19-17-19-40 0-23 16-38l115-116q15-15 38-15 22 0 38 15l82 81q16 16 16 37z m-393 394q0 22-15 38l-115 115q-16 16-38 16-22 0-38-15l-82-82q-16-15-16-37 0-22 16-38l116-116q15-15 38-15 23 0 40 17-2 2-11 11t-12 12-8 10-7 14-2 16q0 22 15 38t38 15q9 0 16-2t14-7 11-8 12-12 10-11q18 17 18 41z m500-394q0-66-48-113l-82-81q-46-47-113-47-68 0-114 48l-115 115q-46 47-46 114 0 68 49 116l-49 49q-48-49-116-49-67 0-114 47l-116 116q-47 47-47 114t47 113l82 82q47 46 114 46 67 0 114-47l115-116q46-46 46-113 0-69-49-117l49-49q48 49 116 49 67 0 114-47l116-116q47-47 47-114z" horiz-adv-x="928.6" />
<glyph glyph-name="user" unicode="&#xe824;" d="M714 76q0-60-35-104t-84-44h-476q-49 0-84 44t-35 104q0 48 5 90t17 85 33 73 52 50 76 19q73-72 174-72t175 72q42 0 75-19t52-50 33-73 18-85 4-90z m-143 495q0-88-62-151t-152-63-151 63-63 151 63 152 151 63 152-63 62-152z" horiz-adv-x="714.3" />
<glyph glyph-name="ok" unicode="&#xe827;" d="M933 541q0-22-16-38l-404-404-76-76q-16-15-38-15t-38 15l-76 76-202 202q-15 16-15 38t15 38l76 76q16 16 38 16t38-16l164-165 366 367q16 16 38 16t38-16l76-76q16-15 16-38z" horiz-adv-x="1000" />
<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" /> <glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" /> <glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Binary file not shown.

View File

@ -345,6 +345,18 @@
"css": "link", "css": "link",
"code": 59427, "code": 59427,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "8b80d36d4ef43889db10bc1f0dc9a862",
"css": "user",
"code": 59428,
"src": "fontawesome"
},
{
"uid": "12f4ece88e46abd864e40b35e05b11cd",
"css": "ok",
"code": 59431,
"src": "fontawesome"
} }
] ]
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -61,7 +61,7 @@ test "new and validate" do
assert is_binary(answer) assert is_binary(answer)
assert :ok = Native.validate(token, answer, answer) assert :ok = Native.validate(token, answer, answer)
assert {:error, "Invalid CAPTCHA"} == Native.validate(token, answer, answer <> "foobar") assert {:error, :invalid} == Native.validate(token, answer, answer <> "foobar")
end end
end end
@ -78,6 +78,7 @@ test "validate" do
assert is_binary(answer) assert is_binary(answer)
assert :ok = Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer) assert :ok = Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer)
Cachex.del(:used_captcha_cache, token)
end end
test "doesn't validate invalid answer" do test "doesn't validate invalid answer" do
@ -92,7 +93,7 @@ test "doesn't validate invalid answer" do
assert is_binary(answer) assert is_binary(answer)
assert {:error, "Invalid answer data"} = assert {:error, :invalid_answer_data} =
Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer <> "foobar") Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer <> "foobar")
end end
@ -108,7 +109,7 @@ test "nil answer_data" do
assert is_binary(answer) assert is_binary(answer)
assert {:error, "Invalid answer data"} = assert {:error, :invalid_answer_data} =
Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", nil) Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", nil)
end end
end end

View File

@ -0,0 +1,112 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"CacheFile": "pt:CacheFile",
"Hashtag": "as:Hashtag",
"Infohash": "pt:Infohash",
"RsaSignature2017": "https://w3id.org/security#RsaSignature2017",
"category": "sc:category",
"commentsEnabled": {
"@id": "pt:commentsEnabled",
"@type": "sc:Boolean"
},
"downloadEnabled": {
"@id": "pt:downloadEnabled",
"@type": "sc:Boolean"
},
"expires": "sc:expires",
"fps": {
"@id": "pt:fps",
"@type": "sc:Number"
},
"language": "sc:inLanguage",
"licence": "sc:license",
"originallyPublishedAt": "sc:datePublished",
"position": {
"@id": "pt:position",
"@type": "sc:Number"
},
"pt": "https://joinpeertube.org/ns#",
"sc": "http://schema.org#",
"sensitive": "as:sensitive",
"size": {
"@id": "pt:size",
"@type": "sc:Number"
},
"startTimestamp": {
"@id": "pt:startTimestamp",
"@type": "sc:Number"
},
"state": {
"@id": "pt:state",
"@type": "sc:Number"
},
"stopTimestamp": {
"@id": "pt:stopTimestamp",
"@type": "sc:Number"
},
"subtitleLanguage": "sc:subtitleLanguage",
"support": {
"@id": "pt:support",
"@type": "sc:Text"
},
"uuid": "sc:identifier",
"views": {
"@id": "pt:views",
"@type": "sc:Number"
},
"waitTranscoding": {
"@id": "pt:waitTranscoding",
"@type": "sc:Boolean"
}
},
{
"comments": {
"@id": "as:comments",
"@type": "@id"
},
"dislikes": {
"@id": "as:dislikes",
"@type": "@id"
},
"likes": {
"@id": "as:likes",
"@type": "@id"
},
"playlists": {
"@id": "pt:playlists",
"@type": "@id"
},
"shares": {
"@id": "as:shares",
"@type": "@id"
}
}
],
"endpoints": {
"sharedInbox": "https://peertube.social/inbox"
},
"followers": "https://peertube.social/accounts/craigmaloney/followers",
"following": "https://peertube.social/accounts/craigmaloney/following",
"icon": {
"mediaType": "image/png",
"type": "Image",
"url": "https://peertube.social/lazy-static/avatars/87bd694b-95bc-4066-83f4-bddfcd2b9caa.png"
},
"id": "https://peertube.social/accounts/craigmaloney",
"inbox": "https://peertube.social/accounts/craigmaloney/inbox",
"name": "Craig Maloney",
"outbox": "https://peertube.social/accounts/craigmaloney/outbox",
"playlists": "https://peertube.social/accounts/craigmaloney/playlists",
"preferredUsername": "craigmaloney",
"publicKey": {
"id": "https://peertube.social/accounts/craigmaloney#main-key",
"owner": "https://peertube.social/accounts/craigmaloney",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9qvGIYUW01yc8CCsrwxK\n5OXlV5s7EbNWY8tJr/p1oGuELZwAnG2XKxtdbvgcCT+YxL5uRXIdCFIIIKrzRFr/\nHfS0mOgNT9u3gu+SstCNgtatciT0RVP77yiC3b2NHq1NRRvvVhzQb4cpIWObIxqh\nb2ypDClTc7XaKtgmQCbwZlGyZMT+EKz/vustD6BlpGsglRkm7iES6s1PPGb1BU+n\nS94KhbS2DOFiLcXCVWt0QarokIIuKznp4+xP1axKyP+SkT5AHx08Nd5TYFb2C1Jl\nz0WD/1q0mAN62m7QrA3SQPUgB+wWD+S3Nzf7FwNPiP4srbBgxVEUnji/r9mQ6BXC\nrQIDAQAB\n-----END PUBLIC KEY-----"
},
"summary": null,
"type": "Person",
"url": "https://peertube.social/accounts/craigmaloney"
}

View File

@ -0,0 +1,234 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"CacheFile": "pt:CacheFile",
"Hashtag": "as:Hashtag",
"Infohash": "pt:Infohash",
"RsaSignature2017": "https://w3id.org/security#RsaSignature2017",
"category": "sc:category",
"commentsEnabled": {
"@id": "pt:commentsEnabled",
"@type": "sc:Boolean"
},
"downloadEnabled": {
"@id": "pt:downloadEnabled",
"@type": "sc:Boolean"
},
"expires": "sc:expires",
"fps": {
"@id": "pt:fps",
"@type": "sc:Number"
},
"language": "sc:inLanguage",
"licence": "sc:license",
"originallyPublishedAt": "sc:datePublished",
"position": {
"@id": "pt:position",
"@type": "sc:Number"
},
"pt": "https://joinpeertube.org/ns#",
"sc": "http://schema.org#",
"sensitive": "as:sensitive",
"size": {
"@id": "pt:size",
"@type": "sc:Number"
},
"startTimestamp": {
"@id": "pt:startTimestamp",
"@type": "sc:Number"
},
"state": {
"@id": "pt:state",
"@type": "sc:Number"
},
"stopTimestamp": {
"@id": "pt:stopTimestamp",
"@type": "sc:Number"
},
"subtitleLanguage": "sc:subtitleLanguage",
"support": {
"@id": "pt:support",
"@type": "sc:Text"
},
"uuid": "sc:identifier",
"views": {
"@id": "pt:views",
"@type": "sc:Number"
},
"waitTranscoding": {
"@id": "pt:waitTranscoding",
"@type": "sc:Boolean"
}
},
{
"comments": {
"@id": "as:comments",
"@type": "@id"
},
"dislikes": {
"@id": "as:dislikes",
"@type": "@id"
},
"likes": {
"@id": "as:likes",
"@type": "@id"
},
"playlists": {
"@id": "pt:playlists",
"@type": "@id"
},
"shares": {
"@id": "as:shares",
"@type": "@id"
}
}
],
"attributedTo": [
{
"id": "https://peertube.social/accounts/craigmaloney",
"type": "Person"
},
{
"id": "https://peertube.social/video-channels/9909c7d9-6b5b-4aae-9164-c1af7229c91c",
"type": "Group"
}
],
"category": {
"identifier": "15",
"name": "Science & Technology"
},
"cc": [
"https://peertube.social/accounts/craigmaloney/followers"
],
"comments": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe/comments",
"commentsEnabled": true,
"content": "Support this and our other Michigan!/usr/group videos and meetings. Learn more at http://mug.org/membership\n\nTwenty Years in Jail: FreeBSD's Jails, Then and Now\n\nJails started as a limited virtualization system, but over the last two years they've...",
"dislikes": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe/dislikes",
"downloadEnabled": true,
"duration": "PT5151S",
"icon": {
"height": 122,
"mediaType": "image/jpeg",
"type": "Image",
"url": "https://peertube.social/static/thumbnails/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe.jpg",
"width": 223
},
"id": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe",
"language": {
"identifier": "en",
"name": "English"
},
"licence": {
"identifier": "1",
"name": "Attribution"
},
"likes": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe/likes",
"mediaType": "text/markdown",
"name": "Twenty Years in Jail: FreeBSD's Jails, Then and Now",
"originallyPublishedAt": "2019-08-13T00:00:00.000Z",
"published": "2020-02-12T01:06:08.054Z",
"sensitive": false,
"shares": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe/announces",
"state": 1,
"subtitleLanguage": [],
"support": "Learn more at http://mug.org",
"tag": [
{
"name": "linux",
"type": "Hashtag"
},
{
"name": "mug.org",
"type": "Hashtag"
},
{
"name": "open",
"type": "Hashtag"
},
{
"name": "oss",
"type": "Hashtag"
},
{
"name": "source",
"type": "Hashtag"
}
],
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Video",
"updated": "2020-02-15T15:01:09.474Z",
"url": [
{
"href": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe",
"mediaType": "text/html",
"type": "Link"
},
{
"fps": 30,
"height": 240,
"href": "https://peertube.social/static/webseed/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-240.mp4",
"mediaType": "video/mp4",
"size": 119465800,
"type": "Link"
},
{
"height": 240,
"href": "https://peertube.social/static/torrents/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-240.torrent",
"mediaType": "application/x-bittorrent",
"type": "Link"
},
{
"height": 240,
"href": "magnet:?xs=https%3A%2F%2Fpeertube.social%2Fstatic%2Ftorrents%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-240.torrent&xt=urn:btih:b3365331a8543bf48d09add56d7fe4b1cbbb5659&dn=Twenty+Years+in+Jail%3A+FreeBSD's+Jails%2C+Then+and+Now&tr=wss%3A%2F%2Fpeertube.social%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.social%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fwebseed%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-240.mp4",
"mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
"type": "Link"
},
{
"fps": 30,
"height": 360,
"href": "https://peertube.social/static/webseed/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-360.mp4",
"mediaType": "video/mp4",
"size": 143930318,
"type": "Link"
},
{
"height": 360,
"href": "https://peertube.social/static/torrents/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-360.torrent",
"mediaType": "application/x-bittorrent",
"type": "Link"
},
{
"height": 360,
"href": "magnet:?xs=https%3A%2F%2Fpeertube.social%2Fstatic%2Ftorrents%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-360.torrent&xt=urn:btih:0d37b23c98cb0d89e28b5dc8f49b3c97a041e569&dn=Twenty+Years+in+Jail%3A+FreeBSD's+Jails%2C+Then+and+Now&tr=wss%3A%2F%2Fpeertube.social%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.social%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fwebseed%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-360.mp4",
"mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
"type": "Link"
},
{
"fps": 30,
"height": 480,
"href": "https://peertube.social/static/webseed/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-480.mp4",
"mediaType": "video/mp4",
"size": 130530754,
"type": "Link"
},
{
"height": 480,
"href": "https://peertube.social/static/torrents/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-480.torrent",
"mediaType": "application/x-bittorrent",
"type": "Link"
},
{
"height": 480,
"href": "magnet:?xs=https%3A%2F%2Fpeertube.social%2Fstatic%2Ftorrents%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-480.torrent&xt=urn:btih:3a13ff822ad9494165eff6167183ddaaabc1372a&dn=Twenty+Years+in+Jail%3A+FreeBSD's+Jails%2C+Then+and+Now&tr=wss%3A%2F%2Fpeertube.social%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.social%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fwebseed%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-480.mp4",
"mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
"type": "Link"
}
],
"uuid": "278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe",
"views": 2,
"waitTranscoding": false
}

View File

@ -315,9 +315,7 @@ test "it creates `follow` notification for approved Follow activity" do
}) })
end end
test "if `follow_request` notifications are enabled, " <> test "it creates `follow_request` notification for pending Follow activity" do
"it creates `follow_request` notification for pending Follow activity" do
clear_config([:notifications, :enable_follow_request_notifications], true)
user = insert(:user) user = insert(:user)
followed_user = insert(:user, locked: true) followed_user = insert(:user, locked: true)
@ -336,21 +334,6 @@ test "if `follow_request` notifications are enabled, " <>
assert %{type: "follow"} = NotificationView.render("show.json", render_opts) assert %{type: "follow"} = NotificationView.render("show.json", render_opts)
end end
test "if `follow_request` notifications are disabled, " <>
"it does NOT create `follow*` notification for pending Follow activity" do
clear_config([:notifications, :enable_follow_request_notifications], false)
user = insert(:user)
followed_user = insert(:user, locked: true)
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
refute FollowingRelationship.following?(user, followed_user)
assert [] = Notification.for_user(followed_user)
# After request is accepted, no new notifications are generated:
assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
assert [] = Notification.for_user(followed_user)
end
test "it doesn't create a notification for follow-unfollow-follow chains" do test "it doesn't create a notification for follow-unfollow-follow chains" do
user = insert(:user) user = insert(:user)
followed_user = insert(:user, locked: false) followed_user = insert(:user, locked: false)
@ -367,7 +350,6 @@ test "it doesn't create a notification for follow-unfollow-follow chains" do
end end
test "dismisses the notification on follow request rejection" do test "dismisses the notification on follow request rejection" do
clear_config([:notifications, :enable_follow_request_notifications], true)
user = insert(:user, locked: true) user = insert(:user, locked: true)
follower = insert(:user) follower = insert(:user)
{:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user) {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user)

View File

@ -44,7 +44,8 @@ test "it returns key" do
test "it returns error when not found user" do test "it returns error when not found user" do
assert capture_log(fn -> assert capture_log(fn ->
assert Signature.fetch_public_key(make_fake_conn("test-ap_id")) == {:error, :error} assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) ==
{:error, :error}
end) =~ "[error] Could not decode user" end) =~ "[error] Could not decode user"
end end
@ -64,7 +65,7 @@ test "it returns key" do
test "it returns error when not found user" do test "it returns error when not found user" do
assert capture_log(fn -> assert capture_log(fn ->
{:error, _} = Signature.refetch_public_key(make_fake_conn("test-ap_id")) {:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id"))
end) =~ "[error] Could not decode user" end) =~ "[error] Could not decode user"
end end
end end
@ -100,12 +101,21 @@ test "it returns error" do
describe "key_id_to_actor_id/1" do describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do test "it properly deduces the actor id for misskey" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") == assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
"https://example.com/users/1234" {:ok, "https://example.com/users/1234"}
end end
test "it properly deduces the actor id for mastodon and pleroma" do test "it properly deduces the actor id for mastodon and pleroma" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") == assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") ==
"https://example.com/users/1234" {:ok, "https://example.com/users/1234"}
end
test "it calls webfinger for 'acct:' accounts" do
with_mock(Pleroma.Web.WebFinger,
finger: fn _ -> %{"ap_id" => "https://gensokyo.2hu/users/raymoo"} end
) do
assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") ==
{:ok, "https://gensokyo.2hu/users/raymoo"}
end
end end
end end

View File

@ -6,12 +6,16 @@ defmodule Pleroma.Captcha.Mock do
alias Pleroma.Captcha.Service alias Pleroma.Captcha.Service
@behaviour Service @behaviour Service
@solution "63615261b77f5354fb8c4e4986477555"
def solution, do: @solution
@impl Service @impl Service
def new, def new,
do: %{ do: %{
type: :mock, type: :mock,
token: "afa1815e14e29355e6c8f6b143a39fa2", token: "afa1815e14e29355e6c8f6b143a39fa2",
answer_data: "63615261b77f5354fb8c4e4986477555", answer_data: @solution,
url: "https://example.org/captcha.png" url: "https://example.org/captcha.png"
} }

View File

@ -308,6 +308,22 @@ def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
}} }}
end end
def get("https://peertube.social/accounts/craigmaloney", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/craigmaloney.json")
}}
end
def get("https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/peertube-social.json")
}}
end
def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, _, [ def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, _, [
{"accept", "application/activity+json"} {"accept", "application/activity+json"}
]) do ]) do

View File

@ -92,7 +92,7 @@ test "user is deleted" do
assert_received {:mix_shell, :info, [message]} assert_received {:mix_shell, :info, [message]}
assert message =~ " deleted" assert message =~ " deleted"
refute User.get_by_nickname(user.nickname) assert %{deactivated: true} = User.get_by_nickname(user.nickname)
end end
test "no user to delete" do test "no user to delete" do

View File

@ -1135,16 +1135,7 @@ test ".delete_user_activities deletes all create activities", %{user: user} do
refute Activity.get_by_id(activity.id) refute Activity.get_by_id(activity.id)
end end
test "it deletes deactivated user" do test "it deactivates a user, all follow relationships and all activities", %{user: user} do
{:ok, user} = insert(:user, deactivated: true) |> User.set_cache()
{:ok, job} = User.delete(user)
{:ok, _user} = ObanHelpers.perform(job)
refute User.get_by_id(user.id)
end
test "it deletes a user, all follow relationships and all activities", %{user: user} do
follower = insert(:user) follower = insert(:user)
{:ok, follower} = User.follow(follower, user) {:ok, follower} = User.follow(follower, user)
@ -1164,8 +1155,7 @@ test "it deletes a user, all follow relationships and all activities", %{user: u
follower = User.get_cached_by_id(follower.id) follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user) refute User.following?(follower, user)
refute User.get_by_id(user.id) assert %{deactivated: true} = User.get_by_id(user.id)
assert {:ok, nil} == Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
user_activities = user_activities =
user.ap_id user.ap_id

View File

@ -1055,12 +1055,12 @@ test "it works for more than 10 users", %{conn: conn} do
assert result["totalItems"] == 15 assert result["totalItems"] == 15
end end
test "returns 403 if requester is not logged in", %{conn: conn} do test "does not require authentication", %{conn: conn} do
user = insert(:user) user = insert(:user)
conn conn
|> get("/users/#{user.nickname}/followers") |> get("/users/#{user.nickname}/followers")
|> json_response(403) |> json_response(200)
end end
end end
@ -1152,12 +1152,12 @@ test "it works for more than 10 users", %{conn: conn} do
assert result["totalItems"] == 15 assert result["totalItems"] == 15
end end
test "returns 403 if requester is not logged in", %{conn: conn} do test "does not require authentication", %{conn: conn} do
user = insert(:user) user = insert(:user)
conn conn
|> get("/users/#{user.nickname}/following") |> get("/users/#{user.nickname}/following")
|> json_response(403) |> json_response(200)
end end
end end

View File

@ -872,7 +872,8 @@ test "it fails for incoming deletes with spoofed origin" do
@tag capture_log: true @tag capture_log: true
test "it works for incoming user deletes" do test "it works for incoming user deletes" do
%{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") %{ap_id: ap_id} =
insert(:user, ap_id: "http://mastodon.example.org/users/admin", local: false)
data = data =
File.read!("test/fixtures/mastodon-delete-user.json") File.read!("test/fixtures/mastodon-delete-user.json")
@ -1221,6 +1222,35 @@ test "it rejects activities without a valid ID" do
:error = Transmogrifier.handle_incoming(data) :error = Transmogrifier.handle_incoming(data)
end end
test "skip converting the content when it is nil" do
object_id = "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe"
{:ok, object} = Fetcher.fetch_and_contain_remote_object_from_id(object_id)
result =
Pleroma.Web.ActivityPub.Transmogrifier.fix_object(Map.merge(object, %{"content" => nil}))
assert result["content"] == nil
end
test "it converts content of object to html" do
object_id = "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe"
{:ok, %{"content" => content_markdown}} =
Fetcher.fetch_and_contain_remote_object_from_id(object_id)
{:ok, %Pleroma.Object{data: %{"content" => content}} = object} =
Fetcher.fetch_object_from_id(object_id)
assert content_markdown ==
"Support this and our other Michigan!/usr/group videos and meetings. Learn more at http://mug.org/membership\n\nTwenty Years in Jail: FreeBSD's Jails, Then and Now\n\nJails started as a limited virtualization system, but over the last two years they've..."
assert content ==
"<p>Support this and our other Michigan!/usr/group videos and meetings. Learn more at <a href=\"http://mug.org/membership\">http://mug.org/membership</a></p><p>Twenty Years in Jail: FreeBSDs Jails, Then and Now</p><p>Jails started as a limited virtualization system, but over the last two years theyve…</p>"
assert object.data["mediaType"] == "text/html"
end
test "it remaps video URLs as attachments if necessary" do test "it remaps video URLs as attachments if necessary" do
{:ok, object} = {:ok, object} =
Fetcher.fetch_object_from_id( Fetcher.fetch_object_from_id(

View File

@ -1347,9 +1347,9 @@ test "returns report by its id", %{conn: conn} do
{:ok, %{id: report_id}} = {:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I feel offended", comment: "I feel offended",
"status_ids" => [activity.id] status_ids: [activity.id]
}) })
response = response =
@ -1374,16 +1374,16 @@ test "returns 404 when report id is invalid", %{conn: conn} do
{:ok, %{id: report_id}} = {:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I feel offended", comment: "I feel offended",
"status_ids" => [activity.id] status_ids: [activity.id]
}) })
{:ok, %{id: second_report_id}} = {:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I feel very offended", comment: "I feel very offended",
"status_ids" => [activity.id] status_ids: [activity.id]
}) })
%{ %{
@ -1523,9 +1523,9 @@ test "returns reports", %{conn: conn} do
{:ok, %{id: report_id}} = {:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I feel offended", comment: "I feel offended",
"status_ids" => [activity.id] status_ids: [activity.id]
}) })
response = response =
@ -1547,15 +1547,15 @@ test "returns reports with specified state", %{conn: conn} do
{:ok, %{id: first_report_id}} = {:ok, %{id: first_report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I feel offended", comment: "I feel offended",
"status_ids" => [activity.id] status_ids: [activity.id]
}) })
{:ok, %{id: second_report_id}} = {:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I don't like this user" comment: "I don't like this user"
}) })
CommonAPI.update_report_state(second_report_id, "closed") CommonAPI.update_report_state(second_report_id, "closed")
@ -3431,9 +3431,9 @@ test "it resend emails for two users", %{conn: conn, admin: admin} do
{:ok, %{id: report_id}} = {:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I feel offended", comment: "I feel offended",
"status_ids" => [activity.id] status_ids: [activity.id]
}) })
post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{ post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{

View File

@ -15,7 +15,7 @@ test "renders a report" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.report(user, %{"account_id" => other_user.id}) {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id})
expected = %{ expected = %{
content: nil, content: nil,
@ -48,7 +48,7 @@ test "includes reported statuses" do
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "toot"}) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "toot"})
{:ok, report_activity} = {:ok, report_activity} =
CommonAPI.report(user, %{"account_id" => other_user.id, "status_ids" => [activity.id]}) CommonAPI.report(user, %{account_id: other_user.id, status_ids: [activity.id]})
other_user = Pleroma.User.get_by_id(other_user.id) other_user = Pleroma.User.get_by_id(other_user.id)
@ -81,7 +81,7 @@ test "renders report's state" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.report(user, %{"account_id" => other_user.id}) {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id})
{:ok, activity} = CommonAPI.update_report_state(activity.id, "closed") {:ok, activity} = CommonAPI.update_report_state(activity.id, "closed")
assert %{state: "closed"} = assert %{state: "closed"} =
@ -94,8 +94,8 @@ test "renders report description" do
{:ok, activity} = {:ok, activity} =
CommonAPI.report(user, %{ CommonAPI.report(user, %{
"account_id" => other_user.id, account_id: other_user.id,
"comment" => "posts are too good for this instance" comment: "posts are too good for this instance"
}) })
assert %{content: "posts are too good for this instance"} = assert %{content: "posts are too good for this instance"} =
@ -108,8 +108,8 @@ test "sanitizes report description" do
{:ok, activity} = {:ok, activity} =
CommonAPI.report(user, %{ CommonAPI.report(user, %{
"account_id" => other_user.id, account_id: other_user.id,
"comment" => "" comment: ""
}) })
data = Map.put(activity.data, "content", "<script> alert('hecked :D:D:D:D:D:D:D') </script>") data = Map.put(activity.data, "content", "<script> alert('hecked :D:D:D:D:D:D:D') </script>")
@ -125,8 +125,8 @@ test "doesn't error out when the user doesn't exists" do
{:ok, activity} = {:ok, activity} =
CommonAPI.report(user, %{ CommonAPI.report(user, %{
"account_id" => other_user.id, account_id: other_user.id,
"comment" => "" comment: ""
}) })
Pleroma.User.delete(other_user) Pleroma.User.delete(other_user)

View File

@ -485,9 +485,9 @@ test "creates a report" do
comment = "foobar" comment = "foobar"
report_data = %{ report_data = %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => comment, comment: comment,
"status_ids" => [activity.id] status_ids: [activity.id]
} }
note_obj = %{ note_obj = %{
@ -517,9 +517,9 @@ test "updates report state" do
{:ok, %Activity{id: report_id}} = {:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I feel offended", comment: "I feel offended",
"status_ids" => [activity.id] status_ids: [activity.id]
}) })
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved") {:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
@ -538,9 +538,9 @@ test "does not update report state when state is unsupported" do
{:ok, %Activity{id: report_id}} = {:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I feel offended", comment: "I feel offended",
"status_ids" => [activity.id] status_ids: [activity.id]
}) })
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"} assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
@ -552,16 +552,16 @@ test "updates state of multiple reports" do
{:ok, %Activity{id: first_report_id}} = {:ok, %Activity{id: first_report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I feel offended", comment: "I feel offended",
"status_ids" => [activity.id] status_ids: [activity.id]
}) })
{:ok, %Activity{id: second_report_id}} = {:ok, %Activity{id: second_report_id}} =
CommonAPI.report(reporter, %{ CommonAPI.report(reporter, %{
"account_id" => target_user.id, account_id: target_user.id,
"comment" => "I feel very offended!", comment: "I feel very offended!",
"status_ids" => [activity.id] status_ids: [activity.id]
}) })
{:ok, report_ids} = {:ok, report_ids} =
@ -697,6 +697,14 @@ test "after rejection, it sets all existing pending follow request states to 're
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end end
test "doesn't create a following relationship if the corresponding follow request doesn't exist" do
user = insert(:user, locked: true)
not_follower = insert(:user)
CommonAPI.accept_follow_request(not_follower, user)
assert Pleroma.FollowingRelationship.following?(not_follower, user) == false
end
end end
describe "vote/3" do describe "vote/3" do

View File

@ -925,7 +925,8 @@ test "returns bad_request if missing email params when :account_activation_requi
|> Map.put(:remote_ip, {127, 0, 0, 5}) |> Map.put(:remote_ip, {127, 0, 0, 5})
|> post("/api/v1/accounts", Map.delete(valid_params, :email)) |> post("/api/v1/accounts", Map.delete(valid_params, :email))
assert json_response_and_validate_schema(res, 400) == %{"error" => "Missing parameters"} assert json_response_and_validate_schema(res, 400) ==
%{"error" => "Missing parameter: email"}
res = res =
conn conn
@ -1093,6 +1094,91 @@ test "respects rate limit setting", %{conn: conn} do
end end
end end
describe "create account with enabled captcha" do
setup %{conn: conn} do
app_token = insert(:oauth_token, user: nil)
conn =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
|> put_req_header("content-type", "multipart/form-data")
[conn: conn]
end
setup do: clear_config([Pleroma.Captcha, :enabled], true)
test "creates an account and returns 200 if captcha is valid", %{conn: conn} do
%{token: token, answer_data: answer_data} = Pleroma.Captcha.new()
params = %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
agreement: true,
captcha_solution: Pleroma.Captcha.Mock.solution(),
captcha_token: token,
captcha_answer_data: answer_data
}
assert %{
"access_token" => access_token,
"created_at" => _,
"scope" => ["read"],
"token_type" => "Bearer"
} =
conn
|> post("/api/v1/accounts", params)
|> json_response_and_validate_schema(:ok)
assert Token |> Repo.get_by(token: access_token) |> Repo.preload(:user) |> Map.get(:user)
Cachex.del(:used_captcha_cache, token)
end
test "returns 400 if any captcha field is not provided", %{conn: conn} do
captcha_fields = [:captcha_solution, :captcha_token, :captcha_answer_data]
valid_params = %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
agreement: true,
captcha_solution: "xx",
captcha_token: "xx",
captcha_answer_data: "xx"
}
for field <- captcha_fields do
expected = %{
"error" => "{\"captcha\":[\"Invalid CAPTCHA (Missing parameter: #{field})\"]}"
}
assert expected ==
conn
|> post("/api/v1/accounts", Map.delete(valid_params, field))
|> json_response_and_validate_schema(:bad_request)
end
end
test "returns an error if captcha is invalid", %{conn: conn} do
params = %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
agreement: true,
captcha_solution: "cofe",
captcha_token: "cofe",
captcha_answer_data: "cofe"
}
assert %{"error" => "{\"captcha\":[\"Invalid answer data\"]}"} ==
conn
|> post("/api/v1/accounts", params)
|> json_response_and_validate_schema(:bad_request)
end
end
describe "GET /api/v1/accounts/:id/lists - account_lists" do describe "GET /api/v1/accounts/:id/lists - account_lists" do
test "returns lists to which the account belongs" do test "returns lists to which the account belongs" do
%{user: user, conn: conn} = oauth_access(["read:lists"]) %{user: user, conn: conn} = oauth_access(["read:lists"])

View File

@ -34,6 +34,10 @@ test "get instance information", %{conn: conn} do
"banner_upload_limit" => _ "banner_upload_limit" => _
} = result } = result
assert result["pleroma"]["metadata"]["features"]
assert result["pleroma"]["metadata"]["federation"]
assert result["pleroma"]["vapid_public_key"]
assert email == from_config_email assert email == from_config_email
end end

View File

@ -25,7 +25,7 @@ test "does NOT render account/pleroma/relationship if this is disabled by defaul
conn conn
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/notifications") |> get("/api/v1/notifications")
|> json_response(200) |> json_response_and_validate_schema(200)
assert Enum.all?(response, fn n -> assert Enum.all?(response, fn n ->
get_in(n, ["account", "pleroma", "relationship"]) == %{} get_in(n, ["account", "pleroma", "relationship"]) == %{}
@ -50,7 +50,9 @@ test "list of notifications" do
user.ap_id user.ap_id
}\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>" }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200) assert [%{"status" => %{"content" => response}} | _rest] =
json_response_and_validate_schema(conn, 200)
assert response == expected_response assert response == expected_response
end end
@ -69,7 +71,7 @@ test "getting a single notification" do
user.ap_id user.ap_id
}\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>" }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
assert %{"status" => %{"content" => response}} = json_response(conn, 200) assert %{"status" => %{"content" => response}} = json_response_and_validate_schema(conn, 200)
assert response == expected_response assert response == expected_response
end end
@ -84,9 +86,10 @@ test "dismissing a single notification (deprecated endpoint)" do
conn = conn =
conn conn
|> assign(:user, user) |> assign(:user, user)
|> post("/api/v1/notifications/dismiss", %{"id" => notification.id}) |> put_req_header("content-type", "application/json")
|> post("/api/v1/notifications/dismiss", %{"id" => to_string(notification.id)})
assert %{} = json_response(conn, 200) assert %{} = json_response_and_validate_schema(conn, 200)
end end
test "dismissing a single notification" do test "dismissing a single notification" do
@ -102,7 +105,7 @@ test "dismissing a single notification" do
|> assign(:user, user) |> assign(:user, user)
|> post("/api/v1/notifications/#{notification.id}/dismiss") |> post("/api/v1/notifications/#{notification.id}/dismiss")
assert %{} = json_response(conn, 200) assert %{} = json_response_and_validate_schema(conn, 200)
end end
test "clearing all notifications" do test "clearing all notifications" do
@ -115,11 +118,11 @@ test "clearing all notifications" do
ret_conn = post(conn, "/api/v1/notifications/clear") ret_conn = post(conn, "/api/v1/notifications/clear")
assert %{} = json_response(ret_conn, 200) assert %{} = json_response_and_validate_schema(ret_conn, 200)
ret_conn = get(conn, "/api/v1/notifications") ret_conn = get(conn, "/api/v1/notifications")
assert all = json_response(ret_conn, 200) assert all = json_response_and_validate_schema(ret_conn, 200)
assert all == [] assert all == []
end end
@ -143,7 +146,7 @@ test "paginates notifications using min_id, since_id, max_id, and limit" do
result = result =
conn conn
|> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}") |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}")
|> json_response(:ok) |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
@ -151,7 +154,7 @@ test "paginates notifications using min_id, since_id, max_id, and limit" do
result = result =
conn conn
|> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}") |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}")
|> json_response(:ok) |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
@ -159,7 +162,7 @@ test "paginates notifications using min_id, since_id, max_id, and limit" do
result = result =
conn conn
|> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}") |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}")
|> json_response(:ok) |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
end end
@ -181,36 +184,28 @@ test "filters notifications for mentions" do
{:ok, private_activity} = {:ok, private_activity} =
CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"}) CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"})
conn_res = query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "private"]})
get(conn, "/api/v1/notifications", %{ conn_res = get(conn, "/api/v1/notifications?" <> query)
exclude_visibilities: ["public", "unlisted", "private"]
})
assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == direct_activity.id assert id == direct_activity.id
conn_res = query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "direct"]})
get(conn, "/api/v1/notifications", %{ conn_res = get(conn, "/api/v1/notifications?" <> query)
exclude_visibilities: ["public", "unlisted", "direct"]
})
assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == private_activity.id assert id == private_activity.id
conn_res = query = params_to_query(%{exclude_visibilities: ["public", "private", "direct"]})
get(conn, "/api/v1/notifications", %{ conn_res = get(conn, "/api/v1/notifications?" <> query)
exclude_visibilities: ["public", "private", "direct"]
})
assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == unlisted_activity.id assert id == unlisted_activity.id
conn_res = query = params_to_query(%{exclude_visibilities: ["unlisted", "private", "direct"]})
get(conn, "/api/v1/notifications", %{ conn_res = get(conn, "/api/v1/notifications?" <> query)
exclude_visibilities: ["unlisted", "private", "direct"]
})
assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == public_activity.id assert id == public_activity.id
end end
@ -237,8 +232,8 @@ test "filters notifications for Like activities" do
activity_ids = activity_ids =
conn conn
|> get("/api/v1/notifications", %{exclude_visibilities: ["direct"]}) |> get("/api/v1/notifications?exclude_visibilities[]=direct")
|> json_response(200) |> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"]) |> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids assert public_activity.id in activity_ids
@ -248,8 +243,8 @@ test "filters notifications for Like activities" do
activity_ids = activity_ids =
conn conn
|> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]}) |> get("/api/v1/notifications?exclude_visibilities[]=unlisted")
|> json_response(200) |> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"]) |> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids assert public_activity.id in activity_ids
@ -259,8 +254,8 @@ test "filters notifications for Like activities" do
activity_ids = activity_ids =
conn conn
|> get("/api/v1/notifications", %{exclude_visibilities: ["private"]}) |> get("/api/v1/notifications?exclude_visibilities[]=private")
|> json_response(200) |> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"]) |> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids assert public_activity.id in activity_ids
@ -270,8 +265,8 @@ test "filters notifications for Like activities" do
activity_ids = activity_ids =
conn conn
|> get("/api/v1/notifications", %{exclude_visibilities: ["public"]}) |> get("/api/v1/notifications?exclude_visibilities[]=public")
|> json_response(200) |> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"]) |> Enum.map(& &1["status"]["id"])
refute public_activity.id in activity_ids refute public_activity.id in activity_ids
@ -295,8 +290,8 @@ test "filters notifications for Announce activities" do
activity_ids = activity_ids =
conn conn
|> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]}) |> get("/api/v1/notifications?exclude_visibilities[]=unlisted")
|> json_response(200) |> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"]) |> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids assert public_activity.id in activity_ids
@ -319,25 +314,27 @@ test "filters notifications using exclude_types" do
reblog_notification_id = get_notification_id_by_activity(reblog_activity) reblog_notification_id = get_notification_id_by_activity(reblog_activity)
follow_notification_id = get_notification_id_by_activity(follow_activity) follow_notification_id = get_notification_id_by_activity(follow_activity)
conn_res = query = params_to_query(%{exclude_types: ["mention", "favourite", "reblog"]})
get(conn, "/api/v1/notifications", %{exclude_types: ["mention", "favourite", "reblog"]}) conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200) assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
conn_res = query = params_to_query(%{exclude_types: ["favourite", "reblog", "follow"]})
get(conn, "/api/v1/notifications", %{exclude_types: ["favourite", "reblog", "follow"]}) conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200) assert [%{"id" => ^mention_notification_id}] =
json_response_and_validate_schema(conn_res, 200)
conn_res = query = params_to_query(%{exclude_types: ["reblog", "follow", "mention"]})
get(conn, "/api/v1/notifications", %{exclude_types: ["reblog", "follow", "mention"]}) conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200) assert [%{"id" => ^favorite_notification_id}] =
json_response_and_validate_schema(conn_res, 200)
conn_res = query = params_to_query(%{exclude_types: ["follow", "mention", "favourite"]})
get(conn, "/api/v1/notifications", %{exclude_types: ["follow", "mention", "favourite"]}) conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200) assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200)
end end
test "filters notifications using include_types" do test "filters notifications using include_types" do
@ -355,32 +352,34 @@ test "filters notifications using include_types" do
reblog_notification_id = get_notification_id_by_activity(reblog_activity) reblog_notification_id = get_notification_id_by_activity(reblog_activity)
follow_notification_id = get_notification_id_by_activity(follow_activity) follow_notification_id = get_notification_id_by_activity(follow_activity)
conn_res = get(conn, "/api/v1/notifications", %{include_types: ["follow"]}) conn_res = get(conn, "/api/v1/notifications?include_types[]=follow")
assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200) assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
conn_res = get(conn, "/api/v1/notifications", %{include_types: ["mention"]}) conn_res = get(conn, "/api/v1/notifications?include_types[]=mention")
assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200) assert [%{"id" => ^mention_notification_id}] =
json_response_and_validate_schema(conn_res, 200)
conn_res = get(conn, "/api/v1/notifications", %{include_types: ["favourite"]}) conn_res = get(conn, "/api/v1/notifications?include_types[]=favourite")
assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200) assert [%{"id" => ^favorite_notification_id}] =
json_response_and_validate_schema(conn_res, 200)
conn_res = get(conn, "/api/v1/notifications", %{include_types: ["reblog"]}) conn_res = get(conn, "/api/v1/notifications?include_types[]=reblog")
assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200) assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200)
result = conn |> get("/api/v1/notifications") |> json_response(200) result = conn |> get("/api/v1/notifications") |> json_response_and_validate_schema(200)
assert length(result) == 4 assert length(result) == 4
query = params_to_query(%{include_types: ["follow", "mention", "favourite", "reblog"]})
result = result =
conn conn
|> get("/api/v1/notifications", %{ |> get("/api/v1/notifications?" <> query)
include_types: ["follow", "mention", "favourite", "reblog"] |> json_response_and_validate_schema(200)
})
|> json_response(200)
assert length(result) == 4 assert length(result) == 4
end end
@ -402,7 +401,7 @@ test "destroy multiple" do
result = result =
conn conn
|> get("/api/v1/notifications") |> get("/api/v1/notifications")
|> json_response(:ok) |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result
@ -414,22 +413,19 @@ test "destroy multiple" do
result = result =
conn2 conn2
|> get("/api/v1/notifications") |> get("/api/v1/notifications")
|> json_response(:ok) |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
conn_destroy = query = params_to_query(%{ids: [notification1_id, notification2_id]})
conn conn_destroy = delete(conn, "/api/v1/notifications/destroy_multiple?" <> query)
|> delete("/api/v1/notifications/destroy_multiple", %{
"ids" => [notification1_id, notification2_id]
})
assert json_response(conn_destroy, 200) == %{} assert json_response_and_validate_schema(conn_destroy, 200) == %{}
result = result =
conn2 conn2
|> get("/api/v1/notifications") |> get("/api/v1/notifications")
|> json_response(:ok) |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
end end
@ -443,13 +439,13 @@ test "doesn't see notifications after muting user with notifications" do
ret_conn = get(conn, "/api/v1/notifications") ret_conn = get(conn, "/api/v1/notifications")
assert length(json_response(ret_conn, 200)) == 1 assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
{:ok, _user_relationships} = User.mute(user, user2) {:ok, _user_relationships} = User.mute(user, user2)
conn = get(conn, "/api/v1/notifications") conn = get(conn, "/api/v1/notifications")
assert json_response(conn, 200) == [] assert json_response_and_validate_schema(conn, 200) == []
end end
test "see notifications after muting user without notifications" do test "see notifications after muting user without notifications" do
@ -461,13 +457,13 @@ test "see notifications after muting user without notifications" do
ret_conn = get(conn, "/api/v1/notifications") ret_conn = get(conn, "/api/v1/notifications")
assert length(json_response(ret_conn, 200)) == 1 assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
{:ok, _user_relationships} = User.mute(user, user2, false) {:ok, _user_relationships} = User.mute(user, user2, false)
conn = get(conn, "/api/v1/notifications") conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1 assert length(json_response_and_validate_schema(conn, 200)) == 1
end end
test "see notifications after muting user with notifications and with_muted parameter" do test "see notifications after muting user with notifications and with_muted parameter" do
@ -479,13 +475,13 @@ test "see notifications after muting user with notifications and with_muted para
ret_conn = get(conn, "/api/v1/notifications") ret_conn = get(conn, "/api/v1/notifications")
assert length(json_response(ret_conn, 200)) == 1 assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
{:ok, _user_relationships} = User.mute(user, user2) {:ok, _user_relationships} = User.mute(user, user2)
conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"}) conn = get(conn, "/api/v1/notifications?with_muted=true")
assert length(json_response(conn, 200)) == 1 assert length(json_response_and_validate_schema(conn, 200)) == 1
end end
@tag capture_log: true @tag capture_log: true
@ -512,7 +508,7 @@ test "see move notifications" do
conn = get(conn, "/api/v1/notifications") conn = get(conn, "/api/v1/notifications")
assert length(json_response(conn, 200)) == 1 assert length(json_response_and_validate_schema(conn, 200)) == 1
end end
describe "link headers" do describe "link headers" do
@ -538,10 +534,10 @@ test "preserves parameters in link headers" do
conn = conn =
conn conn
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/notifications", %{media_only: true}) |> get("/api/v1/notifications?limit=5")
assert [link_header] = get_resp_header(conn, "link") assert [link_header] = get_resp_header(conn, "link")
assert link_header =~ ~r/media_only=true/ assert link_header =~ ~r/limit=5/
assert link_header =~ ~r/min_id=#{notification2.id}/ assert link_header =~ ~r/min_id=#{notification2.id}/
assert link_header =~ ~r/max_id=#{notification1.id}/ assert link_header =~ ~r/max_id=#{notification1.id}/
end end
@ -560,14 +556,14 @@ test "account_id" do
assert [%{"account" => %{"id" => ^account_id}}] = assert [%{"account" => %{"id" => ^account_id}}] =
conn conn
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/notifications", %{account_id: account_id}) |> get("/api/v1/notifications?account_id=#{account_id}")
|> json_response(200) |> json_response_and_validate_schema(200)
assert %{"error" => "Account is not found"} = assert %{"error" => "Account is not found"} =
conn conn
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/notifications", %{account_id: "cofe"}) |> get("/api/v1/notifications?account_id=cofe")
|> json_response(404) |> json_response_and_validate_schema(404)
end end
end end
@ -577,4 +573,11 @@ defp get_notification_id_by_activity(%{id: id}) do
|> Map.get(:id) |> Map.get(:id)
|> to_string() |> to_string()
end end
defp params_to_query(%{} = params) do
Enum.map_join(params, "&", fn
{k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}")
{k, v} -> k <> "=" <> v
end)
end
end end

View File

@ -22,8 +22,9 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
test "submit a basic report", %{conn: conn, target_user: target_user} do test "submit a basic report", %{conn: conn, target_user: target_user} do
assert %{"action_taken" => false, "id" => _} = assert %{"action_taken" => false, "id" => _} =
conn conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"account_id" => target_user.id}) |> post("/api/v1/reports", %{"account_id" => target_user.id})
|> json_response(200) |> json_response_and_validate_schema(200)
end end
test "submit a report with statuses and comment", %{ test "submit a report with statuses and comment", %{
@ -33,23 +34,25 @@ test "submit a report with statuses and comment", %{
} do } do
assert %{"action_taken" => false, "id" => _} = assert %{"action_taken" => false, "id" => _} =
conn conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{ |> post("/api/v1/reports", %{
"account_id" => target_user.id, "account_id" => target_user.id,
"status_ids" => [activity.id], "status_ids" => [activity.id],
"comment" => "bad status!", "comment" => "bad status!",
"forward" => "false" "forward" => "false"
}) })
|> json_response(200) |> json_response_and_validate_schema(200)
end end
test "account_id is required", %{ test "account_id is required", %{
conn: conn, conn: conn,
activity: activity activity: activity
} do } do
assert %{"error" => "Valid `account_id` required"} = assert %{"error" => "Missing field: account_id."} =
conn conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"status_ids" => [activity.id]}) |> post("/api/v1/reports", %{"status_ids" => [activity.id]})
|> json_response(400) |> json_response_and_validate_schema(400)
end end
test "comment must be up to the size specified in the config", %{ test "comment must be up to the size specified in the config", %{
@ -63,17 +66,21 @@ test "comment must be up to the size specified in the config", %{
assert ^error = assert ^error =
conn conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment}) |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment})
|> json_response(400) |> json_response_and_validate_schema(400)
end end
test "returns error when account is not exist", %{ test "returns error when account is not exist", %{
conn: conn, conn: conn,
activity: activity activity: activity
} do } do
conn = post(conn, "/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"}) conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"})
assert json_response(conn, 400) == %{"error" => "Account not found"} assert json_response_and_validate_schema(conn, 400) == %{"error" => "Account not found"}
end end
test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_user} do test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_user} do
@ -81,7 +88,8 @@ test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_
assert %{"action_taken" => false, "id" => _} = assert %{"action_taken" => false, "id" => _} =
conn conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"account_id" => target_user.id}) |> post("/api/v1/reports", %{"account_id" => target_user.id})
|> json_response(200) |> json_response_and_validate_schema(200)
end end
end end

View File

@ -5,8 +5,6 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Config
setup do: oauth_access(["read"]) setup do: oauth_access(["read"])
test "returns empty result", %{conn: conn} do test "returns empty result", %{conn: conn} do

View File

@ -18,7 +18,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
test "it registers a new user and returns the user." do test "it registers a new user and returns the user." do
data = %{ data = %{
:nickname => "lain", :username => "lain",
:email => "lain@wired.jp", :email => "lain@wired.jp",
:fullname => "lain iwakura", :fullname => "lain iwakura",
:password => "bear", :password => "bear",
@ -35,7 +35,7 @@ test "it registers a new user and returns the user." do
test "it registers a new user with empty string in bio and returns the user." do test "it registers a new user with empty string in bio and returns the user." do
data = %{ data = %{
:nickname => "lain", :username => "lain",
:email => "lain@wired.jp", :email => "lain@wired.jp",
:fullname => "lain iwakura", :fullname => "lain iwakura",
:bio => "", :bio => "",
@ -60,7 +60,7 @@ test "it sends confirmation email if :account_activation_required is specified i
end end
data = %{ data = %{
:nickname => "lain", :username => "lain",
:email => "lain@wired.jp", :email => "lain@wired.jp",
:fullname => "lain iwakura", :fullname => "lain iwakura",
:bio => "", :bio => "",
@ -87,7 +87,7 @@ test "it sends confirmation email if :account_activation_required is specified i
test "it registers a new user and parses mentions in the bio" do test "it registers a new user and parses mentions in the bio" do
data1 = %{ data1 = %{
:nickname => "john", :username => "john",
:email => "john@gmail.com", :email => "john@gmail.com",
:fullname => "John Doe", :fullname => "John Doe",
:bio => "test", :bio => "test",
@ -98,7 +98,7 @@ test "it registers a new user and parses mentions in the bio" do
{:ok, user1} = TwitterAPI.register_user(data1) {:ok, user1} = TwitterAPI.register_user(data1)
data2 = %{ data2 = %{
:nickname => "lain", :username => "lain",
:email => "lain@wired.jp", :email => "lain@wired.jp",
:fullname => "lain iwakura", :fullname => "lain iwakura",
:bio => "@john test", :bio => "@john test",
@ -123,7 +123,7 @@ test "returns user on success" do
{:ok, invite} = UserInviteToken.create_invite() {:ok, invite} = UserInviteToken.create_invite()
data = %{ data = %{
:nickname => "vinny", :username => "vinny",
:email => "pasta@pizza.vs", :email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce", :fullname => "Vinny Vinesauce",
:bio => "streamer", :bio => "streamer",
@ -145,7 +145,7 @@ test "returns user on success" do
test "returns error on invalid token" do test "returns error on invalid token" do
data = %{ data = %{
:nickname => "GrimReaper", :username => "GrimReaper",
:email => "death@reapers.afterlife", :email => "death@reapers.afterlife",
:fullname => "Reaper Grim", :fullname => "Reaper Grim",
:bio => "Your time has come", :bio => "Your time has come",
@ -165,7 +165,7 @@ test "returns error on expired token" do
UserInviteToken.update_invite!(invite, used: true) UserInviteToken.update_invite!(invite, used: true)
data = %{ data = %{
:nickname => "GrimReaper", :username => "GrimReaper",
:email => "death@reapers.afterlife", :email => "death@reapers.afterlife",
:fullname => "Reaper Grim", :fullname => "Reaper Grim",
:bio => "Your time has come", :bio => "Your time has come",
@ -186,7 +186,7 @@ test "returns error on expired token" do
setup do setup do
data = %{ data = %{
:nickname => "vinny", :username => "vinny",
:email => "pasta@pizza.vs", :email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce", :fullname => "Vinny Vinesauce",
:bio => "streamer", :bio => "streamer",
@ -250,7 +250,7 @@ test "returns user on success, after him registration fails" do
UserInviteToken.update_invite!(invite, uses: 99) UserInviteToken.update_invite!(invite, uses: 99)
data = %{ data = %{
:nickname => "vinny", :username => "vinny",
:email => "pasta@pizza.vs", :email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce", :fullname => "Vinny Vinesauce",
:bio => "streamer", :bio => "streamer",
@ -269,7 +269,7 @@ test "returns user on success, after him registration fails" do
AccountView.render("show.json", %{user: fetched_user}) AccountView.render("show.json", %{user: fetched_user})
data = %{ data = %{
:nickname => "GrimReaper", :username => "GrimReaper",
:email => "death@reapers.afterlife", :email => "death@reapers.afterlife",
:fullname => "Reaper Grim", :fullname => "Reaper Grim",
:bio => "Your time has come", :bio => "Your time has come",
@ -292,7 +292,7 @@ test "returns user on success" do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
data = %{ data = %{
:nickname => "vinny", :username => "vinny",
:email => "pasta@pizza.vs", :email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce", :fullname => "Vinny Vinesauce",
:bio => "streamer", :bio => "streamer",
@ -317,7 +317,7 @@ test "error after max uses" do
UserInviteToken.update_invite!(invite, uses: 99) UserInviteToken.update_invite!(invite, uses: 99)
data = %{ data = %{
:nickname => "vinny", :username => "vinny",
:email => "pasta@pizza.vs", :email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce", :fullname => "Vinny Vinesauce",
:bio => "streamer", :bio => "streamer",
@ -335,7 +335,7 @@ test "error after max uses" do
AccountView.render("show.json", %{user: fetched_user}) AccountView.render("show.json", %{user: fetched_user})
data = %{ data = %{
:nickname => "GrimReaper", :username => "GrimReaper",
:email => "death@reapers.afterlife", :email => "death@reapers.afterlife",
:fullname => "Reaper Grim", :fullname => "Reaper Grim",
:bio => "Your time has come", :bio => "Your time has come",
@ -355,7 +355,7 @@ test "returns error on overdue date" do
UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
data = %{ data = %{
:nickname => "GrimReaper", :username => "GrimReaper",
:email => "death@reapers.afterlife", :email => "death@reapers.afterlife",
:fullname => "Reaper Grim", :fullname => "Reaper Grim",
:bio => "Your time has come", :bio => "Your time has come",
@ -377,7 +377,7 @@ test "returns error on with overdue date and after max" do
UserInviteToken.update_invite!(invite, uses: 100) UserInviteToken.update_invite!(invite, uses: 100)
data = %{ data = %{
:nickname => "GrimReaper", :username => "GrimReaper",
:email => "death@reapers.afterlife", :email => "death@reapers.afterlife",
:fullname => "Reaper Grim", :fullname => "Reaper Grim",
:bio => "Your time has come", :bio => "Your time has come",
@ -395,16 +395,15 @@ test "returns error on with overdue date and after max" do
test "it returns the error on registration problems" do test "it returns the error on registration problems" do
data = %{ data = %{
:nickname => "lain", :username => "lain",
:email => "lain@wired.jp", :email => "lain@wired.jp",
:fullname => "lain iwakura", :fullname => "lain iwakura",
:bio => "close the world.", :bio => "close the world."
:password => "bear"
} }
{:error, error_object} = TwitterAPI.register_user(data) {:error, error} = TwitterAPI.register_user(data)
assert is_binary(error_object[:error]) assert is_binary(error)
refute User.get_cached_by_nickname("lain") refute User.get_cached_by_nickname("lain")
end end