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.
### Added
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` 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.
- 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.
- 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
<details>
<summary>API Changes</summary>
- Mastodon API: Extended `/api/v1/instance`.
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- 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
## [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
- Logger configuration through AdminFE
- HTTP Basic Authentication permissions issue

View File

@ -562,8 +562,6 @@
inactivity_threshold: 7
}
config :pleroma, :notifications, enable_follow_request_notifications: false
config :pleroma, :oauth2,
token_expires_in: 600,
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,
key: Pleroma.Emails.UserEmail,

View File

@ -202,8 +202,22 @@ Has theses additional parameters (which are the same as in Pleroma-API):
- `bio`: optional
- `captcha_solution`: optional, contains provider-specific captcha solution,
- `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.
## 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,4 +20,9 @@ defmodule Pleroma.Constants do
"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

View File

@ -311,17 +311,8 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
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)
when type in ["Like", "Announce", "Move", "EmojiReact"] do
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
do_create_notifications(activity)
end

View File

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.InstanceStatic do
require Pleroma.Constants
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration.
@ -21,9 +23,6 @@ def file_path(path) do
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
opts
|> Keyword.put(:from, "__unconfigured_instance_static_plug")
@ -31,7 +30,7 @@ def init(opts) do
|> Plug.Static.init()
end
for only <- @only do
for only <- Pleroma.Constants.static_only_files() do
at = Plug.Router.Utils.split("/")
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
defp key_id_from_conn(conn) do
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn) do
Signature.key_id_to_actor_id(key_id)
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
ap_id
else
_ ->
nil

View File

@ -8,6 +8,7 @@ defmodule Pleroma.Signature do
alias Pleroma.Keys
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
def key_id_to_actor_id(key_id) do
uri =
@ -21,12 +22,23 @@ def key_id_to_actor_id(key_id) do
uri
end
URI.to_string(uri)
maybe_ap_id = URI.to_string(uri)
case Types.ObjectID.cast(maybe_ap_id) do
{:ok, ap_id} ->
{:ok, ap_id}
_ ->
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
%{"ap_id" => ap_id} -> {:ok, ap_id}
_ -> {:error, maybe_ap_id}
end
end
end
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_id(kid),
{:ok, actor_id} <- key_id_to_actor_id(kid),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
@ -37,7 +49,7 @@ def fetch_public_key(conn) do
def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_id(kid),
{:ok, actor_id} <- key_id_to_actor_id(kid),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}

View File

@ -1445,8 +1445,15 @@ def perform(:delete, %User{} = user) do
end)
delete_user_activities(user)
invalidate_cache(user)
Repo.delete(user)
if user.local do
user
|> change(%{deactivated: true, email: nil})
|> update_and_set_cache()
else
invalidate_cache(user)
Repo.delete(user)
end
end
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
)
# Note: :following and :followers must be served even without authentication (as via :api)
plug(
EnsureAuthenticatedPlug
when action in [:read_inbox, :update_outbox, :whoami, :upload_media, :following, :followers]
when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
)
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.
"""
alias Pleroma.Activity
alias Pleroma.EarmarkRenderer
alias Pleroma.FollowingRelationship
alias Pleroma.Object
alias Pleroma.Object.Containment
@ -43,6 +44,7 @@ def fix_object(object, options \\ []) do
|> fix_addressing
|> fix_summary
|> fix_type(options)
|> fix_content
end
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
defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
when is_binary(content) do
html_content =
content
|> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
|> Pleroma.HTML.filter_tags()
Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
end
defp fix_content(object), do: object
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
@ -1207,18 +1221,24 @@ def add_attributed_to(object) do
def prepare_attachments(object) do
attachments =
(object["attachment"] || [])
object
|> Map.get("attachment", [])
|> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} | _] = data["url"]
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
%{
"url" => href,
"mediaType" => media_type,
"name" => data["name"],
"type" => "Document"
}
end)
Map.put(object, "attachment", attachments)
end
def strip_internal_fields(object) do
object
|> Map.drop(Pleroma.Constants.object_internal_fields())
Map.drop(object, Pleroma.Constants.object_internal_fields())
end
defp strip_internal_tags(%{"tag" => tags} = object) do

View File

@ -41,9 +41,17 @@ def pagination_params do
Operation.parameter(
:limit,
:query,
%Schema{type: :integer, default: 20, maximum: 40},
"Limit"
%Schema{type: :integer, default: 20},
"Maximum number of items to return. Will be ignored if it's more than 40"
)
]
end
def empty_object_response do
Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}})
end
def empty_array_response do
Operation.response("Empty array", "application/json", %Schema{type: :array, example: []})
end
end

View File

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

View File

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

View File

@ -0,0 +1,211 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.NotificationOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Notifications"],
summary: "Get all notifications",
description:
"Notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values.",
operationId: "NotificationController.index",
security: [%{"oAuth" => ["read:notifications"]}],
parameters:
[
Operation.parameter(
:exclude_types,
:query,
%Schema{type: :array, items: notification_type()},
"Array of types to exclude"
),
Operation.parameter(
:account_id,
:query,
%Schema{type: :string},
"Return only notifications received from this account"
),
Operation.parameter(
:exclude_visibilities,
:query,
%Schema{type: :array, items: VisibilityScope},
"Exclude the notifications for activities with the given visibilities"
),
Operation.parameter(
:include_types,
:query,
%Schema{type: :array, items: notification_type()},
"Include the notifications for activities with the given types"
),
Operation.parameter(
:with_muted,
:query,
BooleanLike,
"Include the notifications from muted users"
)
] ++ pagination_params(),
responses: %{
200 =>
Operation.response("Array of notifications", "application/json", %Schema{
type: :array,
items: notification()
}),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def show_operation do
%Operation{
tags: ["Notifications"],
summary: "Get a single notification",
description: "View information about a notification with a given ID.",
operationId: "NotificationController.show",
security: [%{"oAuth" => ["read:notifications"]}],
parameters: [id_param()],
responses: %{
200 => Operation.response("Notification", "application/json", notification())
}
}
end
def clear_operation do
%Operation{
tags: ["Notifications"],
summary: "Dismiss all notifications",
description: "Clear all notifications from the server.",
operationId: "NotificationController.clear",
security: [%{"oAuth" => ["write:notifications"]}],
responses: %{200 => empty_object_response()}
}
end
def dismiss_operation do
%Operation{
tags: ["Notifications"],
summary: "Dismiss a single notification",
description: "Clear a single notification from the server.",
operationId: "NotificationController.dismiss",
parameters: [id_param()],
security: [%{"oAuth" => ["write:notifications"]}],
responses: %{200 => empty_object_response()}
}
end
def dismiss_via_body_operation do
%Operation{
tags: ["Notifications"],
summary: "Dismiss a single notification",
deprecated: true,
description: "Clear a single notification from the server.",
operationId: "NotificationController.dismiss_via_body",
requestBody:
request_body(
"Parameters",
%Schema{type: :object, properties: %{id: %Schema{type: :string}}},
required: true
),
security: [%{"oAuth" => ["write:notifications"]}],
responses: %{200 => empty_object_response()}
}
end
def destroy_multiple_operation do
%Operation{
tags: ["Notifications"],
summary: "Dismiss multiple notifications",
operationId: "NotificationController.destroy_multiple",
security: [%{"oAuth" => ["write:notifications"]}],
parameters: [
Operation.parameter(
:ids,
:query,
%Schema{type: :array, items: %Schema{type: :string}},
"Array of notification IDs to dismiss",
required: true
)
],
responses: %{200 => empty_object_response()}
}
end
defp notification do
%Schema{
title: "Notification",
description: "Response schema for a notification",
type: :object,
properties: %{
id: %Schema{type: :string},
type: notification_type(),
created_at: %Schema{type: :string, format: :"date-time"},
account: %Schema{
allOf: [Account],
description: "The account that performed the action that generated the notification."
},
status: %Schema{
allOf: [Status],
description:
"Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.",
nullable: true
}
},
example: %{
"id" => "34975861",
"type" => "mention",
"created_at" => "2019-11-23T07:49:02.064Z",
"account" => Account.schema().example,
"status" => Status.schema().example
}
}
end
defp notification_type do
%Schema{
type: :string,
enum: [
"follow",
"favourite",
"reblog",
"mention",
"poll",
"pleroma:emoji_reaction",
"move",
"follow_request"
],
description: """
The type of event that resulted in the notification.
- `follow` - Someone followed you
- `mention` - Someone mentioned you in their status
- `reblog` - Someone boosted one of your statuses
- `favourite` - Someone favourited one of your statuses
- `poll` - A poll you have voted in or created has ended
- `move` - Someone moved their account
- `pleroma:emoji_reaction` - Someone reacted with emoji to your status
"""
}
end
defp id_param do
Operation.parameter(:id, :path, :string, "Notification ID",
example: "123",
required: true
)
end
end

View File

@ -0,0 +1,78 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.ReportOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.ApiError
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def create_operation do
%Operation{
tags: ["reports"],
summary: "File a report",
description: "Report problematic users to your moderators",
operationId: "ReportController.create",
security: [%{"oAuth" => ["follow", "write:reports"]}],
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
responses: %{
200 => Operation.response("Report", "application/json", create_response()),
400 => Operation.response("Report", "application/json", ApiError)
}
}
end
defp create_request do
%Schema{
title: "ReportCreateRequest",
description: "POST body for creating a report",
type: :object,
properties: %{
account_id: %Schema{type: :string, description: "ID of the account to report"},
status_ids: %Schema{
type: :array,
items: %Schema{type: :string},
description: "Array of Statuses to attach to the report, for context"
},
comment: %Schema{
type: :string,
description: "Reason for the report"
},
forward: %Schema{
type: :boolean,
default: false,
description:
"If the account is remote, should the report be forwarded to the remote admin?"
}
},
required: [:account_id],
example: %{
"account_id" => "123",
"status_ids" => ["1337"],
"comment" => "bad status!",
"forward" => "false"
}
}
end
defp create_response do
%Schema{
title: "ReportResponse",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Report ID"},
action_taken: %Schema{type: :boolean, description: "Is action taken?"}
},
example: %{
"id" => "123",
"action_taken" => false
}
}
end
end

View File

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

View File

@ -504,7 +504,8 @@ def make_report_content_html(comment) do
end
end
def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
def get_report_statuses(%User{ap_id: actor}, %{status_ids: status_ids})
when is_list(status_ids) do
{:ok, Activity.all_by_actor_and_id(actor, status_ids)}
end

View File

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

View File

@ -94,24 +94,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts"
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
params =
params
|> Map.take([
:email,
:bio,
:captcha_solution,
:captcha_token,
:captcha_answer_data,
:token,
:password,
:fullname
])
|> Map.put(:nickname, params.username)
|> Map.put(:fullname, Map.get(params, :fullname, params.username))
|> Map.put(:confirm, params.password)
|> Map.put(:trusted_app, app.trusted)
with :ok <- validate_email_param(params),
:ok <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
json(conn, %{
@ -121,7 +105,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
created_at: Token.Utils.format_created_at(token)
})
else
{:error, errors} -> json_response(conn, :bad_request, errors)
{:error, error} -> json_response(conn, :bad_request, %{error: error})
end
end
@ -133,11 +117,11 @@ def create(conn, _) do
render_error(conn, :forbidden, "Invalid credentials")
end
defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
defp validate_email_param(_) do
case Pleroma.Config.get([:instance, :account_activation_required]) do
true -> {:error, %{"error" => "Missing parameters"}}
true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
_ -> :ok
end
end

View File

@ -13,6 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
@oauth_read_actions [:show, :index]
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
plug(
OAuthScopesPlug,
%{scopes: ["read:notifications"]} when action in @oauth_read_actions
@ -20,14 +22,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.NotificationOperation
# GET /api/v1/notifications
def index(conn, %{"account_id" => account_id} = params) do
def index(conn, %{account_id: account_id} = params) do
case Pleroma.User.get_cached_by_id(account_id) do
%{ap_id: account_ap_id} ->
params =
params
|> Map.delete("account_id")
|> Map.put("account_ap_id", account_ap_id)
|> Map.delete(:account_id)
|> Map.put(:account_ap_id, account_ap_id)
index(conn, params)
@ -39,6 +43,7 @@ def index(conn, %{"account_id" => account_id} = params) do
end
def index(%{assigns: %{user: user}} = conn, params) do
params = Map.new(params, fn {k, v} -> {to_string(k), v} end)
notifications = MastodonAPI.get_notifications(user, params)
conn
@ -51,7 +56,7 @@ def index(%{assigns: %{user: user}} = conn, params) do
end
# GET /api/v1/notifications/:id
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with {:ok, notification} <- Notification.get(user, id) do
render(conn, "show.json", notification: notification, for: user)
else
@ -69,8 +74,8 @@ def clear(%{assigns: %{user: user}} = conn, _params) do
end
# POST /api/v1/notifications/:id/dismiss
# POST /api/v1/notifications/dismiss (deprecated)
def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
def dismiss(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
@ -81,8 +86,13 @@ def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
end
end
# POST /api/v1/notifications/dismiss (deprecated)
def dismiss_via_body(%{body_params: params} = conn, _) do
dismiss(conn, params)
end
# DELETE /api/v1/notifications/destroy_multiple
def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
def destroy_multiple(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end

View File

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

View File

@ -5,10 +5,13 @@
defmodule Pleroma.Web.MastodonAPI.InstanceView do
use Pleroma.Web, :view
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
@mastodon_api_level "2.7.2"
def render("show.json", _) do
instance = Pleroma.Config.get(:instance)
instance = Config.get(:instance)
%{
uri: Pleroma.Web.base_url(),
@ -29,7 +32,58 @@ def render("show.json", _) do
upload_limit: Keyword.get(instance, :upload_limit),
avatar_upload_limit: Keyword.get(instance, :avatar_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
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

View File

@ -9,8 +9,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.MastodonAPI.InstanceView
def schemas(conn, _params) do
response = %{
@ -34,51 +34,12 @@ def schemas(conn, _params) do
def raw_nodeinfo do
stats = Stats.get_stats()
quarantined = Config.get([:instance, :quarantined_instances], [])
staff_accounts =
User.all_superusers()
|> Enum.map(fn u -> u.ap_id end)
federation_response =
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]))
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)
features = InstanceView.features()
federation = InstanceView.federation()
%{
version: "2.0",
@ -106,7 +67,7 @@ def raw_nodeinfo do
enabled: false
},
staffAccounts: staff_accounts,
federation: federation_response,
federation: federation,
pollLimits: Config.get([:instance, :poll_limits]),
postFormats: Config.get([:instance, :allowed_post_formats]),
uploadLimits: %{

View File

@ -61,7 +61,10 @@ def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}
else
users =
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,

View File

@ -396,7 +396,7 @@ defmodule Pleroma.Web.Router do
post("/notifications/clear", NotificationController, :clear)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
post("/notifications/dismiss", NotificationController, :dismiss)
post("/notifications/dismiss", NotificationController, :dismiss_via_body)
post("/polls/:id/votes", PollController, :vote)
@ -585,6 +585,7 @@ defmodule Pleroma.Web.Router do
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
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/following", ActivityPubController, :following)
end

View File

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

View File

@ -3,54 +3,27 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
import Pleroma.Web.Gettext
alias Pleroma.Emails.Mailer
alias Pleroma.Emails.UserEmail
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserInviteToken
require Pleroma.Constants
def register_user(params, opts \\ []) do
params =
params
|> Map.take([
:nickname,
:password,
:captcha_solution,
:captcha_token,
:captcha_answer_data,
:token,
:email,
:trusted_app
])
|> Map.put(:bio, User.parse_bio(params[:bio] || ""))
|> Map.put(:name, params.fullname)
|> Map.put(:password_confirmation, params[:confirm])
|> Map.take([:email, :token, :password])
|> Map.put(:bio, params |> Map.get(:bio, "") |> User.parse_bio())
|> Map.put(:nickname, params[:username])
|> Map.put(:name, Map.get(params, :fullname, params[:username]))
|> Map.put(:password_confirmation, params[:password])
case validate_captcha(params) do
:ok ->
if Pleroma.Config.get([:instance, :registrations_open]) do
create_user(params, opts)
else
create_user_with_invite(params, opts)
end
{:error, error} ->
# I have no idea how this error handling works
{:error, %{error: Jason.encode!(%{captcha: [error]})}}
end
end
defp validate_captcha(params) do
if params[:trusted_app] || not Pleroma.Config.get([Pleroma.Captcha, :enabled]) do
:ok
if Pleroma.Config.get([:instance, :registrations_open]) do
create_user(params, opts)
else
Pleroma.Captcha.validate(
params.captcha_token,
params.captcha_solution,
params.captcha_answer_data
)
create_user_with_invite(params, opts)
end
end
@ -75,16 +48,17 @@ defp create_user(params, opts) do
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
changeset
|> Ecto.Changeset.traverse_errors(fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, %{error: errors}}
{:error, errors}
end
end
def password_reset(nickname_or_email) do
with true <- is_binary(nickname_or_email),
%User{local: true, email: email} = user when not is_nil(email) <-
%User{local: true, email: email} = user when is_binary(email) <-
User.get_by_nickname_or_email(nickname_or_email),
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user
@ -106,4 +80,58 @@ def password_reset(nickname_or_email) do
{:error, "unknown user"}
end
end
def validate_captcha(app, params) do
if app.trusted || not Pleroma.Captcha.enabled?() do
:ok
else
do_validate_captcha(params)
end
end
defp do_validate_captcha(params) do
with :ok <- validate_captcha_presence(params),
:ok <-
Pleroma.Captcha.validate(
params[:captcha_token],
params[:captcha_solution],
params[:captcha_answer_data]
) do
:ok
else
{:error, :captcha_error} ->
captcha_error(dgettext("errors", "CAPTCHA Error"))
{:error, :invalid} ->
captcha_error(dgettext("errors", "Invalid CAPTCHA"))
{:error, :kocaptcha_service_unavailable} ->
captcha_error(dgettext("errors", "Kocaptcha service unavailable"))
{:error, :expired} ->
captcha_error(dgettext("errors", "CAPTCHA expired"))
{:error, :already_used} ->
captcha_error(dgettext("errors", "CAPTCHA already used"))
{:error, :invalid_answer_data} ->
captcha_error(dgettext("errors", "Invalid answer data"))
{:error, error} ->
captcha_error(error)
end
end
defp validate_captcha_presence(params) do
[:captcha_solution, :captcha_token, :captcha_answer_data]
|> Enum.find_value(:ok, fn key ->
unless is_binary(params[key]) do
error = dgettext("errors", "Invalid CAPTCHA (Missing parameter: %{name})", name: key)
{:error, error}
end
end)
end
# For some reason FE expects error message to be a serialized JSON
defp captcha_error(error), do: {:error, Jason.encode!(%{captcha: [error]})}
end

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="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="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",
"code": 59427,
"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 :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
@ -78,6 +78,7 @@ test "validate" do
assert is_binary(answer)
assert :ok = Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer)
Cachex.del(:used_captcha_cache, token)
end
test "doesn't validate invalid answer" do
@ -92,7 +93,7 @@ test "doesn't validate invalid answer" do
assert is_binary(answer)
assert {:error, "Invalid answer data"} =
assert {:error, :invalid_answer_data} =
Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer <> "foobar")
end
@ -108,7 +109,7 @@ test "nil answer_data" do
assert is_binary(answer)
assert {:error, "Invalid answer data"} =
assert {:error, :invalid_answer_data} =
Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", nil)
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
test "if `follow_request` notifications are enabled, " <>
"it creates `follow_request` notification for pending Follow activity" do
clear_config([:notifications, :enable_follow_request_notifications], true)
test "it creates `follow_request` notification for pending Follow activity" do
user = insert(:user)
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)
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
user = insert(:user)
followed_user = insert(:user, locked: false)
@ -367,7 +350,6 @@ test "it doesn't create a notification for follow-unfollow-follow chains" do
end
test "dismisses the notification on follow request rejection" do
clear_config([:notifications, :enable_follow_request_notifications], true)
user = insert(:user, locked: true)
follower = insert(: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
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
@ -64,7 +65,7 @@ test "it returns key" do
test "it returns error when not found user" do
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
end
@ -100,12 +101,21 @@ test "it returns error" do
describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do
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
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") ==
"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

View File

@ -6,12 +6,16 @@ defmodule Pleroma.Captcha.Mock do
alias Pleroma.Captcha.Service
@behaviour Service
@solution "63615261b77f5354fb8c4e4986477555"
def solution, do: @solution
@impl Service
def new,
do: %{
type: :mock,
token: "afa1815e14e29355e6c8f6b143a39fa2",
answer_data: "63615261b77f5354fb8c4e4986477555",
answer_data: @solution,
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
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", _, _, [
{"accept", "application/activity+json"}
]) do

View File

@ -92,7 +92,7 @@ test "user is deleted" do
assert_received {:mix_shell, :info, [message]}
assert message =~ " deleted"
refute User.get_by_nickname(user.nickname)
assert %{deactivated: true} = User.get_by_nickname(user.nickname)
end
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)
end
test "it deletes deactivated 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
test "it deactivates a user, all follow relationships and all activities", %{user: user} do
follower = insert(: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)
refute User.following?(follower, user)
refute User.get_by_id(user.id)
assert {:ok, nil} == Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
assert %{deactivated: true} = User.get_by_id(user.id)
user_activities =
user.ap_id

View File

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

View File

@ -872,7 +872,8 @@ test "it fails for incoming deletes with spoofed origin" do
@tag capture_log: true
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 =
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)
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
{:ok, object} =
Fetcher.fetch_object_from_id(

View File

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

View File

@ -15,7 +15,7 @@ test "renders a report" do
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 = %{
content: nil,
@ -48,7 +48,7 @@ test "includes reported statuses" do
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "toot"})
{: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)
@ -81,7 +81,7 @@ test "renders report's state" do
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")
assert %{state: "closed"} =
@ -94,8 +94,8 @@ test "renders report description" do
{:ok, activity} =
CommonAPI.report(user, %{
"account_id" => other_user.id,
"comment" => "posts are too good for this instance"
account_id: other_user.id,
comment: "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} =
CommonAPI.report(user, %{
"account_id" => other_user.id,
"comment" => ""
account_id: other_user.id,
comment: ""
})
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} =
CommonAPI.report(user, %{
"account_id" => other_user.id,
"comment" => ""
account_id: other_user.id,
comment: ""
})
Pleroma.User.delete(other_user)

View File

@ -485,9 +485,9 @@ test "creates a report" do
comment = "foobar"
report_data = %{
"account_id" => target_user.id,
"comment" => comment,
"status_ids" => [activity.id]
account_id: target_user.id,
comment: comment,
status_ids: [activity.id]
}
note_obj = %{
@ -517,9 +517,9 @@ test "updates report state" do
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{: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}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
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}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, %Activity{id: second_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel very offended!",
"status_ids" => [activity.id]
account_id: target_user.id,
comment: "I feel very offended!",
status_ids: [activity.id]
})
{: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_three.id).data["state"] == "pending"
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
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})
|> 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 =
conn
@ -1093,6 +1094,91 @@ test "respects rate limit setting", %{conn: conn} do
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
test "returns lists to which the account belongs" do
%{user: user, conn: conn} = oauth_access(["read:lists"])

View File

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

View File

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

View File

@ -22,8 +22,9 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
test "submit a basic report", %{conn: conn, target_user: target_user} do
assert %{"action_taken" => false, "id" => _} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"account_id" => target_user.id})
|> json_response(200)
|> json_response_and_validate_schema(200)
end
test "submit a report with statuses and comment", %{
@ -33,23 +34,25 @@ test "submit a report with statuses and comment", %{
} do
assert %{"action_taken" => false, "id" => _} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{
"account_id" => target_user.id,
"status_ids" => [activity.id],
"comment" => "bad status!",
"forward" => "false"
})
|> json_response(200)
|> json_response_and_validate_schema(200)
end
test "account_id is required", %{
conn: conn,
activity: activity
} do
assert %{"error" => "Valid `account_id` required"} =
assert %{"error" => "Missing field: account_id."} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"status_ids" => [activity.id]})
|> json_response(400)
|> json_response_and_validate_schema(400)
end
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 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment})
|> json_response(400)
|> json_response_and_validate_schema(400)
end
test "returns error when account is not exist", %{
conn: conn,
activity: activity
} 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
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" => _} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"account_id" => target_user.id})
|> json_response(200)
|> json_response_and_validate_schema(200)
end
end

View File

@ -5,8 +5,6 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Config
setup do: oauth_access(["read"])
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
data = %{
:nickname => "lain",
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
: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
data = %{
:nickname => "lain",
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "",
@ -60,7 +60,7 @@ test "it sends confirmation email if :account_activation_required is specified i
end
data = %{
:nickname => "lain",
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
: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
data1 = %{
:nickname => "john",
:username => "john",
:email => "john@gmail.com",
:fullname => "John Doe",
: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)
data2 = %{
:nickname => "lain",
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "@john test",
@ -123,7 +123,7 @@ test "returns user on success" do
{:ok, invite} = UserInviteToken.create_invite()
data = %{
:nickname => "vinny",
:username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
@ -145,7 +145,7 @@ test "returns user on success" do
test "returns error on invalid token" do
data = %{
:nickname => "GrimReaper",
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
@ -165,7 +165,7 @@ test "returns error on expired token" do
UserInviteToken.update_invite!(invite, used: true)
data = %{
:nickname => "GrimReaper",
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
@ -186,7 +186,7 @@ test "returns error on expired token" do
setup do
data = %{
:nickname => "vinny",
:username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
@ -250,7 +250,7 @@ test "returns user on success, after him registration fails" do
UserInviteToken.update_invite!(invite, uses: 99)
data = %{
:nickname => "vinny",
:username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
@ -269,7 +269,7 @@ test "returns user on success, after him registration fails" do
AccountView.render("show.json", %{user: fetched_user})
data = %{
:nickname => "GrimReaper",
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
: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})
data = %{
:nickname => "vinny",
:username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
@ -317,7 +317,7 @@ test "error after max uses" do
UserInviteToken.update_invite!(invite, uses: 99)
data = %{
:nickname => "vinny",
:username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
@ -335,7 +335,7 @@ test "error after max uses" do
AccountView.render("show.json", %{user: fetched_user})
data = %{
:nickname => "GrimReaper",
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
: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})
data = %{
:nickname => "GrimReaper",
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
: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)
data = %{
:nickname => "GrimReaper",
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
: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
data = %{
:nickname => "lain",
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "close the world.",
:password => "bear"
:bio => "close the world."
}
{: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")
end