Browse Source

Merge remote-tracking branch 'remotes/origin/develop' into automatic-authentication-and-instance-publicity-checks

# Conflicts:
#	lib/pleroma/web/mastodon_api/controllers/account_controller.ex
pleroma-fe-2020-05-01-c67e9daf
Ivan Tashkinov 4 years ago
parent
commit
908cf22a6c
67 changed files with 3297 additions and 1013 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +2
    -2
      benchmarks/load_testing/activities.ex
  3. +55
    -0
      benchmarks/load_testing/fetcher.ex
  4. +1
    -0
      benchmarks/mix/tasks/pleroma/load_testing.ex
  5. +14
    -1
      docs/API/differences_in_mastoapi_responses.md
  6. +1
    -1
      docs/configuration/hardening.md
  7. +1
    -1
      lib/pleroma/plugs/http_security_plug.ex
  8. +13
    -5
      lib/pleroma/stats.ex
  9. +14
    -0
      lib/pleroma/user.ex
  10. +2
    -2
      lib/pleroma/user/query.ex
  11. +47
    -36
      lib/pleroma/web/activity_pub/activity_pub.ex
  12. +6
    -1
      lib/pleroma/web/activity_pub/activity_pub_controller.ex
  13. +9
    -4
      lib/pleroma/web/activity_pub/side_effects.ex
  14. +3
    -15
      lib/pleroma/web/admin_api/views/status_view.ex
  15. +8
    -0
      lib/pleroma/web/api_spec.ex
  16. +22
    -0
      lib/pleroma/web/api_spec/helpers.ex
  17. +701
    -0
      lib/pleroma/web/api_spec/operations/account_operation.ex
  18. +57
    -9
      lib/pleroma/web/api_spec/operations/app_operation.ex
  19. +65
    -2
      lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
  20. +26
    -5
      lib/pleroma/web/api_spec/operations/domain_block_operation.ex
  21. +231
    -0
      lib/pleroma/web/api_spec/render_error.ex
  22. +167
    -0
      lib/pleroma/web/api_spec/schemas/account.ex
  23. +26
    -0
      lib/pleroma/web/api_spec/schemas/account_field.ex
  24. +44
    -0
      lib/pleroma/web/api_spec/schemas/account_relationship.ex
  25. +13
    -0
      lib/pleroma/web/api_spec/schemas/actor_type.ex
  26. +6
    -7
      lib/pleroma/web/api_spec/schemas/api_error.ex
  27. +0
    -33
      lib/pleroma/web/api_spec/schemas/app_create_request.ex
  28. +0
    -33
      lib/pleroma/web/api_spec/schemas/app_create_response.ex
  29. +36
    -0
      lib/pleroma/web/api_spec/schemas/boolean_like.ex
  30. +0
    -30
      lib/pleroma/web/api_spec/schemas/custom_emoji.ex
  31. +0
    -42
      lib/pleroma/web/api_spec/schemas/custom_emojis_response.ex
  32. +0
    -16
      lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex
  33. +29
    -0
      lib/pleroma/web/api_spec/schemas/emoji.ex
  34. +14
    -0
      lib/pleroma/web/api_spec/schemas/flake_id.ex
  35. +36
    -0
      lib/pleroma/web/api_spec/schemas/poll.ex
  36. +226
    -0
      lib/pleroma/web/api_spec/schemas/status.ex
  37. +14
    -0
      lib/pleroma/web/api_spec/schemas/visibility_scope.ex
  38. +10
    -6
      lib/pleroma/web/common_api/activity_draft.ex
  39. +13
    -10
      lib/pleroma/web/common_api/common_api.ex
  40. +0
    -18
      lib/pleroma/web/common_api/utils.ex
  41. +3
    -2
      lib/pleroma/web/controller_helper.ex
  42. +63
    -41
      lib/pleroma/web/mastodon_api/controllers/account_controller.ex
  43. +2
    -1
      lib/pleroma/web/mastodon_api/controllers/status_controller.ex
  44. +2
    -0
      lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
  45. +14
    -8
      lib/pleroma/web/mastodon_api/views/status_view.ex
  46. +2
    -2
      lib/pleroma/web/mongooseim/mongoose_im_controller.ex
  47. +1
    -5
      lib/pleroma/web/oauth/scopes.ex
  48. +46
    -62
      lib/pleroma/web/twitter_api/twitter_api.ex
  49. +3
    -1
      mix.exs
  50. +1
    -1
      mix.lock
  51. +11
    -1
      test/stats_test.exs
  52. +57
    -0
      test/support/api_spec_helpers.ex
  53. +54
    -0
      test/support/conn_case.ex
  54. +500
    -70
      test/web/activity_pub/activity_pub_test.exs
  55. +2
    -3
      test/web/activity_pub/utils_test.exs
  56. +0
    -45
      test/web/api_spec/app_operation_test.exs
  57. +43
    -0
      test/web/api_spec/schema_examples_test.exs
  58. +69
    -3
      test/web/common_api/common_api_test.exs
  59. +0
    -20
      test/web/common_api/common_api_utils_test.exs
  60. +34
    -32
      test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
  61. +323
    -284
      test/web/mastodon_api/controllers/account_controller_test.exs
  62. +2
    -2
      test/web/mastodon_api/controllers/app_controller_test.exs
  63. +1
    -19
      test/web/mastodon_api/controllers/custom_emoji_controller_test.exs
  64. +7
    -21
      test/web/mastodon_api/controllers/domain_block_controller_test.exs
  65. +11
    -0
      test/web/mastodon_api/controllers/status_controller_test.exs
  66. +22
    -0
      test/web/mongooseim/mongoose_im_controller_test.exs
  67. +111
    -111
      test/web/twitter_api/twitter_api_test.exs

+ 1
- 0
CHANGELOG.md View File

@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
<summary>API Changes</summary>
- 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
- Admin API: endpoints for create/update/delete OAuth Apps.
</details>



+ 2
- 2
benchmarks/load_testing/activities.ex View File

@@ -279,7 +279,7 @@ defmodule Pleroma.LoadTesting.Activities do
actor = get_actor(group, user, friends, non_friends)

with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
{:ok, _activity, _object} <- CommonAPI.favorite(activity_id, actor) do
{:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
:ok
else
{:error, _} ->
@@ -313,7 +313,7 @@ defmodule Pleroma.LoadTesting.Activities do
tasks = get_reply_tasks(visibility, group)

{:ok, activity} =
CommonAPI.post(user, %{"status" => "Simple status", "visibility" => "unlisted"})
CommonAPI.post(user, %{"status" => "Simple status", "visibility" => visibility})

acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
insert_replies(tasks, visibility, user, friends, non_friends, acc)


+ 55
- 0
benchmarks/load_testing/fetcher.ex View File

@@ -41,6 +41,7 @@ defmodule Pleroma.LoadTesting.Fetcher do
fetch_notifications(user)
fetch_favourites(user)
fetch_long_thread(user)
fetch_timelines_with_reply_filtering(user)
end

defp render_views(user) do
@@ -495,4 +496,58 @@ defmodule Pleroma.LoadTesting.Fetcher do
formatters: formatters()
)
end

defp fetch_timelines_with_reply_filtering(user) do
public_params = opts_for_public_timeline(user)

Benchee.run(
%{
"Public timeline without reply filtering" => fn ->
ActivityPub.fetch_public_activities(public_params)
end,
"Public timeline with reply filtering - following" => fn ->
public_params
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
end,
"Public timeline with reply filtering - self" => fn ->
public_params
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
end
},
formatters: formatters()
)

private_params = opts_for_home_timeline(user)

recipients = [user.ap_id | User.following(user)]

Benchee.run(
%{
"Home timeline without reply filtering" => fn ->
ActivityPub.fetch_activities(recipients, private_params)
end,
"Home timeline with reply filtering - following" => fn ->
private_params =
private_params
|> Map.put("reply_filtering_user", user)
|> Map.put("reply_visibility", "following")

ActivityPub.fetch_activities(recipients, private_params)
end,
"Home timeline with reply filtering - self" => fn ->
private_params =
private_params
|> Map.put("reply_filtering_user", user)
|> Map.put("reply_visibility", "self")

ActivityPub.fetch_activities(recipients, private_params)
end
},
formatters: formatters()
)
end
end

+ 1
- 0
benchmarks/mix/tasks/pleroma/load_testing.ex View File

@@ -44,6 +44,7 @@ defmodule Mix.Tasks.Pleroma.LoadTesting do
]

def run(args) do
Logger.configure(level: :error)
Mix.Pleroma.start_pleroma()
clean_tables()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)


+ 14
- 1
docs/API/differences_in_mastoapi_responses.md View File

@@ -4,7 +4,7 @@ A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma

## Flake IDs

Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are sortable strings
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings

## Attachment cap

@@ -14,6 +14,7 @@ Some apps operate under the assumption that no more than 4 attachments can be re

Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
Adding the parameter `reply_visibility` to the public and home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you.

## Statuses

@@ -119,6 +120,18 @@ Accepts additional parameters:
- `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
- `include_types`: will include the notifications for activities with the given types. The parameter accepts an array of types (`mention`, `follow`, `reblog`, `favourite`, `move`, `pleroma:emoji_reaction`). Usage example: `GET /api/v1/notifications?include_types[]=mention&include_types[]=reblog`.

## DELETE `/api/v1/notifications/destroy_multiple`

An endpoint to delete multiple statuses by IDs.

Required parameters:

- `ids`: array of activity ids

Usage example: `DELETE /api/v1/notifications/destroy_multiple/?ids[]=1&ids[]=2`.

Returns on success: 200 OK `{}`

## POST `/api/v1/statuses`

Additional parameters can be added to the JSON body/Form data:


+ 1
- 1
docs/configuration/hardening.md View File

@@ -36,7 +36,7 @@ content-security-policy:
default-src 'none';
base-uri 'self';
frame-ancestors 'none';
img-src 'self' data: https:;
img-src 'self' data: blob: https:;
media-src 'self' https:;
style-src 'self' 'unsafe-inline';
font-src 'self';


+ 1
- 1
lib/pleroma/plugs/http_security_plug.ex View File

@@ -75,7 +75,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
"default-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
"img-src 'self' data: https:",
"img-src 'self' data: blob: https:",
"media-src 'self' https:",
"style-src 'self' 'unsafe-inline'",
"font-src 'self'",


+ 13
- 5
lib/pleroma/stats.ex View File

@@ -45,11 +45,11 @@ defmodule Pleroma.Stats do
end

def init(_args) do
{:ok, get_stat_data()}
{:ok, calculate_stat_data()}
end

def handle_call(:force_update, _from, _state) do
new_stats = get_stat_data()
new_stats = calculate_stat_data()
{:reply, new_stats, new_stats}
end

@@ -58,12 +58,12 @@ defmodule Pleroma.Stats do
end

def handle_cast(:run_update, _state) do
new_stats = get_stat_data()
new_stats = calculate_stat_data()

{:noreply, new_stats}
end

defp get_stat_data do
def calculate_stat_data do
peers =
from(
u in User,
@@ -77,7 +77,15 @@ defmodule Pleroma.Stats do

status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count)

user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id)
users_query =
from(u in User,
where: u.deactivated != true,
where: u.local == true,
where: not is_nil(u.nickname),
where: not u.invisible
)

user_count = Repo.aggregate(users_query, :count, :id)

%{
peers: peers,


+ 14
- 0
lib/pleroma/user.ex View File

@@ -832,6 +832,7 @@ defmodule Pleroma.User do
def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
{:ok, user}
end

@@ -847,9 +848,22 @@ defmodule Pleroma.User do
end
end

def get_user_friends_ap_ids(user) do
from(u in User.get_friends_query(user), select: u.ap_id)
|> Repo.all()
end

@spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
def get_cached_user_friends_ap_ids(user) do
Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
get_user_friends_ap_ids(user)
end)
end

def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
end

@spec get_cached_by_ap_id(String.t()) :: User.t() | nil


+ 2
- 2
lib/pleroma/user/query.ex View File

@@ -54,13 +54,13 @@ defmodule Pleroma.User.Query do
select: term(),
limit: pos_integer()
}
| %{}
| map()

@ilike_criteria [:nickname, :name, :query]
@equal_criteria [:email]
@contains_criteria [:ap_id, :nickname]

@spec build(criteria()) :: Query.t()
@spec build(Query.t(), criteria()) :: Query.t()
def build(query \\ base_query(), criteria) do
prepare_query(query, criteria)
end


+ 47
- 36
lib/pleroma/web/activity_pub/activity_pub.ex View File

@@ -398,36 +398,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end

# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
@spec like(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def like(user, object, activity_id \\ nil, local \\ true) do
with {:ok, result} <- Repo.transaction(fn -> do_like(user, object, activity_id, local) end) do
result
end
end

defp do_like(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => _}} = object,
activity_id,
local
) do
with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id),
{:ok, activity} <- insert(like_data, local),
{:ok, object} <- add_like_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
%Activity{} = activity ->
{:ok, activity, object}

{:error, error} ->
Repo.rollback(error)
end
end

@spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
@@ -468,6 +438,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

defp do_announce(user, object, activity_id, local, public) do
with true <- is_announceable?(object, user, public),
object <- Object.get_by_id(object.id),
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
@@ -854,7 +825,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end

defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
when visibility not in @valid_visibilities do
when visibility not in [nil | @valid_visibilities] do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
@@ -1061,7 +1032,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
raise "Can't use the child object without preloading!"
end

defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do
from(
[_activity, object] in query,
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
@@ -1070,16 +1041,51 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

defp restrict_media(query, _), do: query

defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or val == "1" do
defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do
from(
[_activity, object] in query,
where: fragment("?->>'inReplyTo' is null", object.data)
)
end

defp restrict_replies(query, %{
"reply_filtering_user" => user,
"reply_visibility" => "self"
}) do
from(
[activity, object] in query,
where:
fragment(
"?->>'inReplyTo' is null OR ? = ANY(?)",
object.data,
^user.ap_id,
activity.recipients
)
)
end

defp restrict_replies(query, %{
"reply_filtering_user" => user,
"reply_visibility" => "following"
}) do
from(
[activity, object] in query,
where:
fragment(
"?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?",
object.data,
^[user.ap_id | User.get_cached_user_friends_ap_ids(user)],
activity.recipients,
activity.actor,
activity.actor,
^user.ap_id
)
)
end

defp restrict_replies(query, _), do: query

defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end

@@ -1158,7 +1164,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end

defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
# TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only,
# the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2'
# and `restrict_muted/2`

defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids})
when pinned in [true, "true", "1"] do
from(activity in query, where: activity.id in ^ids)
end

@@ -1291,6 +1302,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"])
|> restrict_replies(opts)
|> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> restrict_tag_all(opts)
@@ -1305,7 +1317,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)


+ 6
- 1
lib/pleroma/web/activity_pub/activity_pub_controller.ex View File

@@ -12,8 +12,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Plugs.EnsureAuthenticatedPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
@@ -421,7 +423,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
{:ok, activity, _object} <- ActivityPub.like(user, object) do
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
{:ok, activity}
else
_ -> {:error, dgettext("errors", "Can't like object")}


+ 9
- 4
lib/pleroma/web/activity_pub/side_effects.ex View File

@@ -15,12 +15,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Add like to object
# - Set up notification
def handle(%{data: %{"type" => "Like"}} = object, meta) do
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)
{:ok, result} =
Pleroma.Repo.transaction(fn ->
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)

Notification.create_notifications(object)
Notification.create_notifications(object)

{:ok, object, meta}
{:ok, object, meta}
end)

result
end

# Nothing to do


+ 3
- 15
lib/pleroma/web/admin_api/views/status_view.ex View File

@@ -8,15 +8,16 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
require Pleroma.Constants

alias Pleroma.User
alias Pleroma.Web.MastodonAPI.StatusView

def render("index.json", opts) do
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end

def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
user = get_user(activity.data["actor"])
user = StatusView.get_user(activity.data["actor"])

Pleroma.Web.MastodonAPI.StatusView.render("show.json", opts)
StatusView.render("show.json", opts)
|> Map.merge(%{account: merge_account_views(user)})
end

@@ -26,17 +27,4 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
end

defp merge_account_views(_), do: %{}

defp get_user(ap_id) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
user

user = User.get_by_guessed_nickname(ap_id) ->
user

true ->
User.error_user(ap_id)
end
end
end

+ 8
- 0
lib/pleroma/web/api_spec.ex View File

@@ -4,6 +4,7 @@

defmodule Pleroma.Web.ApiSpec do
alias OpenApiSpex.OpenApi
alias OpenApiSpex.Operation
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router

@@ -24,6 +25,13 @@ defmodule Pleroma.Web.ApiSpec do
# populate the paths from a phoenix router
paths: OpenApiSpex.Paths.from_router(Router),
components: %OpenApiSpex.Components{
parameters: %{
"accountIdOrNickname" =>
Operation.parameter(:id, :path, :string, "Account ID or nickname",
example: "123",
required: true
)
},
securitySchemes: %{
"oAuth" => %OpenApiSpex.SecurityScheme{
type: "oauth2",


+ 22
- 0
lib/pleroma/web/api_spec/helpers.ex View File

@@ -3,6 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ApiSpec.Helpers do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema

def request_body(description, schema_ref, opts \\ []) do
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]

@@ -24,4 +27,23 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
required: opts[:required] || false
}
end

def pagination_params do
[
Operation.parameter(:max_id, :query, :string, "Return items older than this ID"),
Operation.parameter(:min_id, :query, :string, "Return the oldest items newer than this ID"),
Operation.parameter(
:since_id,
:query,
:string,
"Return the newest items newer than this ID"
),
Operation.parameter(
:limit,
:query,
%Schema{type: :integer, default: 20, maximum: 40},
"Limit"
)
]
end
end

+ 701
- 0
lib/pleroma/web/api_spec/operations/account_operation.ex View File

@@ -0,0 +1,701 @@
# 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.AccountOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Reference
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ActorType
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

@spec open_api_operation(atom) :: Operation.t()
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end

@spec create_operation() :: Operation.t()
def create_operation do
%Operation{
tags: ["accounts"],
summary: "Register an account",
description:
"Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.",
operationId: "AccountController.create",
requestBody: request_body("Parameters", create_request(), required: true),
responses: %{
200 => Operation.response("Account", "application/json", create_response()),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError),
429 => Operation.response("Error", "application/json", ApiError)
}
}
end

def verify_credentials_operation do
%Operation{
tags: ["accounts"],
description: "Test to make sure that the user token works.",
summary: "Verify account credentials",
operationId: "AccountController.verify_credentials",
security: [%{"oAuth" => ["read:accounts"]}],
responses: %{
200 => Operation.response("Account", "application/json", Account)
}
}
end

def update_credentials_operation do
%Operation{
tags: ["accounts"],
summary: "Update account credentials",
description: "Update the user's display and preferences.",
operationId: "AccountController.update_credentials",
security: [%{"oAuth" => ["write:accounts"]}],
requestBody: request_body("Parameters", update_creadentials_request(), required: true),
responses: %{
200 => Operation.response("Account", "application/json", Account),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end

def relationships_operation do
%Operation{
tags: ["accounts"],
summary: "Check relationships to other accounts",
operationId: "AccountController.relationships",
description: "Find out whether a given account is followed, blocked, muted, etc.",
security: [%{"oAuth" => ["read:follows"]}],
parameters: [
Operation.parameter(
:id,
:query,
%Schema{
oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}]
},
"Account IDs",
example: "123"
)
],
responses: %{
200 => Operation.response("Account", "application/json", array_of_relationships())
}
}
end

def show_operation do
%Operation{
tags: ["accounts"],
summary: "Account",
operationId: "AccountController.show",
description: "View information about a profile.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Account", "application/json", Account),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end

def statuses_operation do
%Operation{
tags: ["accounts"],
summary: "Statuses",
operationId: "AccountController.statuses",
description:
"Statuses posted to the given account. Public (for public statuses only), or user token + `read:statuses` (for private statuses the user is authorized to see)",
parameters:
[
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"),
Operation.parameter(:tagged, :query, :string, "With tag"),
Operation.parameter(
:only_media,
:query,
BooleanLike,
"Include only statuses with media attached"
),
Operation.parameter(
:with_muted,
:query,
BooleanLike,
"Include statuses from muted acccounts."
),
Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
Operation.parameter(
:exclude_visibilities,
:query,
%Schema{type: :array, items: VisibilityScope},
"Exclude visibilities"
)
] ++ pagination_params(),
responses: %{
200 => Operation.response("Statuses", "application/json", array_of_statuses()),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end

def followers_operation do
%Operation{
tags: ["accounts"],
summary: "Followers",
operationId: "AccountController.followers",
security: [%{"oAuth" => ["read:accounts"]}],
description:
"Accounts which follow the given account, if network is not hidden by the account owner.",
parameters:
[%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
}
end

def following_operation do
%Operation{
tags: ["accounts"],
summary: "Following",
operationId: "AccountController.following",
security: [%{"oAuth" => ["read:accounts"]}],
description:
"Accounts which the given account is following, if network is not hidden by the account owner.",
parameters:
[%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
}
end

def lists_operation do
%Operation{
tags: ["accounts"],
summary: "Lists containing this account",
operationId: "AccountController.lists",
security: [%{"oAuth" => ["read:lists"]}],
description: "User lists that you have added this account to.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{200 => Operation.response("Lists", "application/json", array_of_lists())}
}
end

def follow_operation do
%Operation{
tags: ["accounts"],
summary: "Follow",
operationId: "AccountController.follow",
security: [%{"oAuth" => ["follow", "write:follows"]}],
description: "Follow the given account",
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
Operation.parameter(
:reblogs,
:query,
BooleanLike,
"Receive this account's reblogs in home timeline? Defaults to true."
)
],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end

def unfollow_operation do
%Operation{
tags: ["accounts"],
summary: "Unfollow",
operationId: "AccountController.unfollow",
security: [%{"oAuth" => ["follow", "write:follows"]}],
description: "Unfollow the given account",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end

def mute_operation do
%Operation{
tags: ["accounts"],
summary: "Mute",
operationId: "AccountController.mute",
security: [%{"oAuth" => ["follow", "write:mutes"]}],
requestBody: request_body("Parameters", mute_request()),
description:
"Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).",
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
Operation.parameter(
:notifications,
:query,
%Schema{allOf: [BooleanLike], default: true},
"Mute notifications in addition to statuses? Defaults to `true`."
)
],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end

def unmute_operation do
%Operation{
tags: ["accounts"],
summary: "Unmute",
operationId: "AccountController.unmute",
security: [%{"oAuth" => ["follow", "write:mutes"]}],
description: "Unmute the given account.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end

def block_operation do
%Operation{
tags: ["accounts"],
summary: "Block",
operationId: "AccountController.block",
security: [%{"oAuth" => ["follow", "write:blocks"]}],
description:
"Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end

def unblock_operation do
%Operation{
tags: ["accounts"],
summary: "Unblock",
operationId: "AccountController.unblock",
security: [%{"oAuth" => ["follow", "write:blocks"]}],
description: "Unblock the given account.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end

def follows_operation do
%Operation{
tags: ["accounts"],
summary: "Follows",
operationId: "AccountController.follows",
security: [%{"oAuth" => ["follow", "write:follows"]}],
requestBody: request_body("Parameters", follows_request(), required: true),
responses: %{
200 => Operation.response("Account", "application/json", AccountRelationship),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end

def mutes_operation do
%Operation{
tags: ["accounts"],
summary: "Muted accounts",
operationId: "AccountController.mutes",
description: "Accounts the user has muted.",
security: [%{"oAuth" => ["follow", "read:mutes"]}],
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
}
end

def blocks_operation do
%Operation{
tags: ["accounts"],
summary: "Blocked users",
operationId: "AccountController.blocks",
description: "View your blocks. See also accounts/:id/{block,unblock}",
security: [%{"oAuth" => ["read:blocks"]}],
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
}
end

def endorsements_operation do
%Operation{
tags: ["accounts"],
summary: "Endorsements",
operationId: "AccountController.endorsements",
description: "Not implemented",
security: [%{"oAuth" => ["read:accounts"]}],
responses: %{
200 => Operation.response("Empry array", "application/json", %Schema{type: :array})
}
}
end

def identity_proofs_operation do
%Operation{
tags: ["accounts"],
summary: "Identity proofs",
operationId: "AccountController.identity_proofs",
description: "Not implemented",
responses: %{
200 => Operation.response("Empry array", "application/json", %Schema{type: :array})
}
}
end

defp create_request do
%Schema{
title: "AccountCreateRequest",
description: "POST body for creating an account",
type: :object,
properties: %{
reason: %Schema{
type: :string,
description:
"Text that will be reviewed by moderators if registrations require manual approval"
},
username: %Schema{type: :string, description: "The desired username for the account"},
email: %Schema{
type: :string,
description:
"The email address to be used for login. Required when `account_activation_required` is enabled.",
format: :email
},
password: %Schema{
type: :string,
description: "The password to be used for login",
format: :password
},
agreement: %Schema{
type: :boolean,
description:
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE."
},
locale: %Schema{
type: :string,
description: "The language of the confirmation email that will be sent"
},
# Pleroma-specific properties:
fullname: %Schema{type: :string, description: "Full name"},
bio: %Schema{type: :string, description: "Bio", default: ""},
captcha_solution: %Schema{
type: :string,
description: "Provider-specific captcha solution"
},
captcha_token: %Schema{type: :string, description: "Provider-specific captcha token"},
captcha_answer_data: %Schema{type: :string, description: "Provider-specific captcha data"},
token: %Schema{
type: :string,
description: "Invite token required when the registrations aren't public"
}
},
required: [:username, :password, :agreement],
example: %{
"username" => "cofe",
"email" => "cofe@example.com",
"password" => "secret",
"agreement" => "true",
"bio" => "☕️"
}
}
end

defp create_response do
%Schema{
title: "AccountCreateResponse",
description: "Response schema for an account",
type: :object,
properties: %{
token_type: %Schema{type: :string},
access_token: %Schema{type: :string},
scope: %Schema{type: :array, items: %Schema{type: :string}},
created_at: %Schema{type: :integer, format: :"date-time"}
},
example: %{
"access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
"created_at" => 1_585_918_714,
"scope" => ["read", "write", "follow", "push"],
"token_type" => "Bearer"
}
}
end

defp update_creadentials_request do
%Schema{
title: "AccountUpdateCredentialsRequest",
description: "POST body for creating an account",
type: :object,
properties: %{
bot: %Schema{
type: :boolean,
description: "Whether the account has a bot flag."
},
display_name: %Schema{
type: :string,
description: "The display name to use for the profile."
},
note: %Schema{type: :string, description: "The account bio."},
avatar: %Schema{
type: :string,
description: "Avatar image encoded using multipart/form-data",
format: :binary
},
header: %Schema{
type: :string,
description: "Header image encoded using multipart/form-data",
format: :binary
},
locked: %Schema{
type: :boolean,
description: "Whether manual approval of follow requests is required."
},
fields_attributes: %Schema{
oneOf: [
%Schema{type: :array, items: attribute_field()},
%Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
]
},
# NOTE: `source` field is not supported
#
# source: %Schema{
# type: :object,
# properties: %{
# privacy: %Schema{type: :string},
# sensitive: %Schema{type: :boolean},
# language: %Schema{type: :string}
# }
# },

# Pleroma-specific fields
no_rich_text: %Schema{
type: :boolean,
description: "html tags are stripped from all statuses requested from the API"
},
hide_followers: %Schema{type: :boolean, description: "user's followers will be hidden"},
hide_follows: %Schema{type: :boolean, description: "user's follows will be hidden"},
hide_followers_count: %Schema{
type: :boolean,
description: "user's follower count will be hidden"
},
hide_follows_count: %Schema{
type: :boolean,
description: "user's follow count will be hidden"
},
hide_favorites: %Schema{
type: :boolean,
description: "user's favorites timeline will be hidden"
},
show_role: %Schema{
type: :boolean,
description: "user's role (e.g admin, moderator) will be exposed to anyone in the
API"
},
default_scope: VisibilityScope,
pleroma_settings_store: %Schema{
type: :object,
description: "Opaque user settings to be saved on the backend."
},
skip_thread_containment: %Schema{
type: :boolean,
description: "Skip filtering out broken threads"
},
allow_following_move: %Schema{
type: :boolean,
description: "Allows automatically follow moved following accounts"
},
pleroma_background_image: %Schema{
type: :string,
description: "Sets the background image of the user.",
format: :binary
},
discoverable: %Schema{
type: :boolean,
description:
"Discovery of this account in search results and other services is allowed."
},
actor_type: ActorType
},
example: %{
bot: false,
display_name: "cofe",
note: "foobar",
fields_attributes: [%{name: "foo", value: "bar"}],
no_rich_text: false,
hide_followers: true,
hide_follows: false,
hide_followers_count: false,
hide_follows_count: false,
hide_favorites: false,
show_role: false,
default_scope: "private",
pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
skip_thread_containment: false,
allow_following_move: false,
discoverable: false,
actor_type: "Person"
}
}
end

defp array_of_accounts do
%Schema{
title: "ArrayOfAccounts",
type: :array,
items: Account
}
end

defp array_of_relationships do
%Schema{
title: "ArrayOfRelationships",
description: "Response schema for account relationships",
type: :array,
items: AccountRelationship,
example: [
%{
"id" => "1",
"following" => true,
"showing_reblogs" => true,
"followed_by" => true,
"blocking" => false,
"blocked_by" => true,
"muting" => false,
"muting_notifications" => false,
"requested" => false,
"domain_blocking" => false,
"subscribing" => false,
"endorsed" => true
},
%{
"id" => "2",
"following" => true,
"showing_reblogs" => true,
"followed_by" => true,
"blocking" => false,
"blocked_by" => true,
"muting" => true,
"muting_notifications" => false,
"requested" => true,
"domain_blocking" => false,
"subscribing" => false,
"endorsed" => false
},
%{
"id" => "3",
"following" => true,
"showing_reblogs" => true,
"followed_by" => true,
"blocking" => true,
"blocked_by" => false,
"muting" => true,
"muting_notifications" => false,
"requested" => false,
"domain_blocking" => true,
"subscribing" => true,
"endorsed" => false
}
]
}
end

defp follows_request do
%Schema{
title: "AccountFollowsRequest",
description: "POST body for muting an account",
type: :object,
properties: %{
uri: %Schema{type: :string, format: :uri}
},
required: [:uri]
}
end

defp mute_request do
%Schema{
title: "AccountMuteRequest",
description: "POST body for muting an account",
type: :object,
properties: %{
notifications: %Schema{
type: :boolean,
description: "Mute notifications in addition to statuses? Defaults to true.",
default: true
}
},
example: %{
"notifications" => true
}
}
end

defp list do
%Schema{
title: "List",
description: "Response schema for a list",
type: :object,
properties: %{
id: %Schema{type: :string},
title: %Schema{type: :string}
},
example: %{
"id" => "123",
"title" => "my list"
}
}
end

defp array_of_lists do
%Schema{
title: "ArrayOfLists",
description: "Response schema for lists",
type: :array,
items: list(),
example: [
%{"id" => "123", "title" => "my list"},
%{"id" => "1337", "title" => "anotehr list"}
]
}
end

defp array_of_statuses do
%Schema{
title: "ArrayOfStatuses",
type: :array,
items: Status
}
end

defp attribute_field do
%Schema{
title: "AccountAttributeField",
description: "Request schema for account custom fields",
type: :object,
properties: %{
name: %Schema{type: :string},
value: %Schema{type: :string}
},
required: [:name, :value],
example: %{
"name" => "Website",
"value" => "https://pleroma.com"
}
}
end
end

+ 57
- 9
lib/pleroma/web/api_spec/operations/app_operation.ex View File

@@ -6,8 +6,6 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse

@spec open_api_operation(atom) :: Operation.t()
def open_api_operation(action) do
@@ -22,9 +20,9 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
summary: "Create an application",
description: "Create a new application to obtain OAuth2 credentials",
operationId: "AppController.create",
requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true),
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
responses: %{
200 => Operation.response("App", "application/json", AppCreateResponse),
200 => Operation.response("App", "application/json", create_response()),
422 =>
Operation.response(
"Unprocessable Entity",
@@ -51,11 +49,7 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
summary: "Verify your app works",
description: "Confirm that the app's OAuth2 credentials work.",
operationId: "AppController.verify_credentials",
security: [
%{
"oAuth" => ["read"]
}
],
security: [%{"oAuth" => ["read"]}],
responses: %{
200 =>
Operation.response("App", "application/json", %Schema{
@@ -93,4 +87,58 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
}
}
end

defp create_request do
%Schema{
title: "AppCreateRequest",
description: "POST body for creating an app",
type: :object,
properties: %{
client_name: %Schema{type: :string, description: "A name for your application."},
redirect_uris: %Schema{
type: :string,
description:
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
},
scopes: %Schema{
type: :string,
description: "Space separated list of scopes",
default: "read"
},
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
},
required: [:client_name, :redirect_uris],
example: %{
"client_name" => "My App",
"redirect_uris" => "https://myapp.com/auth/callback",
"website" => "https://myapp.com/"
}
}
end

defp create_response do
%Schema{
title: "AppCreateResponse",
description: "Response schema for an app",
type: :object,
properties: %{
id: %Schema{type: :string},
name: %Schema{type: :string},
client_id: %Schema{type: :string},
client_secret: %Schema{type: :string},
redirect_uri: %Schema{type: :string},
vapid_key: %Schema{type: :string},
website: %Schema{type: :string, nullable: true}
},
example: %{
"id" => "123",
"name" => "My App",
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
"vapid_key" =>
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
"website" => "https://myapp.com/"
}
}
end
end

+ 65
- 2
lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex View File

@@ -4,7 +4,8 @@

defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do
alias OpenApiSpex.Operation
alias Pleroma.Web.ApiSpec.Schemas.CustomEmojisResponse
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Emoji

def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
@@ -18,7 +19,69 @@ defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do
description: "Returns custom emojis that are available on the server.",
operationId: "CustomEmojiController.index",
responses: %{
200 => Operation.response("Custom Emojis", "application/json", CustomEmojisResponse)
200 => Operation.response("Custom Emojis", "application/json", resposnse())
}
}
end

defp resposnse do
%Schema{
title: "CustomEmojisResponse",
description: "Response schema for custom emojis",
type: :array,
items: custom_emoji(),
example: [
%{
"category" => "Fun",
"shortcode" => "blank",
"static_url" => "https://lain.com/emoji/blank.png",
"tags" => ["Fun"],
"url" => "https://lain.com/emoji/blank.png",
"visible_in_picker" => false
},
%{
"category" => "Gif,Fun",
"shortcode" => "firefox",
"static_url" => "https://lain.com/emoji/Firefox.gif",
"tags" => ["Gif", "Fun"],
"url" => "https://lain.com/emoji/Firefox.gif",
"visible_in_picker" => true
},
%{
"category" => "pack:mixed",
"shortcode" => "sadcat",
"static_url" => "https://lain.com/emoji/mixed/sadcat.png",
"tags" => ["pack:mixed"],
"url" => "https://lain.com/emoji/mixed/sadcat.png",
"visible_in_picker" => true
}
]
}
end

defp custom_emoji do
%Schema{
title: "CustomEmoji",
description: "Schema for a CustomEmoji",
allOf: [
Emoji,
%Schema{
type: :object,
properties: %{
category: %Schema{type: :string},
tags: %Schema{type: :array}
}
}
],
example: %{
"category" => "Fun",
"shortcode" => "aaaa",
"url" =>
"https://files.mastodon.social/custom_emojis/images/000/007/118/original/aaaa.png",
"static_url" =>
"https://files.mastodon.social/custom_emojis/images/000/007/118/static/aaaa.png",
"visible_in_picker" => true,
"tags" => ["Gif", "Fun"]
}
}
end


+ 26
- 5
lib/pleroma/web/api_spec/operations/domain_block_operation.ex View File

@@ -6,8 +6,6 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest
alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse

def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
@@ -22,7 +20,13 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
security: [%{"oAuth" => ["follow", "read:blocks"]}],
operationId: "DomainBlockController.index",
responses: %{
200 => Operation.response("Domain blocks", "application/json", DomainBlocksResponse)
200 =>
Operation.response("Domain blocks", "application/json", %Schema{
description: "Response schema for domain blocks",
type: :array,
items: %Schema{type: :string},
example: ["google.com", "facebook.com"]
})
}
}
end
@@ -40,7 +44,7 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
- prevent following new users from it (but does not remove existing follows)
""",
operationId: "DomainBlockController.create",
requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true),
requestBody: domain_block_request(),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
@@ -54,11 +58,28 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
summary: "Unblock a domain",
description: "Remove a domain block, if it exists in the user's array of blocked domains.",
operationId: "DomainBlockController.delete",
requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true),
requestBody: domain_block_request(),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
}
}
end

defp domain_block_request do
Helpers.request_body(
"Parameters",
%Schema{
type: :object,
properties: %{
domain: %Schema{type: :string}
},
required: [:domain]
},
required: true,
example: %{
"domain" => "facebook.com"
}
)
end
end

+ 231
- 0
lib/pleroma/web/api_spec/render_error.ex View File

@@ -0,0 +1,231 @@
# 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.RenderError do
@behaviour Plug

import Plug.Conn, only: [put_status: 2]
import Phoenix.Controller, only: [json: 2]
import Pleroma.Web.Gettext

@impl Plug
def init(opts), do: opts

@impl Plug

def call(conn, errors) do
errors =
Enum.map(errors, fn
%{name: nil} = err ->
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}

err ->
err
end)

conn
|> put_status(:bad_request)
|> json(%{
error: errors |> Enum.map(&message/1) |> Enum.join(" "),
errors: errors |> Enum.map(&render_error/1)
})
end

defp render_error(error) do
pointer = OpenApiSpex.path_to_string(error)

%{
title: "Invalid value",
source: %{
pointer: pointer
},
message: OpenApiSpex.Cast.Error.message(error)
}
end

defp message(%{reason: :invalid_schema_type, type: type, name: name}) do
gettext("%{name} - Invalid schema.type. Got: %{type}.",
name: name,
type: inspect(type)
)
end

defp message(%{reason: :null_value, name: name} = error) do
case error.type do
nil ->
gettext("%{name} - null value.", name: name)

type ->
gettext("%{name} - null value where %{type} expected.",
name: name,
type: type
)
end
end

defp message(%{reason: :all_of, meta: %{invalid_schema: invalid_schema}}) do
gettext(
"Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed.",
invalid_schema: invalid_schema
)
end

defp message(%{reason: :any_of, meta: %{failed_schemas: failed_schemas}}) do
gettext("Failed to cast value using any of: %{failed_schemas}.",
failed_schemas: failed_schemas
)
end

defp message(%{reason: :one_of, meta: %{failed_schemas: failed_schemas}}) do
gettext("Failed to cast value to one of: %{failed_schemas}.", failed_schemas: failed_schemas)
end

defp message(%{reason: :min_length, length: length, name: name}) do
gettext("%{name} - String length is smaller than minLength: %{length}.",
name: name,
length: length
)
end

defp message(%{reason: :max_length, length: length, name: name}) do
gettext("%{name} - String length is larger than maxLength: %{length}.",
name: name,
length: length
)
end

defp message(%{reason: :unique_items, name: name}) do
gettext("%{name} - Array items must be unique.", name: name)
end

defp message(%{reason: :min_items, length: min, value: array, name: name}) do
gettext("%{name} - Array length %{length} is smaller than minItems: %{min}.",
name: name,
length: length(array),
min: min
)
end

defp message(%{reason: :max_items, length: max, value: array, name: name}) do
gettext("%{name} - Array length %{length} is larger than maxItems: %{}.",
name: name,
length: length(array),
max: max
)
end

defp message(%{reason: :multiple_of, length: multiple, value: count, name: name}) do
gettext("%{name} - %{count} is not a multiple of %{multiple}.",
name: name,
count: count,
multiple: multiple
)
end

defp message(%{reason: :exclusive_max, length: max, value: value, name: name})
when value >= max do
gettext("%{name} - %{value} is larger than exclusive maximum %{max}.",
name: name,
value: value,
max: max
)
end

defp message(%{reason: :maximum, length: max, value: value, name: name})
when value > max do
gettext("%{name} - %{value} is larger than inclusive maximum %{max}.",
name: name,
value: value,
max: max
)
end

defp message(%{reason: :exclusive_multiple, length: min, value: value, name: name})
when value <= min do
gettext("%{name} - %{value} is smaller than exclusive minimum %{min}.",
name: name,
value: value,
min: min
)
end

defp message(%{reason: :minimum, length: min, value: value, name: name})
when value < min do
gettext("%{name} - %{value} is smaller than inclusive minimum %{min}.",
name: name,
value: value,
min: min
)
end

defp message(%{reason: :invalid_type, type: type, value: value, name: name}) do
gettext("%{name} - Invalid %{type}. Got: %{value}.",
name: name,
value: OpenApiSpex.TermType.type(value),
type: type
)
end

defp message(%{reason: :invalid_format, format: format, name: name}) do
gettext("%{name} - Invalid format. Expected %{format}.", name: name, format: inspect(format))
end

defp message(%{reason: :invalid_enum, name: name}) do
gettext("%{name} - Invalid value for enum.", name: name)
end

defp message(%{reason: :polymorphic_failed, type: polymorphic_type}) do
gettext("Failed to cast to any schema in %{polymorphic_type}",
polymorphic_type: polymorphic_type
)
end

defp message(%{reason: :unexpected_field, name: name}) do
gettext("Unexpected field: %{name}.", name: safe_string(name))
end

defp message(%{reason: :no_value_for_discriminator, name: field}) do
gettext("Value used as discriminator for `%{field}` matches no schemas.", name: field)
end

defp message(%{reason: :invalid_discriminator_value, name: field}) do
gettext("No value provided for required discriminator `%{field}`.", name: field)
end

defp message(%{reason: :unknown_schema, name: name}) do
gettext("Unknown schema: %{name}.", name: name)
end

defp message(%{reason: :missing_field, name: name}) do
gettext("Missing field: %{name}.", name: name)
end

defp message(%{reason: :missing_header, name: name}) do
gettext("Missing header: %{name}.", name: name)
end

defp message(%{reason: :invalid_header, name: name}) do
gettext("Invalid value for header: %{name}.", name: name)
end

defp message(%{reason: :max_properties, meta: meta}) do
gettext(
"Object property count %{property_count} is greater than maxProperties: %{max_properties}.",
property_count: meta.property_count,
max_properties: meta.max_properties
)
end

defp message(%{reason: :min_properties, meta: meta}) do
gettext(
"Object property count %{property_count} is less than minProperties: %{min_properties}",
property_count: meta.property_count,
min_properties: meta.min_properties
)
end

defp safe_string(string) do
to_string(string) |> String.slice(0..39)
end
end

+ 167
- 0
lib/pleroma/web/api_spec/schemas/account.ex View File

@@ -0,0 +1,167 @@
# 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.Schemas.Account do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.AccountField
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ActorType
alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope

require OpenApiSpex

OpenApiSpex.schema(%{
title: "Account",
description: "Response schema for an account",
type: :object,
properties: %{
acct: %Schema{type: :string},
avatar_static: %Schema{type: :string, format: :uri},
avatar: %Schema{type: :string, format: :uri},
bot: %Schema{type: :boolean},
created_at: %Schema{type: :string, format: "date-time"},
display_name: %Schema{type: :string},
emojis: %Schema{type: :array, items: Emoji},
fields: %Schema{type: :array, items: AccountField},
follow_requests_count: %Schema{type: :integer},
followers_count: %Schema{type: :integer},
following_count: %Schema{type: :integer},
header_static: %Schema{type: :string, format: :uri},
header: %Schema{type: :string, format: :uri},
id: FlakeID,
locked: %Schema{type: :boolean},
note: %Schema{type: :string, format: :html},
statuses_count: %Schema{type: :integer},
url: %Schema{type: :string, format: :uri},
username: %Schema{type: :string},
pleroma: %Schema{
type: :object,
properties: %{
allow_following_move: %Schema{type: :boolean},
background_image: %Schema{type: :string, nullable: true},
chat_token: %Schema{type: :string},
confirmation_pending: %Schema{type: :boolean},
hide_favorites: %Schema{type: :boolean},
hide_followers_count: %Schema{type: :boolean},
hide_followers: %Schema{type: :boolean},
hide_follows_count: %Schema{type: :boolean},
hide_follows: %Schema{type: :boolean},
is_admin: %Schema{type: :boolean},
is_moderator: %Schema{type: :boolean},
skip_thread_containment: %Schema{type: :boolean},
tags: %Schema{type: :array, items: %Schema{type: :string}},
unread_conversation_count: %Schema{type: :integer},
notification_settings: %Schema{
type: :object,
properties: %{
followers: %Schema{type: :boolean},
follows: %Schema{type: :boolean},
non_followers: %Schema{type: :boolean},
non_follows: %Schema{type: :boolean},
privacy_option: %Schema{type: :boolean}
}
},
relationship: AccountRelationship,
settings_store: %Schema{
type: :object
}
}
},
source: %Schema{
type: :object,
properties: %{
fields: %Schema{type: :array, items: AccountField},
note: %Schema{type: :string},
privacy: VisibilityScope,
sensitive: %Schema{type: :boolean},
pleroma: %Schema{
type: :object,
properties: %{
actor_type: ActorType,
discoverable: %Schema{type: :boolean},
no_rich_text: %Schema{type: :boolean},
show_role: %Schema{type: :boolean}
}
}
}
}
},
example: %{
"acct" => "foobar",
"avatar" => "https://mypleroma.com/images/avi.png",
"avatar_static" => "https://mypleroma.com/images/avi.png",
"bot" => false,
"created_at" => "2020-03-24T13:05:58.000Z",
"display_name" => "foobar",
"emojis" => [],
"fields" => [],
"follow_requests_count" => 0,
"followers_count" => 0,
"following_count" => 1,
"header" => "https://mypleroma.com/images/banner.png",
"header_static" => "https://mypleroma.com/images/banner.png",
"id" => "9tKi3esbG7OQgZ2920",
"locked" => false,
"note" => "cofe",
"pleroma" => %{
"allow_following_move" => true,
"background_image" => nil,
"confirmation_pending" => true,
"hide_favorites" => true,
"hide_followers" => false,
"hide_followers_count" => false,
"hide_follows" => false,
"hide_follows_count" => false,
"is_admin" => false,
"is_moderator" => false,
"skip_thread_containment" => false,
"chat_token" =>
"SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc",
"unread_conversation_count" => 0,
"tags" => [],
"notification_settings" => %{
"followers" => true,
"follows" => true,
"non_followers" => true,
"non_follows" => true,
"privacy_option" => false
},
"relationship" => %{
"blocked_by" => false,
"blocking" => false,
"domain_blocking" => false,
"endorsed" => false,
"followed_by" => false,
"following" => false,
"id" => "9tKi3esbG7OQgZ2920",
"muting" => false,
"muting_notifications" => false,
"requested" => false,
"showing_reblogs" => true,
"subscribing" => false
},
"settings_store" => %{
"pleroma-fe" => %{}
}
},
"source" => %{
"fields" => [],
"note" => "foobar",
"pleroma" => %{
"actor_type" => "Person",
"discoverable" => false,
"no_rich_text" => false,
"show_role" => true
},
"privacy" => "public",
"sensitive" => false
},
"statuses_count" => 0,
"url" => "https://mypleroma.com/users/foobar",
"username" => "foobar"
}
})
end

+ 26
- 0
lib/pleroma/web/api_spec/schemas/account_field.ex View File

@@ -0,0 +1,26 @@
# 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.Schemas.AccountField do
alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(%{
title: "AccountField",
description: "Response schema for account custom fields",
type: :object,
properties: %{
name: %Schema{type: :string},
value: %Schema{type: :string, format: :html},
verified_at: %Schema{type: :string, format: :"date-time", nullable: true}
},
example: %{
"name" => "Website",
"value" =>
"<a href=\"https://pleroma.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">pleroma.com</span><span class=\"invisible\"></span></a>",
"verified_at" => "2019-08-29T04:14:55.571+00:00"
}
})
end

+ 44
- 0
lib/pleroma/web/api_spec/schemas/account_relationship.ex View File

@@ -0,0 +1,44 @@
# 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.Schemas.AccountRelationship do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.FlakeID

require OpenApiSpex

OpenApiSpex.schema(%{
title: "AccountRelationship",
description: "Response schema for relationship",
type: :object,
properties: %{
blocked_by: %Schema{type: :boolean},
blocking: %Schema{type: :boolean},
domain_blocking: %Schema{type: :boolean},
endorsed: %Schema{type: :boolean},
followed_by: %Schema{type: :boolean},
following: %Schema{type: :boolean},
id: FlakeID,
muting: %Schema{type: :boolean},
muting_notifications: %Schema{type: :boolean},
requested: %Schema{type: :boolean},
showing_reblogs: %Schema{type: :boolean},
subscribing: %Schema{type: :boolean}
},
example: %{
"blocked_by" => false,
"blocking" => false,
"domain_blocking" => false,
"endorsed" => false,
"followed_by" => false,
"following" => false,
"id" => "9tKi3esbG7OQgZ2920",
"muting" => false,
"muting_notifications" => false,
"requested" => false,
"showing_reblogs" => true,
"subscribing" => false
}
})
end

+ 13
- 0
lib/pleroma/web/api_spec/schemas/actor_type.ex View File

@@ -0,0 +1,13 @@
# 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.Schemas.ActorType do
require OpenApiSpex

OpenApiSpex.schema(%{
title: "ActorType",
type: :string,
enum: ["Application", "Group", "Organization", "Person", "Service"]
})
end

lib/pleroma/web/api_spec/schemas/domain_block_request.ex → lib/pleroma/web/api_spec/schemas/api_error.ex View File

@@ -2,19 +2,18 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest do
defmodule Pleroma.Web.ApiSpec.Schemas.ApiError do
alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(%{
title: "DomainBlockRequest",
title: "ApiError",
description: "Response schema for API error",
type: :object,
properties: %{
domain: %Schema{type: :string}
},
required: [:domain],
properties: %{error: %Schema{type: :string}},
example: %{
"domain" => "facebook.com"
"error" => "Something went wrong"
}
})
end

+ 0
- 33
lib/pleroma/web/api_spec/schemas/app_create_request.ex View File

@@ -1,33 +0,0 @@
# 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.Schemas.AppCreateRequest do
alias OpenApiSpex.Schema
require OpenApiSpex

OpenApiSpex.schema(%{
title: "AppCreateRequest",
description: "POST body for creating an app",
type: :object,
properties: %{
client_name: %Schema{type: :string, description: "A name for your application."},
redirect_uris: %Schema{
type: :string,
description:
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
},
scopes: %Schema{
type: :string,
description: "Space separated list of scopes. If none is provided, defaults to `read`."
},
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
},
required: [:client_name, :redirect_uris],
example: %{
"client_name" => "My App",
"redirect_uris" => "https://myapp.com/auth/callback",
"website" => "https://myapp.com/"
}
})
end

+ 0
- 33
lib/pleroma/web/api_spec/schemas/app_create_response.ex View File

@@ -1,33 +0,0 @@
# 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.Schemas.AppCreateResponse do
alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(%{
title: "AppCreateResponse",
description: "Response schema for an app",
type: :object,
properties: %{
id: %Schema{type: :string},
name: %Schema{type: :string},
client_id: %Schema{type: :string},
client_secret: %Schema{type: :string},
redirect_uri: %Schema{type: :string},
vapid_key: %Schema{type: :string},
website: %Schema{type: :string, nullable: true}
},
example: %{
"id" => "123",
"name" => "My App",
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
"vapid_key" =>
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
"website" => "https://myapp.com/"
}
})
end

+ 36
- 0
lib/pleroma/web/api_spec/schemas/boolean_like.ex View File

@@ -0,0 +1,36 @@
# 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.Schemas.BooleanLike do
alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(%{
title: "BooleanLike",
description: """
The following values will be treated as `false`:
- false
- 0
- "0",
- "f",
- "F",
- "false",
- "FALSE",
- "off",
- "OFF"

All other non-null values will be treated as `true`
""",
anyOf: [
%Schema{type: :boolean},
%Schema{type: :string},
%Schema{type: :integer}
]
})

def after_cast(value, _schmea) do
{:ok, Pleroma.Web.ControllerHelper.truthy_param?(value)}
end
end

+ 0
- 30
lib/pleroma/web/api_spec/schemas/custom_emoji.ex View File

@@ -1,30 +0,0 @@
# 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.Schemas.CustomEmoji do
alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(%{
title: "CustomEmoji",
description: "Response schema for an CustomEmoji",
type: :object,
properties: %{
shortcode: %Schema{type: :string},
url: %Schema{type: :string},
static_url: %Schema{type: :string},
visible_in_picker: %Schema{type: :boolean},
category: %Schema{type: :string},
tags: %Schema{type: :array}
},
example: %{
"shortcode" => "aaaa",
"url" => "https://files.mastodon.social/custom_emojis/images/000/007/118/original/aaaa.png",
"static_url" =>
"https://files.mastodon.social/custom_emojis/images/000/007/118/static/aaaa.png",
"visible_in_picker" => true
}
})
end

+ 0
- 42
lib/pleroma/web/api_spec/schemas/custom_emojis_response.ex View File

@@ -1,42 +0,0 @@
# 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.Schemas.CustomEmojisResponse do
alias Pleroma.Web.ApiSpec.Schemas.CustomEmoji

require OpenApiSpex

OpenApiSpex.schema(%{
title: "CustomEmojisResponse",
description: "Response schema for custom emojis",
type: :array,
items: CustomEmoji,
example: [
%{
"category" => "Fun",
"shortcode" => "blank",
"static_url" => "https://lain.com/emoji/blank.png",
"tags" => ["Fun"],
"url" => "https://lain.com/emoji/blank.png",
"visible_in_picker" => true
},
%{
"category" => "Gif,Fun",
"shortcode" => "firefox",
"static_url" => "https://lain.com/emoji/Firefox.gif",
"tags" => ["Gif", "Fun"],
"url" => "https://lain.com/emoji/Firefox.gif",
"visible_in_picker" => true
},
%{
"category" => "pack:mixed",
"shortcode" => "sadcat",
"static_url" => "https://lain.com/emoji/mixed/sadcat.png",
"tags" => ["pack:mixed"],
"url" => "https://lain.com/emoji/mixed/sadcat.png",
"visible_in_picker" => true
}
]
})
end

+ 0
- 16
lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex View File

@@ -1,16 +0,0 @@
# 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.Schemas.DomainBlocksResponse do
require OpenApiSpex
alias OpenApiSpex.Schema

OpenApiSpex.schema(%{
title: "DomainBlocksResponse",
description: "Response schema for domain blocks",
type: :array,
items: %Schema{type: :string},
example: ["google.com", "facebook.com"]
})
end

+ 29
- 0
lib/pleroma/web/api_spec/schemas/emoji.ex View File

@@ -0,0 +1,29 @@
# 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.Schemas.Emoji do
alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(%{
title: "Emoji",
description: "Response schema for an emoji",
type: :object,
properties: %{
shortcode: %Schema{type: :string},
url: %Schema{type: :string, format: :uri},
static_url: %Schema{type: :string, format: :uri},
visible_in_picker: %Schema{type: :boolean}
},
example: %{
"shortcode" => "fatyoshi",
"url" =>
"https://files.mastodon.social/custom_emojis/images/000/023/920/original/e57ecb623faa0dc9.png",
"static_url" =>
"https://files.mastodon.social/custom_emojis/images/000/023/920/static/e57ecb623faa0dc9.png",
"visible_in_picker" => true
}
})
end

+ 14
- 0
lib/pleroma/web/api_spec/schemas/flake_id.ex View File

@@ -0,0 +1,14 @@
# 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.Schemas.FlakeID do
require OpenApiSpex

OpenApiSpex.schema(%{
title: "FlakeID",
description:
"Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings",
type: :string
})
end

+ 36
- 0
lib/pleroma/web/api_spec/schemas/poll.ex View File

@@ -0,0 +1,36 @@
# 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.Schemas.Poll do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID

require OpenApiSpex

OpenApiSpex.schema(%{
title: "Poll",
description: "Response schema for account custom fields",
type: :object,
properties: %{
id: FlakeID,
expires_at: %Schema{type: :string, format: "date-time"},
expired: %Schema{type: :boolean},
multiple: %Schema{type: :boolean},
votes_count: %Schema{type: :integer},
voted: %Schema{type: :boolean},
emojis: %Schema{type: :array, items: Emoji},
options: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
title: %Schema{type: :string},
votes_count: %Schema{type: :integer}
}
}
}
}
})
end

+ 226
- 0
lib/pleroma/web/api_spec/schemas/status.ex View File

@@ -0,0 +1,226 @@
# 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.Schemas.Status do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Poll
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope

require OpenApiSpex

OpenApiSpex.schema(%{
title: "Status",
description: "Response schema for a status",
type: :object,
properties: %{
account: Account,
application: %Schema{
type: :object,
properties: %{
name: %Schema{type: :string},
website: %Schema{type: :string, nullable: true, format: :uri}
}
},
bookmarked: %Schema{type: :boolean},
card: %Schema{
type: :object,
nullable: true,
properties: %{
type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
provider_name: %Schema{type: :string, nullable: true},
provider_url: %Schema{type: :string, format: :uri},
url: %Schema{type: :string, format: :uri},
image: %Schema{type: :string, nullable: true, format: :uri},
title: %Schema{type: :string},
description: %Schema{type: :string}
}
},
content: %Schema{type: :string, format: :html},
created_at: %Schema{type: :string, format: "date-time"},
emojis: %Schema{type: :array, items: Emoji},
favourited: %Schema{type: :boolean},
favourites_count: %Schema{type: :integer},
id: FlakeID,
in_reply_to_account_id: %Schema{type: :string, nullable: true},
in_reply_to_id: %Schema{type: :string, nullable: true},
language: %Schema{type: :string, nullable: true},
media_attachments: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
id: %Schema{type: :string},
url: %Schema{type: :string, format: :uri},
remote_url: %Schema{type: :string, format: :uri},
preview_url: %Schema{type: :string, format: :uri},
text_url: %Schema{type: :string, format: :uri},
description: %Schema{type: :string},
type: %Schema{type: :string, enum: ["image", "video", "audio", "unknown"]},
pleroma: %Schema{
type: :object,
properties: %{mime_type: %Schema{type: :string}}
}
}
}
},
mentions: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
id: %Schema{type: :string},
acct: %Schema{type: :string},
username: %Schema{type: :string},
url: %Schema{type: :string, format: :uri}
}
}
},
muted: %Schema{type: :boolean},
pinned: %Schema{type: :boolean},
pleroma: %Schema{
type: :object,
properties: %{
content: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
conversation_id: %Schema{type: :integer},
direct_conversation_id: %Schema{type: :string, nullable: true},
emoji_reactions: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
name: %Schema{type: :string},
count: %Schema{type: :integer},
me: %Schema{type: :boolean}
}
}
},
expires_at: %Schema{type: :string, format: "date-time", nullable: true},
in_reply_to_account_acct: %Schema{type: :string, nullable: true},
local: %Schema{type: :boolean},
spoiler_text: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
thread_muted: %Schema{type: :boolean}
}
},
poll: %Schema{type: Poll, nullable: true},
reblog: %Schema{
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
nullable: true
},
reblogged: %Schema{type: :boolean},
reblogs_count: %Schema{type: :integer},
replies_count: %Schema{type: :integer},
sensitive: %Schema{type: :boolean},
spoiler_text: %Schema{type: :string},
tags: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
name: %Schema{type: :string},
url: %Schema{type: :string, format: :uri}
}
}
},
uri: %Schema{type: :string, format: :uri},
url: %Schema{type: :string, nullable: true, format: :uri},
visibility: VisibilityScope
},
example: %{
"account" => %{
"acct" => "nick6",
"avatar" => "http://localhost:4001/images/avi.png",
"avatar_static" => "http://localhost:4001/images/avi.png",
"bot" => false,
"created_at" => "2020-04-07T19:48:51.000Z",
"display_name" => "Test テスト User 6",
"emojis" => [],
"fields" => [],
"followers_count" => 1,
"following_count" => 0,
"header" => "http://localhost:4001/images/banner.png",
"header_static" => "http://localhost:4001/images/banner.png",
"id" => "9toJCsKN7SmSf3aj5c",
"locked" => false,
"note" => "Tester Number 6",
"pleroma" => %{
"background_image" => nil,
"confirmation_pending" => false,
"hide_favorites" => true,
"hide_followers" => false,
"hide_followers_count" => false,
"hide_follows" => false,
"hide_follows_count" => false,
"is_admin" => false,
"is_moderator" => false,
"relationship" => %{
"blocked_by" => false,
"blocking" => false,
"domain_blocking" => false,
"endorsed" => false,
"followed_by" => false,
"following" => true,
"id" => "9toJCsKN7SmSf3aj5c",
"muting" => false,
"muting_notifications" => false,
"requested" => false,
"showing_reblogs" => true,
"subscribing" => false
},
"skip_thread_containment" => false,
"tags" => []
},
"source" => %{
"fields" => [],
"note" => "Tester Number 6",
"pleroma" => %{"actor_type" => "Person", "discoverable" => false},
"sensitive" => false
},
"statuses_count" => 1,
"url" => "http://localhost:4001/users/nick6",
"username" => "nick6"
},
"application" => %{"name" => "Web", "website" => nil},
"bookmarked" => false,
"card" => nil,
"content" => "foobar",
"created_at" => "2020-04-07T19:48:51.000Z",
"emojis" => [],
"favourited" => false,
"favourites_count" => 0,
"id" => "9toJCu5YZW7O7gfvH6",
"in_reply_to_account_id" => nil,
"in_reply_to_id" => nil,
"language" => nil,
"media_attachments" => [],
"mentions" => [],
"muted" => false,
"pinned" => false,
"pleroma" => %{
"content" => %{"text/plain" => "foobar"},
"conversation_id" => 345_972,
"direct_conversation_id" => nil,
"emoji_reactions" => [],
"expires_at" => nil,
"in_reply_to_account_acct" => nil,
"local" => true,
"spoiler_text" => %{"text/plain" => ""},
"thread_muted" => false
},
"poll" => nil,
"reblog" => nil,
"reblogged" => false,
"reblogs_count" => 0,
"replies_count" => 0,
"sensitive" => false,
"spoiler_text" => "",
"tags" => [],
"uri" => "http://localhost:4001/objects/0f5dad44-0e9e-4610-b377-a2631e499190",
"url" => "http://localhost:4001/notice/9toJCu5YZW7O7gfvH6",
"visibility" => "private"
}
})
end

+ 14
- 0
lib/pleroma/web/api_spec/schemas/visibility_scope.ex View File

@@ -0,0 +1,14 @@
# 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.Schemas.VisibilityScope do
require OpenApiSpex

OpenApiSpex.schema(%{
title: "VisibilityScope",
description: "Status visibility",
type: :string,
enum: ["public", "unlisted", "private", "direct"]
})
end

+ 10
- 6
lib/pleroma/web/common_api/activity_draft.ex View File

@@ -84,14 +84,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
%__MODULE__{draft | attachments: attachments}
end

defp in_reply_to(draft) do
case Map.get(draft.params, "in_reply_to_status_id") do
"" -> draft
nil -> draft
id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
end
defp in_reply_to(%{params: %{"in_reply_to_status_id" => ""}} = draft), do: draft

defp in_reply_to(%{params: %{"in_reply_to_status_id" => id}} = draft) when is_binary(id) do
%__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
end

defp in_reply_to(%{params: %{"in_reply_to_status_id" => %Activity{} = in_reply_to}} = draft) do
%__MODULE__{draft | in_reply_to: in_reply_to}
end

defp in_reply_to(draft), do: draft

defp in_reply_to_conversation(draft) do
in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}


+ 13
- 10
lib/pleroma/web/common_api/common_api.ex View File

@@ -86,8 +86,9 @@ defmodule Pleroma.Web.CommonAPI do
end
end

def repeat(id_or_ap_id, user, params \\ %{}) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
def repeat(id, user, params \\ %{}) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
object <- Object.normalize(activity),
announce_activity <- Utils.get_existing_announce(user.ap_id, object),
public <- public_announce?(object, params) do
@@ -102,8 +103,9 @@ defmodule Pleroma.Web.CommonAPI do
end
end

def unrepeat(id_or_ap_id, user) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)} do
object = Object.normalize(activity)
ActivityPub.unannounce(user, object)
else
@@ -160,8 +162,9 @@ defmodule Pleroma.Web.CommonAPI do
end
end

def unfavorite(id_or_ap_id, user) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)} do
object = Object.normalize(activity)
ActivityPub.unlike(user, object)
else
@@ -332,12 +335,12 @@ defmodule Pleroma.Web.CommonAPI do

defp maybe_create_activity_expiration(result, _), do: result

def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
def pin(id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
data: %{"type" => "Create"},
object: %Object{data: %{"type" => object_type}}
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
} = activity <- Activity.get_by_id_with_object(id),
true <- object_type in ["Note", "Article", "Question"],
true <- Visibility.is_public?(activity),
{:ok, _user} <- User.add_pinnned_activity(user, activity) do
@@ -348,8 +351,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end

def unpin(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
def unpin(id, user) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
{:ok, _user} <- User.remove_pinnned_activity(user, activity) do
{:ok, activity}
else


+ 0
- 18
lib/pleroma/web/common_api/utils.ex View File

@@ -22,24 +22,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
require Logger
require Pleroma.Constants

# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
activity =
with true <- FlakeId.flake_id?(id),
%Activity{} = activity <- Activity.get_by_id_with_object(id) do
activity
else
_ -> Activity.get_create_by_object_ap_id_with_object(id)
end

activity &&
if activity.data["type"] == "Create" do
activity
else
Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
end
end

def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
attachments_from_ids_descs(ids, desc)
end


+ 3
- 2
lib/pleroma/web/controller_helper.ex View File

@@ -82,8 +82,9 @@ defmodule Pleroma.Web.ControllerHelper do
end
end

def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do
case Pleroma.User.get_cached_by_id(id) do
def assign_account_by_id(conn, _) do
# TODO: use `conn.params[:id]` only after moving to OpenAPI
case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do
%Pleroma.User{} = account -> assign(conn, :account, account)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end


+ 63
- 41
lib/pleroma/web/mastodon_api/controllers/account_controller.ex View File

@@ -27,6 +27,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI

plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)

plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)

plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
@@ -88,26 +90,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do

action_fallback(Pleroma.Web.MastodonAPI.FallbackController)

defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation

@doc "POST /api/v1/accounts"
def create(
%{assigns: %{app: app}} = conn,
%{"username" => nickname, "password" => _, "agreement" => true} = params
) do
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
params =
params
|> Map.take([
"email",
"captcha_solution",
"captcha_token",
"captcha_answer_data",
"token",
"password"
:email,
:bio,
:captcha_solution,
:captcha_token,
:captcha_answer_data,
:token,
:password,
:fullname
])
|> Map.put("nickname", nickname)
|> Map.put("fullname", params["fullname"] || nickname)
|> Map.put("bio", params["bio"] || "")
|> Map.put("confirm", params["password"])
|> Map.put("trusted_app", app.trusted)
|> 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, user} <- TwitterAPI.register_user(params, need_confirmation: true),
@@ -131,7 +133,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
render_error(conn, :forbidden, "Invalid credentials")
end

defp validate_email_param(%{"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
@@ -153,7 +155,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end

@doc "PATCH /api/v1/accounts/update_credentials"
def update_credentials(%{assigns: %{user: user}} = conn, params) do
def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
user = original_user

params =
params
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
|> Enum.into(%{})

user_params =
[
:no_rich_text,
@@ -169,22 +178,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
:discoverable
]
|> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
end)
|> add_if_present(params, "display_name", :name)
|> add_if_present(params, "note", :bio)
|> add_if_present(params, "avatar", :avatar)
|> add_if_present(params, "header", :banner)
|> add_if_present(params, "pleroma_background_image", :background)
|> add_if_present(params, :display_name, :name)
|> add_if_present(params, :note, :bio)
|> add_if_present(params, :avatar, :avatar)
|> add_if_present(params, :header, :banner)
|> add_if_present(params, :pleroma_background_image, :background)
|> add_if_present(
params,
"fields_attributes",
:fields_attributes,
:raw_fields,
&{:ok, normalize_fields_attributes(&1)}
)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
|> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "actor_type", :actor_type)
|> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
|> add_if_present(params, :default_scope, :default_scope)
|> add_if_present(params, :actor_type, :actor_type)

changeset = User.update_changeset(user, user_params)

@@ -197,7 +206,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do

defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
with true <- Map.has_key?(params, params_field),
{:ok, new_value} <- value_function.(params[params_field]) do
{:ok, new_value} <- value_function.(Map.get(params, params_field)) do
Map.put(map, map_field, new_value)
else
_ -> map
@@ -208,12 +217,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end)
else
fields
Enum.map(fields, fn
%{} = field -> %{"name" => field.name, "value" => field.value}
field -> field
end)
end
end

@doc "GET /api/v1/accounts/relationships"
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
targets = User.get_all_by_ids(List.wrap(id))

render(conn, "relationships.json", user: user, targets: targets)
@@ -223,7 +235,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])

@doc "GET /api/v1/accounts/:id"
def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
true <- User.visible_for?(user, for_user) do
render(conn, "show.json", user: user, for: for_user)
@@ -234,12 +246,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do

@doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
true <- User.visible_for?(user, reading_user) do
params =
params
|> Map.put("tag", params["tagged"])
|> Map.delete("godmode")
|> Map.delete(:tagged)
|> Enum.filter(&(not is_nil(&1)))
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("tag", params[:tagged])

activities = ActivityPub.fetch_user_activities(user, reading_user, params)

@@ -259,6 +273,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do

@doc "GET /api/v1/accounts/:id/followers"
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> Enum.into(%{})

followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
@@ -273,6 +292,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do

@doc "GET /api/v1/accounts/:id/following"
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> Enum.into(%{})

followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
@@ -299,8 +323,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
{:error, "Can not follow yourself"}
end

def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -319,10 +343,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end

@doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
notifications? = params |> Map.get("notifications", true) |> truthy_param?()

with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -359,7 +381,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end

@doc "POST /api/v1/follows"
def follow_by_uri(conn, %{"uri" => uri}) do
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
case User.get_cached_by_nickname(uri) do
%User{} = user ->
conn


+ 2
- 1
lib/pleroma/web/mastodon_api/controllers/status_controller.ex View File

@@ -127,7 +127,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
def create(
%{assigns: %{user: user}} = conn,
%{"status" => _, "scheduled_at" => scheduled_at} = params
) do
)
when not is_nil(scheduled_at) do
params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])

with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},


+ 2
- 0
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex View File

@@ -44,6 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_filtering_user", user)
|> Map.put("user", user)

recipients = [user.ap_id | User.following(user)]
@@ -109,6 +110,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()

conn


+ 14
- 8
lib/pleroma/web/mastodon_api/views/status_view.ex View File

@@ -45,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end)
end

defp get_user(ap_id) do
def get_user(ap_id, fake_record_fallback \\ true) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
user
@@ -53,8 +53,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
user = User.get_by_guessed_nickname(ap_id) ->
user

true ->
fake_record_fallback ->
# TODO: refactor (fake records is never a good idea)
User.error_user(ap_id)

true ->
nil
end
end

@@ -97,7 +101,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
UserRelationship.view_relationships_option(nil, [])

true ->
actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"]))
# Note: unresolved users are filtered out
actors =
(activities ++ parent_activities)
|> Enum.map(&get_user(&1.data["actor"], false))
|> Enum.filter(& &1)

UserRelationship.view_relationships_option(reading_user, actors,
source_mutes_only: opts[:skip_relationships]
@@ -521,11 +529,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
"""
@spec build_tags(list(any())) :: list(map())
def build_tags(object_tags) when is_list(object_tags) do
object_tags = for tag when is_binary(tag) <- object_tags, do: tag

Enum.reduce(object_tags, [], fn tag, tags ->
tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
end)
object_tags
|> Enum.filter(&is_binary/1)
|> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
end

def build_tags(_), do: []


+ 2
- 2
lib/pleroma/web/mongooseim/mongoose_im_controller.ex View File

@@ -14,7 +14,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)

def user_exists(conn, %{"user" => username}) do
with %User{} <- Repo.get_by(User, nickname: username, local: true) do
with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do
conn
|> json(true)
else
@@ -26,7 +26,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
end

def check_password(conn, %{"user" => username, "pass" => password}) do
with %User{password_hash: password_hash} <-
with %User{password_hash: password_hash, deactivated: false} <-
Repo.get_by(User, nickname: username, local: true),
true <- Pbkdf2.checkpw(password, password_hash) do
conn


+ 1
- 5
lib/pleroma/web/oauth/scopes.ex View File

@@ -17,12 +17,8 @@ defmodule Pleroma.Web.OAuth.Scopes do
"""
@spec fetch_scopes(map() | struct(), list()) :: list()

def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do
parse_scopes(scopes, default)
end

def fetch_scopes(params, default) do
parse_scopes(params["scope"] || params["scopes"], default)
parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default)
end

def parse_scopes(scopes, _default) when is_list(scopes) do


+ 46
- 62
lib/pleroma/web/twitter_api/twitter_api.ex View File

@@ -12,73 +12,57 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
require Pleroma.Constants

def register_user(params, opts \\ []) do
token = params["token"]
trusted_app? = params["trusted_app"]

params = %{
nickname: params["nickname"],
name: params["fullname"],
bio: User.parse_bio(params["bio"]),
email: params["email"],
password: params["password"],
password_confirmation: params["confirm"],
captcha_solution: params["captcha_solution"],
captcha_token: params["captcha_token"],
captcha_answer_data: params["captcha_answer_data"]
}

captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])
# true if captcha is disabled or enabled and valid, false otherwise
captcha_ok =
if trusted_app? || not captcha_enabled do
:ok
else
Pleroma.Captcha.validate(
params[:captcha_token],
params[:captcha_solution],
params[:captcha_answer_data]
)
end

# Captcha invalid
if captcha_ok != :ok do
{:error, error} = captcha_ok
# I have no idea how this error handling works
{:error, %{error: Jason.encode!(%{captcha: [error]})}}
else
registration_process(
params,
%{
registrations_open: Pleroma.Config.get([:instance, :registrations_open]),
token: token
},
opts
)
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])

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 registration_process(params, %{registrations_open: true}, opts) do
create_user(params, opts)
defp validate_captcha(params) do
if params[:trusted_app] || not Pleroma.Config.get([Pleroma.Captcha, :enabled]) do
:ok
else
Pleroma.Captcha.validate(
params.captcha_token,
params.captcha_solution,
params.captcha_answer_data
)
end
end

defp registration_process(params, %{token: token}, opts) do
invite =
unless is_nil(token) do
Repo.get_by(UserInviteToken, %{token: token})
end

valid_invite? = invite && UserInviteToken.valid_invite?(invite)

case invite do
nil ->
{:error, "Invalid token"}

invite when valid_invite? ->
UserInviteToken.update_usage!(invite)
create_user(params, opts)

_ ->
{:error, "Expired token"}
defp create_user_with_invite(params, opts) do
with %{token: token} when is_binary(token) <- params,
%UserInviteToken{} = invite <- Repo.get_by(UserInviteToken, %{token: token}),
true <- UserInviteToken.valid_invite?(invite) do
UserInviteToken.update_usage!(invite)
create_user(params, opts)
else
nil -> {:error, "Invalid token"}
_ -> {:error, "Expired token"}
end
end



+ 3
- 1
mix.exs View File

@@ -189,7 +189,9 @@ defmodule Pleroma.Mixfile do
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
{:mox, "~> 0.5", only: :test},
{:restarter, path: "./restarter"},
{:open_api_spex, "~> 3.6"}
{:open_api_spex,
git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git",
ref: "b862ebd78de0df95875cf46feb6e9607130dc2a8"}
] ++ oauth_deps()
end



+ 1
- 1
mix.lock View File

@@ -74,7 +74,7 @@
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
"oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"},
"open_api_spex": {:hex, :open_api_spex, "3.6.0", "64205aba9f2607f71b08fd43e3351b9c5e9898ec5ef49fc0ae35890da502ade9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "126ba3473966277132079cb1d5bf1e3df9e36fe2acd00166e75fd125cecb59c5"},
"open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "b862ebd78de0df95875cf46feb6e9607130dc2a8", [ref: "b862ebd78de0df95875cf46feb6e9607130dc2a8"]},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"},
"phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},


test/stat_test.exs → test/stats_test.exs View File

@@ -2,11 +2,21 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.StateTest do
defmodule Pleroma.StatsTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Web.CommonAPI

describe "user count" do
test "it ignores internal users" do
_user = insert(:user, local: true)
_internal = insert(:user, local: true, nickname: nil)
_internal = Pleroma.Web.ActivityPub.Relay.get_actor()

assert match?(%{stats: %{user_count: 1}}, Pleroma.Stats.calculate_stat_data())
end
end

describe "status visibility count" do
test "on new status" do
user = insert(:user)

+ 57
- 0
test/support/api_spec_helpers.ex View File

@@ -0,0 +1,57 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Tests.ApiSpecHelpers do
@moduledoc """
OpenAPI spec test helpers
"""

import ExUnit.Assertions

alias OpenApiSpex.Cast.Error
alias OpenApiSpex.Reference
alias OpenApiSpex.Schema

def assert_schema(value, schema) do
api_spec = Pleroma.Web.ApiSpec.spec()

case OpenApiSpex.cast_value(value, schema, api_spec) do
{:ok, data} ->
data

{:error, errors} ->
errors =
Enum.map(errors, fn error ->
message = Error.message(error)
path = Error.path_to_string(error)
"#{message} at #{path}"
end)

flunk(
"Value does not conform to schema #{schema.title}: #{Enum.join(errors, "\n")}\n#{
inspect(value)
}"
)
end
end

def resolve_schema(%Schema{} = schema), do: schema

def resolve_schema(%Reference{} = ref) do
schemas = Pleroma.Web.ApiSpec.spec().components.schemas
Reference.resolve_schema(ref, schemas)
end

def api_operations do
paths = Pleroma.Web.ApiSpec.spec().paths

Enum.flat_map(paths, fn {_, path_item} ->
path_item
|> Map.take([:delete, :get, :head, :options, :patch, :post, :put, :trace])
|> Map.values()
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
end)
end
end

+ 54
- 0
test/support/conn_case.ex View File

@@ -51,6 +51,60 @@ defmodule Pleroma.Web.ConnCase do
%{user: user, token: token, conn: conn}
end

defp request_content_type(%{conn: conn}) do
conn = put_req_header(conn, "content-type", "multipart/form-data")
[conn: conn]
end

defp json_response_and_validate_schema(
%{
private: %{
open_api_spex: %{operation_id: op_id, operation_lookup: lookup, spec: spec}
}
} = conn,
status
) do
content_type =
conn
|> Plug.Conn.get_resp_header("content-type")
|> List.first()
|> String.split(";")
|> List.first()

status = Plug.Conn.Status.code(status)

unless lookup[op_id].responses[status] do
err = "Response schema not found for #{conn.status} #{conn.method} #{conn.request_path}"
flunk(err)
end

schema = lookup[op_id].responses[status].content[content_type].schema
json = json_response(conn, status)

case OpenApiSpex.cast_value(json, schema, spec) do
{:ok, _data} ->
json

{:error, errors} ->
errors =
Enum.map(errors, fn error ->
message = OpenApiSpex.Cast.Error.message(error)
path = OpenApiSpex.Cast.Error.path_to_string(error)
"#{message} at #{path}"
end)

flunk(
"Response does not conform to schema of #{op_id} operation: #{
Enum.join(errors, "\n")
}\n#{inspect(json)}"
)
end
end

defp json_response_and_validate_schema(conn, _status) do
flunk("Response schema not found for #{conn.method} #{conn.request_path} #{conn.status}")
end

defp ensure_federating_or_authenticated(conn, url, user) do
initial_setting = Config.get([:instance, :federating])
on_exit(fn -> Config.put([:instance, :federating], initial_setting) end)


+ 500
- 70
test/web/activity_pub/activity_pub_test.exs View File

@@ -994,72 +994,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
end

describe "like an object" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)

user = insert(:user)

{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
assert called(Federator.publish(like_activity))
end

test "returns exist activity if object already liked" do
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)

user = insert(:user)

{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)

{:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity)
assert like_activity == like_activity_exist
end

test "reverts like activity on error" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)

with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.like(user, object)
end

assert Repo.aggregate(Activity, :count, :id) == 1
assert Repo.get(Object, object.id) == object
end

test "adds a like activity to the db" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity)

user = insert(:user)
user_two = insert(:user)

{:ok, like_activity, object} = ActivityPub.like(user, object)

assert like_activity.data["actor"] == user.ap_id
assert like_activity.data["type"] == "Like"
assert like_activity.data["object"] == object.data["id"]
assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
assert like_activity.data["context"] == object.data["context"]
assert object.data["like_count"] == 1
assert object.data["likes"] == [user.ap_id]

# Just return the original activity if the user already liked it.
{:ok, same_like_activity, object} = ActivityPub.like(user, object)

assert like_activity == same_like_activity
assert object.data["likes"] == [user.ap_id]
assert object.data["like_count"] == 1

{:ok, _like_activity, object} = ActivityPub.like(user_two, object)
assert object.data["like_count"] == 2
end
end

describe "unliking" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true)
@@ -1071,7 +1005,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
{:ok, object} = ActivityPub.unlike(user, object)
refute called(Federator.publish())

{:ok, _like_activity, object} = ActivityPub.like(user, object)
{:ok, _like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.get_by_id(object.id)
assert object.data["like_count"] == 1

{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
@@ -1082,10 +1017,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do

test "reverts unliking on error" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)

{:ok, like_activity, object} = ActivityPub.like(user, object)
{:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.normalize(note_activity)
assert object.data["like_count"] == 1

with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
@@ -1106,7 +1041,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
{:ok, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0

{:ok, like_activity, object} = ActivityPub.like(user, object)
{:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)

object = Object.get_by_id(object.id)
assert object.data["like_count"] == 1

{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
@@ -1973,4 +1910,497 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
ActivityPub.move(old_user, new_user)
end
end

test "doesn't retrieve replies activities with exclude_replies" do
user = insert(:user)

{:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})

{:ok, _reply} =
CommonAPI.post(user, %{"status" => "yeah", "in_reply_to_status_id" => activity.id})

[result] = ActivityPub.fetch_public_activities(%{"exclude_replies" => "true"})

assert result.id == activity.id

assert length(ActivityPub.fetch_public_activities()) == 2
end

describe "replies filtering with public messages" do
setup :public_messages

test "public timeline", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)

assert length(activities_ids) == 16
end

test "public timeline with reply_visibility `following`", %{
users: %{u1: user},
u1: u1,
u2: u2,
u3: u3,
u4: u4,
activities: activities
} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)

assert length(activities_ids) == 14

visible_ids =
Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]]

assert Enum.all?(visible_ids, &(&1 in activities_ids))
end

test "public timeline with reply_visibility `self`", %{
users: %{u1: user},
u1: u1,
u2: u2,
u3: u3,
u4: u4,
activities: activities
} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)

assert length(activities_ids) == 10
visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities)
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end

test "home timeline", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_filtering_user", user)

activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)

assert length(activities_ids) == 13

visible_ids =
Map.values(u1) ++
Map.values(u3) ++
[
activities[:a1],
activities[:a2],
activities[:a4],
u2[:r1],
u2[:r3],
u4[:r1],
u4[:r2]
]

assert Enum.all?(visible_ids, &(&1 in activities_ids))
end

test "home timeline with reply_visibility `following`", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)

activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)

assert length(activities_ids) == 11

visible_ids =
Map.values(u1) ++
[
activities[:a1],
activities[:a2],
activities[:a4],
u2[:r1],
u2[:r3],
u3[:r1],
u4[:r1],
u4[:r2]
]

assert Enum.all?(visible_ids, &(&1 in activities_ids))
end

test "home timeline with reply_visibility `self`", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)

activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)

assert length(activities_ids) == 9

visible_ids =
Map.values(u1) ++
[
activities[:a1],
activities[:a2],
activities[:a4],
u2[:r1],
u3[:r1],
u4[:r1]
]

assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
end

describe "replies filtering with private messages" do
setup :private_messages

test "public timeline", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)

assert activities_ids == []
end

test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)

assert activities_ids == []
end

test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)

assert activities_ids == []
end

test "home timeline", %{users: %{u1: user}} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)

activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)

assert length(activities_ids) == 12
end

test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)

activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)

assert length(activities_ids) == 12
end

test "home timeline with default reply_visibility `self`", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)

activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)

assert length(activities_ids) == 10

visible_ids =
Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities)

assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
end

defp public_messages(_) do
[u1, u2, u3, u4] = insert_list(4, :user)
{:ok, u1} = User.follow(u1, u2)
{:ok, u2} = User.follow(u2, u1)
{:ok, u1} = User.follow(u1, u4)
{:ok, u4} = User.follow(u4, u1)

{:ok, u2} = User.follow(u2, u3)
{:ok, u3} = User.follow(u3, u2)

{:ok, a1} = CommonAPI.post(u1, %{"status" => "Status"})

{:ok, r1_1} =
CommonAPI.post(u2, %{
"status" => "@#{u1.nickname} reply from u2 to u1",
"in_reply_to_status_id" => a1.id
})

{:ok, r1_2} =
CommonAPI.post(u3, %{
"status" => "@#{u1.nickname} reply from u3 to u1",
"in_reply_to_status_id" => a1.id
})

{:ok, r1_3} =
CommonAPI.post(u4, %{
"status" => "@#{u1.nickname} reply from u4 to u1",
"in_reply_to_status_id" => a1.id
})

{:ok, a2} = CommonAPI.post(u2, %{"status" => "Status"})

{:ok, r2_1} =
CommonAPI.post(u1, %{
"status" => "@#{u2.nickname} reply from u1 to u2",
"in_reply_to_status_id" => a2.id
})

{:ok, r2_2} =
CommonAPI.post(u3, %{
"status" => "@#{u2.nickname} reply from u3 to u2",
"in_reply_to_status_id" => a2.id
})

{:ok, r2_3} =
CommonAPI.post(u4, %{
"status" => "@#{u2.nickname} reply from u4 to u2",
"in_reply_to_status_id" => a2.id
})

{:ok, a3} = CommonAPI.post(u3, %{"status" => "Status"})

{:ok, r3_1} =
CommonAPI.post(u1, %{
"status" => "@#{u3.nickname} reply from u1 to u3",
"in_reply_to_status_id" => a3.id
})

{:ok, r3_2} =
CommonAPI.post(u2, %{
"status" => "@#{u3.nickname} reply from u2 to u3",
"in_reply_to_status_id" => a3.id
})

{:ok, r3_3} =
CommonAPI.post(u4, %{
"status" => "@#{u3.nickname} reply from u4 to u3",
"in_reply_to_status_id" => a3.id
})

{:ok, a4} = CommonAPI.post(u4, %{"status" => "Status"})

{:ok, r4_1} =
CommonAPI.post(u1, %{
"status" => "@#{u4.nickname} reply from u1 to u4",
"in_reply_to_status_id" => a4.id
})

{:ok, r4_2} =
CommonAPI.post(u2, %{
"status" => "@#{u4.nickname} reply from u2 to u4",
"in_reply_to_status_id" => a4.id
})

{:ok, r4_3} =
CommonAPI.post(u3, %{
"status" => "@#{u4.nickname} reply from u3 to u4",
"in_reply_to_status_id" => a4.id
})

{:ok,
users: %{u1: u1, u2: u2, u3: u3, u4: u4},
activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id},
u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id},
u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}}
end

defp private_messages(_) do
[u1, u2, u3, u4] = insert_list(4, :user)
{:ok, u1} = User.follow(u1, u2)
{:ok, u2} = User.follow(u2, u1)
{:ok, u1} = User.follow(u1, u3)
{:ok, u3} = User.follow(u3, u1)
{:ok, u1} = User.follow(u1, u4)
{:ok, u4} = User.follow(u4, u1)

{:ok, u2} = User.follow(u2, u3)
{:ok, u3} = User.follow(u3, u2)

{:ok, a1} = CommonAPI.post(u1, %{"status" => "Status", "visibility" => "private"})

{:ok, r1_1} =
CommonAPI.post(u2, %{
"status" => "@#{u1.nickname} reply from u2 to u1",
"in_reply_to_status_id" => a1.id,
"visibility" => "private"
})

{:ok, r1_2} =
CommonAPI.post(u3, %{
"status" => "@#{u1.nickname} reply from u3 to u1",
"in_reply_to_status_id" => a1.id,
"visibility" => "private"
})

{:ok, r1_3} =
CommonAPI.post(u4, %{
"status" => "@#{u1.nickname} reply from u4 to u1",
"in_reply_to_status_id" => a1.id,
"visibility" => "private"
})

{:ok, a2} = CommonAPI.post(u2, %{"status" => "Status", "visibility" => "private"})

{:ok, r2_1} =
CommonAPI.post(u1, %{
"status" => "@#{u2.nickname} reply from u1 to u2",
"in_reply_to_status_id" => a2.id,
"visibility" => "private"
})

{:ok, r2_2} =
CommonAPI.post(u3, %{
"status" => "@#{u2.nickname} reply from u3 to u2",
"in_reply_to_status_id" => a2.id,
"visibility" => "private"
})

{:ok, a3} = CommonAPI.post(u3, %{"status" => "Status", "visibility" => "private"})

{:ok, r3_1} =
CommonAPI.post(u1, %{
"status" => "@#{u3.nickname} reply from u1 to u3",
"in_reply_to_status_id" => a3.id,
"visibility" => "private"
})

{:ok, r3_2} =
CommonAPI.post(u2, %{
"status" => "@#{u3.nickname} reply from u2 to u3",
"in_reply_to_status_id" => a3.id,
"visibility" => "private"
})

{:ok, a4} = CommonAPI.post(u4, %{"status" => "Status", "visibility" => "private"})

{:ok, r4_1} =
CommonAPI.post(u1, %{
"status" => "@#{u4.nickname} reply from u1 to u4",
"in_reply_to_status_id" => a4.id,
"visibility" => "private"
})

{:ok,
users: %{u1: u1, u2: u2, u3: u3, u4: u4},
activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
u2: %{r1: r2_1.id, r2: r2_2.id},
u3: %{r1: r3_1.id, r2: r3_2.id},
u4: %{r1: r4_1.id}}
end
end

+ 2
- 3
test/web/activity_pub/utils_test.exs View File

@@ -224,8 +224,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do

object = Object.normalize(activity)
{:ok, [vote], object} = CommonAPI.vote(other_user, object, [0])
vote_object = Object.normalize(vote)
{:ok, _activity, _object} = ActivityPub.like(user, vote_object)
{:ok, _activity} = CommonAPI.favorite(user, activity.id)
[fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object)
assert fetched_vote.id == vote.id
end
@@ -346,7 +345,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do

user = insert(:user)
refute Utils.get_existing_like(user.ap_id, object)
{:ok, like_activity, _object} = ActivityPub.like(user, object)
{:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)

assert ^like_activity = Utils.get_existing_like(user.ap_id, object)
end


+ 0
- 45
test/web/api_spec/app_operation_test.exs View File

@@ -1,45 +0,0 @@
# 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.AppOperationTest do
use Pleroma.Web.ConnCase, async: true

alias Pleroma.Web.ApiSpec
alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse

import OpenApiSpex.TestAssertions
import Pleroma.Factory

test "AppCreateRequest example matches schema" do
api_spec = ApiSpec.spec()
schema = AppCreateRequest.schema()
assert_schema(schema.example, "AppCreateRequest", api_spec)
end

test "AppCreateResponse example matches schema" do
api_spec = ApiSpec.spec()
schema = AppCreateResponse.schema()
assert_schema(schema.example, "AppCreateResponse", api_spec)
end

test "AppController produces a AppCreateResponse", %{conn: conn} do
api_spec = ApiSpec.spec()
app_attrs = build(:oauth_app)

json =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/apps",
Jason.encode!(%{
client_name: app_attrs.client_name,
redirect_uris: app_attrs.redirect_uris
})
)
|> json_response(200)

assert_schema(json, "AppCreateResponse", api_spec)
end
end

+ 43
- 0
test/web/api_spec/schema_examples_test.exs View File

@@ -0,0 +1,43 @@
# 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.SchemaExamplesTest do
use ExUnit.Case, async: true
import Pleroma.Tests.ApiSpecHelpers

@content_type "application/json"

for operation <- api_operations() do
describe operation.operationId <> " Request Body" do
if operation.requestBody do
@media_type operation.requestBody.content[@content_type]
@schema resolve_schema(@media_type.schema)

if @media_type.example do
test "request body media type example matches schema" do
assert_schema(@media_type.example, @schema)
end
end

if @schema.example do
test "request body schema example matches schema" do
assert_schema(@schema.example, @schema)
end
end
end
end

for {status, response} <- operation.responses do
describe "#{operation.operationId} - #{status} Response" do
@schema resolve_schema(response.content[@content_type].schema)

if @schema.example do
test "example matches schema" do
assert_schema(@schema.example, @schema)
end
end
end
end
end
end

+ 69
- 3
test/web/common_api/common_api_test.exs View File

@@ -21,6 +21,60 @@ defmodule Pleroma.Web.CommonAPITest do
setup do: clear_config([:instance, :limit])
setup do: clear_config([:instance, :max_pinned_statuses])

test "favoriting race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
users = insert_list(10, :user)

{:ok, activity} = CommonAPI.post(user, %{"status" => "."})

users_serial
|> Enum.map(fn user ->
CommonAPI.favorite(user, activity.id)
end)

object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 10

users
|> Enum.map(fn user ->
Task.async(fn ->
CommonAPI.favorite(user, activity.id)
end)
end)
|> Enum.map(&Task.await/1)

object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 20
end

test "repeating race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
users = insert_list(10, :user)

{:ok, activity} = CommonAPI.post(user, %{"status" => "."})

users_serial
|> Enum.map(fn user ->
CommonAPI.repeat(activity.id, user)
end)

object = Object.get_by_ap_id(activity.data["object"])
assert object.data["announcement_count"] == 10

users
|> Enum.map(fn user ->
Task.async(fn ->
CommonAPI.repeat(activity.id, user)
end)
end)
|> Enum.map(&Task.await/1)

object = Object.get_by_ap_id(activity.data["object"])
assert object.data["announcement_count"] == 20
end

test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
@@ -256,6 +310,16 @@ defmodule Pleroma.Web.CommonAPITest do
{:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user)
end

test "can't repeat a repeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})

{:ok, %Activity{} = announce, _} = CommonAPI.repeat(activity.id, other_user)

refute match?({:ok, %Activity{}, _}, CommonAPI.repeat(announce.id, user))
end

test "repeating a status privately" do
user = insert(:user)
other_user = insert(:user)
@@ -285,8 +349,8 @@ defmodule Pleroma.Web.CommonAPITest do
other_user = insert(:user)

{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{} = activity, object} = CommonAPI.repeat(activity.id, user)
{:ok, ^activity, ^object} = CommonAPI.repeat(activity.id, user)
{:ok, %Activity{} = announce, object} = CommonAPI.repeat(activity.id, user)
{:ok, ^announce, ^object} = CommonAPI.repeat(activity.id, user)
end

test "favoriting a status twice returns ok, but without the like activity" do
@@ -360,7 +424,9 @@ defmodule Pleroma.Web.CommonAPITest do

user = refresh_record(user)

assert {:ok, ^activity} = CommonAPI.unpin(activity.id, user)
id = activity.id

assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user))

user = refresh_record(user)



+ 0
- 20
test/web/common_api/common_api_utils_test.exs View File

@@ -335,26 +335,6 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
end
end

describe "get_by_id_or_ap_id/1" do
test "get activity by id" do
activity = insert(:note_activity)
%Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.id)
assert note.id == activity.id
end

test "get activity by ap_id" do
activity = insert(:note_activity)
%Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.data["object"])
assert note.id == activity.id
end

test "get activity by object when type isn't `Create` " do
activity = insert(:like_activity)
%Pleroma.Activity{} = like = Utils.get_by_id_or_ap_id(activity.id)
assert like.data["object"] == activity.data["object"]
end
end

describe "to_master_date/1" do
test "removes microseconds from date (NaiveDateTime)" do
assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z"


+ 34
- 32
test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs View File

@@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do

describe "updating credentials" do
setup do: oauth_access(["write:accounts"])
setup :request_content_type

test "sets user settings in a generic way", %{conn: conn} do
res_conn =
@@ -25,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
}
})

assert user_data = json_response(res_conn, 200)
assert user_data = json_response_and_validate_schema(res_conn, 200)
assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}}

user = Repo.get(User, user_data["id"])
@@ -41,7 +42,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
}
})

assert user_data = json_response(res_conn, 200)
assert user_data = json_response_and_validate_schema(res_conn, 200)

assert user_data["pleroma"]["settings_store"] ==
%{
@@ -62,7 +63,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
}
})

assert user_data = json_response(res_conn, 200)
assert user_data = json_response_and_validate_schema(res_conn, 200)

assert user_data["pleroma"]["settings_store"] ==
%{
@@ -79,7 +80,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
"note" => "I drink #cofe with @#{user2.nickname}\n\nsuya.."
})

assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)

assert user_data["note"] ==
~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a class="u-url mention" data-user="#{
@@ -90,7 +91,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
test "updates the user's locking status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"})

assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["locked"] == true
end

@@ -100,21 +101,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"})

assert refresh_record(user).allow_following_move == false
assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["allow_following_move"] == false
end

test "updates the user's default scope", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "cofe"})
conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "unlisted"})

assert user_data = json_response(conn, 200)
assert user_data["source"]["privacy"] == "cofe"
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["source"]["privacy"] == "unlisted"
end

test "updates the user's hide_followers status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"})

assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["hide_followers"] == true
end

@@ -122,12 +123,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} =
conn
|> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"})
|> json_response(:ok)
|> json_response_and_validate_schema(:ok)

assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} =
conn
|> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"})
|> json_response(:ok)
|> json_response_and_validate_schema(:ok)
end

test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do
@@ -137,7 +138,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
hide_follows_count: "true"
})

assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["hide_followers_count"] == true
assert user_data["pleroma"]["hide_follows_count"] == true
end
@@ -146,7 +147,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
response =
conn
|> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"})
|> json_response(200)
|> json_response_and_validate_schema(200)

assert response["pleroma"]["skip_thread_containment"] == true
assert refresh_record(user).skip_thread_containment
@@ -155,28 +156,28 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
test "updates the user's hide_follows status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"})

assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["hide_follows"] == true
end

test "updates the user's hide_favorites status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"})

assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["hide_favorites"] == true
end

test "updates the user's show_role status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"})

assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["source"]["pleroma"]["show_role"] == false
end

test "updates the user's no_rich_text status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"})

assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["source"]["pleroma"]["no_rich_text"] == true
end

@@ -184,7 +185,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
conn =
patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})

assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["display_name"] == "markorepairs"
end

@@ -197,7 +198,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do

conn = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})

assert user_response = json_response(conn, 200)
assert user_response = json_response_and_validate_schema(conn, 200)
assert user_response["avatar"] != User.avatar_url(user)
end

@@ -210,7 +211,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do

conn = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header})

assert user_response = json_response(conn, 200)
assert user_response = json_response_and_validate_schema(conn, 200)
assert user_response["header"] != User.banner_url(user)
end

@@ -226,7 +227,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
"pleroma_background_image" => new_header
})

assert user_response = json_response(conn, 200)
assert user_response = json_response_and_validate_schema(conn, 200)
assert user_response["pleroma"]["background_image"]
end

@@ -237,14 +238,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
for token <- [token1, token2] do
conn =
build_conn()
|> put_req_header("content-type", "multipart/form-data")
|> put_req_header("authorization", "Bearer #{token.token}")
|> patch("/api/v1/accounts/update_credentials", %{})

if token == token1 do
assert %{"error" => "Insufficient permissions: write:accounts."} ==
json_response(conn, 403)
json_response_and_validate_schema(conn, 403)
else
assert json_response(conn, 200)
assert json_response_and_validate_schema(conn, 200)
end
end
end
@@ -259,11 +261,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
"display_name" => name
})

assert json_response(ret_conn, 200)
assert json_response_and_validate_schema(ret_conn, 200)

conn = get(conn, "/api/v1/accounts/#{user.id}")

assert user_data = json_response(conn, 200)
assert user_data = json_response_and_validate_schema(conn, 200)

assert user_data["note"] == note
assert user_data["display_name"] == name
@@ -279,7 +281,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
account_data =
conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(200)
|> json_response_and_validate_schema(200)

assert account_data["fields"] == [
%{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"},
@@ -312,7 +314,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
conn
|> put_req_header("content-type", "application/x-www-form-urlencoded")
|> patch("/api/v1/accounts/update_credentials", fields)
|> json_response(200)
|> json_response_and_validate_schema(200)

assert account["fields"] == [
%{"name" => "foo", "value" => "bar"},
@@ -337,7 +339,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(200)
|> json_response_and_validate_schema(200)

assert account["fields"] == [
%{"name" => "foo", "value" => ""}
@@ -356,14 +358,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
assert %{"error" => "Invalid request"} ==
conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403)
|> json_response_and_validate_schema(403)

fields = [%{"name" => long_name, "value" => "bar"}]

assert %{"error" => "Invalid request"} ==
conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403)
|> json_response_and_validate_schema(403)

Pleroma.Config.put([:instance, :max_account_fields], 1)

@@ -375,7 +377,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
assert %{"error" => "Invalid request"} ==
conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403)
|> json_response_and_validate_schema(403)
end
end
end

+ 323
- 284
test/web/mastodon_api/controllers/account_controller_test.exs
File diff suppressed because it is too large
View File


+ 2
- 2
test/web/mastodon_api/controllers/app_controller_test.exs View File

@@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
"vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
}

assert expected == json_response(conn, 200)
assert expected == json_response_and_validate_schema(conn, 200)
end

test "creates an oauth app", %{conn: conn} do
@@ -55,6 +55,6 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
"vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
}

assert expected == json_response(conn, 200)
assert expected == json_response_and_validate_schema(conn, 200)
end
end

+ 1
- 19
test/web/mastodon_api/controllers/custom_emoji_controller_test.exs View File

@@ -4,16 +4,12 @@

defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Web.ApiSpec
alias Pleroma.Web.ApiSpec.Schemas.CustomEmoji
alias Pleroma.Web.ApiSpec.Schemas.CustomEmojisResponse
import OpenApiSpex.TestAssertions

test "with tags", %{conn: conn} do
assert resp =
conn
|> get("/api/v1/custom_emojis")
|> json_response(200)
|> json_response_and_validate_schema(200)

assert [emoji | _body] = resp
assert Map.has_key?(emoji, "shortcode")
@@ -23,19 +19,5 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do
assert Map.has_key?(emoji, "category")
assert Map.has_key?(emoji, "url")
assert Map.has_key?(emoji, "visible_in_picker")
assert_schema(resp, "CustomEmojisResponse", ApiSpec.spec())
assert_schema(emoji, "CustomEmoji", ApiSpec.spec())
end

test "CustomEmoji example matches schema" do
api_spec = ApiSpec.spec()
schema = CustomEmoji.schema()
assert_schema(schema.example, "CustomEmoji", api_spec)
end

test "CustomEmojisResponse example matches schema" do
api_spec = ApiSpec.spec()
schema = CustomEmojisResponse.schema()
assert_schema(schema.example, "CustomEmojisResponse", api_spec)
end
end

+ 7
- 21
test/web/mastodon_api/controllers/domain_block_controller_test.exs View File

@@ -6,11 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
use Pleroma.Web.ConnCase

alias Pleroma.User
alias Pleroma.Web.ApiSpec
alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse

import Pleroma.Factory
import OpenApiSpex.TestAssertions

test "blocking / unblocking a domain" do
%{user: user, conn: conn} = oauth_access(["write:blocks"])
@@ -21,7 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
|> put_req_header("content-type", "application/json")
|> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})

assert %{} = json_response(ret_conn, 200)
assert %{} == json_response_and_validate_schema(ret_conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
assert User.blocks?(user, other_user)

@@ -30,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
|> put_req_header("content-type", "application/json")
|> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})

assert %{} = json_response(ret_conn, 200)
assert %{} == json_response_and_validate_schema(ret_conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
refute User.blocks?(user, other_user)
end
@@ -41,21 +38,10 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
{:ok, user} = User.block_domain(user, "bad.site")
{:ok, user} = User.block_domain(user, "even.worse.site")

conn =
conn
|> assign(:user, user)
|> get("/api/v1/domain_blocks")

domain_blocks = json_response(conn, 200)

assert "bad.site" in domain_blocks
assert "even.worse.site" in domain_blocks
assert_schema(domain_blocks, "DomainBlocksResponse", ApiSpec.spec())
end

test "DomainBlocksResponse example matches schema" do
api_spec = ApiSpec.spec()
schema = DomainBlocksResponse.schema()
assert_schema(schema.example, "DomainBlocksResponse", api_spec)
assert ["even.worse.site", "bad.site"] ==
conn
|> assign(:user, user)
|> get("/api/v1/domain_blocks")
|> json_response_and_validate_schema(200)
end
end

+ 11
- 0
test/web/mastodon_api/controllers/status_controller_test.exs View File

@@ -302,6 +302,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert [] == Repo.all(Activity)
end

test "ignores nil values", %{conn: conn} do
conn =
post(conn, "/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => nil
})

assert result = json_response(conn, 200)
assert Activity.get_by_id(result["id"])
end

test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)



+ 22
- 0
test/web/mongooseim/mongoose_im_controller_test.exs View File

@@ -9,6 +9,7 @@ defmodule Pleroma.Web.MongooseIMController do
test "/user_exists", %{conn: conn} do
_user = insert(:user, nickname: "lain")
_remote_user = insert(:user, nickname: "alice", local: false)
_deactivated_user = insert(:user, nickname: "konata", deactivated: true)

res =
conn
@@ -30,11 +31,25 @@ defmodule Pleroma.Web.MongooseIMController do
|> json_response(404)

assert res == false

res =
conn
|> get(mongoose_im_path(conn, :user_exists), user: "konata")
|> json_response(404)

assert res == false
end

test "/check_password", %{conn: conn} do
user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt("cool"))

_deactivated_user =
insert(:user,
nickname: "konata",
deactivated: true,
password_hash: Comeonin.Pbkdf2.hashpwsalt("cool")
)

res =
conn
|> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "cool")
@@ -51,6 +66,13 @@ defmodule Pleroma.Web.MongooseIMController do

res =
conn
|> get(mongoose_im_path(conn, :check_password), user: "konata", pass: "cool")
|> json_response(404)

assert res == false

res =
conn
|> get(mongoose_im_path(conn, :check_password), user: "nobody", pass: "cool")
|> json_response(404)



+ 111
- 111
test/web/twitter_api/twitter_api_test.exs View File

@@ -18,11 +18,11 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do

test "it registers a new user and returns the user." do
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"password" => "bear",
"confirm" => "bear"
:nickname => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:password => "bear",
:confirm => "bear"
}

{:ok, user} = TwitterAPI.register_user(data)
@@ -35,12 +35,12 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do

test "it registers a new user with empty string in bio and returns the user." do
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "",
"password" => "bear",
"confirm" => "bear"
:nickname => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "",
:password => "bear",
:confirm => "bear"
}

{:ok, user} = TwitterAPI.register_user(data)
@@ -60,12 +60,12 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
end

data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "",
"password" => "bear",
"confirm" => "bear"
:nickname => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "",
:password => "bear",
:confirm => "bear"
}

{:ok, user} = TwitterAPI.register_user(data)
@@ -87,23 +87,23 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do

test "it registers a new user and parses mentions in the bio" do
data1 = %{
"nickname" => "john",
"email" => "john@gmail.com",
"fullname" => "John Doe",
"bio" => "test",
"password" => "bear",
"confirm" => "bear"
:nickname => "john",
:email => "john@gmail.com",
:fullname => "John Doe",
:bio => "test",
:password => "bear",
:confirm => "bear"
}

{:ok, user1} = TwitterAPI.register_user(data1)

data2 = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "@john test",
"password" => "bear",
"confirm" => "bear"
:nickname => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "@john test",
:password => "bear",
:confirm => "bear"
}

{:ok, user2} = TwitterAPI.register_user(data2)
@@ -123,13 +123,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
{:ok, invite} = UserInviteToken.create_invite()

data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees",
"token" => invite.token
:nickname => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
:password => "hiptofbees",
:confirm => "hiptofbees",
:token => invite.token
}

{:ok, user} = TwitterAPI.register_user(data)
@@ -145,13 +145,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do

test "returns error on invalid token" do
data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => "DudeLetMeInImAFairy"
:nickname => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => "DudeLetMeInImAFairy"
}

{:error, msg} = TwitterAPI.register_user(data)
@@ -165,13 +165,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
UserInviteToken.update_invite!(invite, used: true)

data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => invite.token
:nickname => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => invite.token
}

{:error, msg} = TwitterAPI.register_user(data)
@@ -186,16 +186,16 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do

setup do
data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees"
:nickname => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
:password => "hiptofbees",
:confirm => "hiptofbees"
}

check_fn = fn invite ->
data = Map.put(data, "token", invite.token)
data = Map.put(data, :token, invite.token)
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = User.get_cached_by_nickname("vinny")

@@ -250,13 +250,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
UserInviteToken.update_invite!(invite, uses: 99)

data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees",
"token" => invite.token
:nickname => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
:password => "hiptofbees",
:confirm => "hiptofbees",
:token => invite.token
}

{:ok, user} = TwitterAPI.register_user(data)
@@ -269,13 +269,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
AccountView.render("show.json", %{user: fetched_user})

data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => invite.token
:nickname => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => invite.token
}

{:error, msg} = TwitterAPI.register_user(data)
@@ -292,13 +292,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})

data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees",
"token" => invite.token
:nickname => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
:password => "hiptofbees",
:confirm => "hiptofbees",
:token => invite.token
}

{:ok, user} = TwitterAPI.register_user(data)
@@ -317,13 +317,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
UserInviteToken.update_invite!(invite, uses: 99)

data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees",
"token" => invite.token
:nickname => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
:password => "hiptofbees",
:confirm => "hiptofbees",
:token => invite.token
}

{:ok, user} = TwitterAPI.register_user(data)
@@ -335,13 +335,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
AccountView.render("show.json", %{user: fetched_user})

data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => invite.token
:nickname => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => invite.token
}

{:error, msg} = TwitterAPI.register_user(data)
@@ -355,13 +355,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})

data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => invite.token
:nickname => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => invite.token
}

{:error, msg} = TwitterAPI.register_user(data)
@@ -377,13 +377,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
UserInviteToken.update_invite!(invite, uses: 100)

data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => invite.token
:nickname => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => invite.token
}

{:error, msg} = TwitterAPI.register_user(data)
@@ -395,11 +395,11 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do

test "it returns the error on registration problems" do
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "close the world.",
"password" => "bear"
:nickname => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "close the world.",
:password => "bear"
}

{:error, error_object} = TwitterAPI.register_user(data)


Loading…
Cancel
Save