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

# Conflicts:
#	lib/pleroma/web/mastodon_api/controllers/account_controller.ex
This commit is contained in:
Ivan Tashkinov 2020-04-28 19:56:20 +03:00
commit 908cf22a6c
67 changed files with 3285 additions and 1001 deletions

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> <summary>API Changes</summary>
- Mastodon API: Support for `include_types` in `/api/v1/notifications`. - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint. - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Mastodon API: Add support for filtering replies in public and home timelines
- Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoints for create/update/delete OAuth Apps.
</details> </details>

View file

@ -279,7 +279,7 @@ defp insert_activity("like", visibility, group, user, friends, non_friends, opts
actor = get_actor(group, user, friends, non_friends) actor = get_actor(group, user, friends, non_friends)
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), 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 :ok
else else
{:error, _} -> {:error, _} ->
@ -313,7 +313,7 @@ defp insert_activity("simple_thread", visibility, group, user, friends, non_frie
tasks = get_reply_tasks(visibility, group) tasks = get_reply_tasks(visibility, group)
{:ok, activity} = {: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"]} acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
insert_replies(tasks, visibility, user, friends, non_friends, acc) insert_replies(tasks, visibility, user, friends, non_friends, acc)

View file

@ -41,6 +41,7 @@ defp fetch_timelines(user) do
fetch_notifications(user) fetch_notifications(user)
fetch_favourites(user) fetch_favourites(user)
fetch_long_thread(user) fetch_long_thread(user)
fetch_timelines_with_reply_filtering(user)
end end
defp render_views(user) do defp render_views(user) do
@ -495,4 +496,58 @@ defp render_long_thread(user) do
formatters: formatters() formatters: formatters()
) )
end 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 end

View file

@ -44,6 +44,7 @@ defmodule Mix.Tasks.Pleroma.LoadTesting do
] ]
def run(args) do def run(args) do
Logger.configure(level: :error)
Mix.Pleroma.start_pleroma() Mix.Pleroma.start_pleroma()
clean_tables() clean_tables()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)

View file

@ -4,7 +4,7 @@ A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma
## Flake IDs ## 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 ## 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 `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 `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 ## 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`. - `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`. - `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` ## POST `/api/v1/statuses`
Additional parameters can be added to the JSON body/Form data: Additional parameters can be added to the JSON body/Form data:

View file

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

View file

@ -75,7 +75,7 @@ defp csp_string do
"default-src 'none'", "default-src 'none'",
"base-uri 'self'", "base-uri 'self'",
"frame-ancestors 'none'", "frame-ancestors 'none'",
"img-src 'self' data: https:", "img-src 'self' data: blob: https:",
"media-src 'self' https:", "media-src 'self' https:",
"style-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'",
"font-src 'self'", "font-src 'self'",

View file

@ -45,11 +45,11 @@ def get_peers do
end end
def init(_args) do def init(_args) do
{:ok, get_stat_data()} {:ok, calculate_stat_data()}
end end
def handle_call(:force_update, _from, _state) do def handle_call(:force_update, _from, _state) do
new_stats = get_stat_data() new_stats = calculate_stat_data()
{:reply, new_stats, new_stats} {:reply, new_stats, new_stats}
end end
@ -58,12 +58,12 @@ def handle_call(:get_state, _from, state) do
end end
def handle_cast(:run_update, _state) do def handle_cast(:run_update, _state) do
new_stats = get_stat_data() new_stats = calculate_stat_data()
{:noreply, new_stats} {:noreply, new_stats}
end end
defp get_stat_data do def calculate_stat_data do
peers = peers =
from( from(
u in User, u in User,
@ -77,7 +77,15 @@ defp get_stat_data do
status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count) 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, peers: peers,

View file

@ -832,6 +832,7 @@ def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", 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} {:ok, user}
end end
@ -847,9 +848,22 @@ def update_and_set_cache(changeset) do
end end
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 def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}") Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}") Cachex.del(:user_cache, "nickname:#{user.nickname}")
Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
end end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil @spec get_cached_by_ap_id(String.t()) :: User.t() | nil

View file

@ -54,13 +54,13 @@ defmodule Pleroma.User.Query do
select: term(), select: term(),
limit: pos_integer() limit: pos_integer()
} }
| %{} | map()
@ilike_criteria [:nickname, :name, :query] @ilike_criteria [:nickname, :name, :query]
@equal_criteria [:email] @equal_criteria [:email]
@contains_criteria [:ap_id, :nickname] @contains_criteria [:ap_id, :nickname]
@spec build(criteria()) :: Query.t() @spec build(Query.t(), criteria()) :: Query.t()
def build(query \\ base_query(), criteria) do def build(query \\ base_query(), criteria) do
prepare_query(query, criteria) prepare_query(query, criteria)
end end

View file

@ -398,36 +398,6 @@ defp do_unreact_with_emoji(user, reaction_id, options) do
end end
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()) :: @spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()} {:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
@ -468,6 +438,7 @@ def announce(
defp do_announce(user, object, activity_id, local, public) do defp do_announce(user, object, activity_id, local, public) do
with true <- is_announceable?(object, user, public), with true <- is_announceable?(object, user, public),
object <- Object.get_by_id(object.id),
announce_data <- make_announce_data(user, object, activity_id, public), announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local), {:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object), {:ok, object} <- add_announce_to_object(activity, object),
@ -854,7 +825,7 @@ defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
end end
defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) 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}") Logger.error("Could not exclude visibility to #{visibility}")
query query
end end
@ -1061,7 +1032,7 @@ defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
raise "Can't use the child object without preloading!" raise "Can't use the child object without preloading!"
end 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( from(
[_activity, object] in query, [_activity, object] in query,
where: fragment("not (?)->'attachment' = (?)", object.data, ^[]) where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
@ -1070,16 +1041,51 @@ defp restrict_media(query, %{"only_media" => val}) when val == "true" or val ==
defp restrict_media(query, _), do: query 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( from(
[_activity, object] in query, [_activity, object] in query,
where: fragment("?->>'inReplyTo' is null", object.data) where: fragment("?->>'inReplyTo' is null", object.data)
) )
end 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_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)) from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end end
@ -1158,7 +1164,12 @@ defp restrict_unlisted(query) do
) )
end 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) from(activity in query, where: activity.id in ^ids)
end end
@ -1291,6 +1302,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> maybe_set_thread_muted_field(opts) |> maybe_set_thread_muted_field(opts)
|> maybe_order(opts) |> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"]) |> restrict_recipients(recipients, opts["user"])
|> restrict_replies(opts)
|> restrict_tag(opts) |> restrict_tag(opts)
|> restrict_tag_reject(opts) |> restrict_tag_reject(opts)
|> restrict_tag_all(opts) |> restrict_tag_all(opts)
@ -1305,7 +1317,6 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_media(opts) |> restrict_media(opts)
|> restrict_visibility(opts) |> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config) |> restrict_thread_visibility(opts, config)
|> restrict_replies(opts)
|> restrict_reblogs(opts) |> restrict_reblogs(opts)
|> restrict_pinned(opts) |> restrict_pinned(opts)
|> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_muted_reblogs(restrict_muted_reblogs_opts)

View file

@ -12,8 +12,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Plugs.EnsureAuthenticatedPlug alias Pleroma.Plugs.EnsureAuthenticatedPlug
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.UserView
@ -421,7 +423,10 @@ defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
with %Object{} = object <- Object.normalize(params["object"]), 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} {:ok, activity}
else else
_ -> {:error, dgettext("errors", "Can't like object")} _ -> {:error, dgettext("errors", "Can't like object")}

View file

@ -15,12 +15,17 @@ def handle(object, meta \\ [])
# - Add like to object # - Add like to object
# - Set up notification # - Set up notification
def handle(%{data: %{"type" => "Like"}} = object, meta) do def handle(%{data: %{"type" => "Like"}} = object, meta) do
{:ok, result} =
Pleroma.Repo.transaction(fn ->
liked_object = Object.get_by_ap_id(object.data["object"]) liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_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 end
# Nothing to do # Nothing to do

View file

@ -8,15 +8,16 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
require Pleroma.Constants require Pleroma.Constants
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", opts) do def render("index.json", opts) do
safe_render_many(opts.activities, __MODULE__, "show.json", opts) safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do 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)}) |> Map.merge(%{account: merge_account_views(user)})
end end
@ -26,17 +27,4 @@ defp merge_account_views(%User{} = user) do
end end
defp merge_account_views(_), do: %{} 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 end

View file

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

View file

@ -3,6 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Helpers do defmodule Pleroma.Web.ApiSpec.Helpers do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
def request_body(description, schema_ref, opts \\ []) do def request_body(description, schema_ref, opts \\ []) do
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"] media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
@ -24,4 +27,23 @@ def request_body(description, schema_ref, opts \\ []) do
required: opts[:required] || false required: opts[:required] || false
} }
end 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 end

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

View file

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

View file

@ -4,7 +4,8 @@
defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias Pleroma.Web.ApiSpec.Schemas.CustomEmojisResponse alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Emoji
def open_api_operation(action) do def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation") operation = String.to_existing_atom("#{action}_operation")
@ -18,7 +19,69 @@ def index_operation do
description: "Returns custom emojis that are available on the server.", description: "Returns custom emojis that are available on the server.",
operationId: "CustomEmojiController.index", operationId: "CustomEmojiController.index",
responses: %{ 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 end

View file

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

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

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

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

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

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

View file

@ -2,19 +2,18 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest do defmodule Pleroma.Web.ApiSpec.Schemas.ApiError do
alias OpenApiSpex.Schema alias OpenApiSpex.Schema
require OpenApiSpex require OpenApiSpex
OpenApiSpex.schema(%{ OpenApiSpex.schema(%{
title: "DomainBlockRequest", title: "ApiError",
description: "Response schema for API error",
type: :object, type: :object,
properties: %{ properties: %{error: %Schema{type: :string}},
domain: %Schema{type: :string}
},
required: [:domain],
example: %{ example: %{
"domain" => "facebook.com" "error" => "Something went wrong"
} }
}) })
end end

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

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

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

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

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

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

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

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

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

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

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

View file

@ -84,14 +84,18 @@ defp attachments(%{params: params} = draft) do
%__MODULE__{draft | attachments: attachments} %__MODULE__{draft | attachments: attachments}
end end
defp in_reply_to(draft) do defp in_reply_to(%{params: %{"in_reply_to_status_id" => ""}} = draft), do: draft
case Map.get(draft.params, "in_reply_to_status_id") do
"" -> draft defp in_reply_to(%{params: %{"in_reply_to_status_id" => id}} = draft) when is_binary(id) do
nil -> draft %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
end 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 end
defp in_reply_to(draft), do: draft
defp in_reply_to_conversation(draft) do defp in_reply_to_conversation(draft) do
in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"]) in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}

View file

@ -86,8 +86,9 @@ def delete(activity_id, user) do
end end
end end
def repeat(id_or_ap_id, user, params \\ %{}) do def repeat(id, user, params \\ %{}) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)}, with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
object <- Object.normalize(activity), object <- Object.normalize(activity),
announce_activity <- Utils.get_existing_announce(user.ap_id, object), announce_activity <- Utils.get_existing_announce(user.ap_id, object),
public <- public_announce?(object, params) do public <- public_announce?(object, params) do
@ -102,8 +103,9 @@ def repeat(id_or_ap_id, user, params \\ %{}) do
end end
end end
def unrepeat(id_or_ap_id, user) do def unrepeat(id, user) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)} do
object = Object.normalize(activity) object = Object.normalize(activity)
ActivityPub.unannounce(user, object) ActivityPub.unannounce(user, object)
else else
@ -160,8 +162,9 @@ def favorite_helper(user, id) do
end end
end end
def unfavorite(id_or_ap_id, user) do def unfavorite(id, user) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)} do
object = Object.normalize(activity) object = Object.normalize(activity)
ActivityPub.unlike(user, object) ActivityPub.unlike(user, object)
else else
@ -332,12 +335,12 @@ defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expire
defp maybe_create_activity_expiration(result, _), do: result 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{ with %Activity{
actor: ^user_ap_id, actor: ^user_ap_id,
data: %{"type" => "Create"}, data: %{"type" => "Create"},
object: %Object{data: %{"type" => object_type}} 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 <- object_type in ["Note", "Article", "Question"],
true <- Visibility.is_public?(activity), true <- Visibility.is_public?(activity),
{:ok, _user} <- User.add_pinnned_activity(user, activity) do {:ok, _user} <- User.add_pinnned_activity(user, activity) do
@ -348,8 +351,8 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
end end
end end
def unpin(id_or_ap_id, user) do def unpin(id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
{:ok, _user} <- User.remove_pinnned_activity(user, activity) do {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
{:ok, activity} {:ok, activity}
else else

View file

@ -22,24 +22,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
require Logger require Logger
require Pleroma.Constants 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 def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
attachments_from_ids_descs(ids, desc) attachments_from_ids_descs(ids, desc)
end end

View file

@ -82,8 +82,9 @@ def add_link_headers(conn, activities, extra_params) do
end end
end end
def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do def assign_account_by_id(conn, _) do
case Pleroma.User.get_cached_by_id(id) 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) %Pleroma.User{} = account -> assign(conn, :account, account)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end end

View file

@ -27,6 +27,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI 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, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses]) 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) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
@doc "POST /api/v1/accounts" @doc "POST /api/v1/accounts"
def create( def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
%{assigns: %{app: app}} = conn,
%{"username" => nickname, "password" => _, "agreement" => true} = params
) do
params = params =
params params
|> Map.take([ |> Map.take([
"email", :email,
"captcha_solution", :bio,
"captcha_token", :captcha_solution,
"captcha_answer_data", :captcha_token,
"token", :captcha_answer_data,
"password" :token,
:password,
:fullname
]) ])
|> Map.put("nickname", nickname) |> Map.put(:nickname, params.username)
|> Map.put("fullname", params["fullname"] || nickname) |> Map.put(:fullname, Map.get(params, :fullname, params.username))
|> Map.put("bio", params["bio"] || "") |> Map.put(:confirm, params.password)
|> Map.put("confirm", params["password"]) |> Map.put(:trusted_app, app.trusted)
|> Map.put("trusted_app", app.trusted)
with :ok <- validate_email_param(params), with :ok <- validate_email_param(params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
@ -131,7 +133,7 @@ def create(conn, _) do
render_error(conn, :forbidden, "Invalid credentials") render_error(conn, :forbidden, "Invalid credentials")
end 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 defp validate_email_param(_) do
case Pleroma.Config.get([:instance, :account_activation_required]) do case Pleroma.Config.get([:instance, :account_activation_required]) do
@ -153,7 +155,14 @@ def verify_credentials(%{assigns: %{user: user}} = conn, _) do
end end
@doc "PATCH /api/v1/accounts/update_credentials" @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 = user_params =
[ [
:no_rich_text, :no_rich_text,
@ -169,22 +178,22 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
:discoverable :discoverable
] ]
|> Enum.reduce(%{}, fn key, acc -> |> 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) end)
|> add_if_present(params, "display_name", :name) |> add_if_present(params, :display_name, :name)
|> add_if_present(params, "note", :bio) |> add_if_present(params, :note, :bio)
|> add_if_present(params, "avatar", :avatar) |> add_if_present(params, :avatar, :avatar)
|> add_if_present(params, "header", :banner) |> add_if_present(params, :header, :banner)
|> add_if_present(params, "pleroma_background_image", :background) |> add_if_present(params, :pleroma_background_image, :background)
|> add_if_present( |> add_if_present(
params, params,
"fields_attributes", :fields_attributes,
:raw_fields, :raw_fields,
&{:ok, normalize_fields_attributes(&1)} &{:ok, normalize_fields_attributes(&1)}
) )
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store) |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
|> add_if_present(params, "default_scope", :default_scope) |> add_if_present(params, :default_scope, :default_scope)
|> add_if_present(params, "actor_type", :actor_type) |> add_if_present(params, :actor_type, :actor_type)
changeset = User.update_changeset(user, user_params) changeset = User.update_changeset(user, user_params)
@ -197,7 +206,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
with true <- Map.has_key?(params, params_field), 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) Map.put(map, map_field, new_value)
else else
_ -> map _ -> map
@ -208,12 +217,15 @@ defp normalize_fields_attributes(fields) do
if Enum.all?(fields, &is_tuple/1) do if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end) Enum.map(fields, fn {_, v} -> v end)
else else
fields Enum.map(fields, fn
%{} = field -> %{"name" => field.name, "value" => field.value}
field -> field
end)
end end
end end
@doc "GET /api/v1/accounts/relationships" @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)) targets = User.get_all_by_ids(List.wrap(id))
render(conn, "relationships.json", user: user, targets: targets) render(conn, "relationships.json", user: user, targets: targets)
@ -223,7 +235,7 @@ def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, []) def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id" @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), with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
true <- User.visible_for?(user, for_user) do true <- User.visible_for?(user, for_user) do
render(conn, "show.json", user: user, for: for_user) render(conn, "show.json", user: user, for: for_user)
@ -234,12 +246,14 @@ def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
@doc "GET /api/v1/accounts/:id/statuses" @doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do 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 true <- User.visible_for?(user, reading_user) do
params = params =
params params
|> Map.put("tag", params["tagged"]) |> Map.delete(:tagged)
|> Map.delete("godmode") |> 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) activities = ActivityPub.fetch_user_activities(user, reading_user, params)
@ -259,6 +273,11 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do
@doc "GET /api/v1/accounts/:id/followers" @doc "GET /api/v1/accounts/:id/followers"
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do 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 = followers =
cond do cond do
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params) for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
@ -273,6 +292,11 @@ def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
@doc "GET /api/v1/accounts/:id/following" @doc "GET /api/v1/accounts/:id/following"
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do 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 = followers =
cond do cond do
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params) for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
@ -299,8 +323,8 @@ def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not follow yourself"} {:error, "Can not follow yourself"}
end end
def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed) render(conn, "relationship.json", user: follower, target: followed)
else else
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})
@ -319,10 +343,8 @@ def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) d
end end
@doc "POST /api/v1/accounts/:id/mute" @doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
notifications? = params |> Map.get("notifications", true) |> truthy_param?() with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
render(conn, "relationship.json", user: muter, target: muted) render(conn, "relationship.json", user: muter, target: muted)
else else
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})
@ -359,7 +381,7 @@ def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
end end
@doc "POST /api/v1/follows" @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 case User.get_cached_by_nickname(uri) do
%User{} = user -> %User{} = user ->
conn conn

View file

@ -127,7 +127,8 @@ def index(%{assigns: %{user: user}} = conn, %{"ids" => ids} = params) do
def create( def create(
%{assigns: %{user: user}} = conn, %{assigns: %{user: user}} = conn,
%{"status" => _, "scheduled_at" => scheduled_at} = params %{"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"]) 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)}, with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},

View file

@ -44,6 +44,7 @@ def home(%{assigns: %{user: user}} = conn, params) do
|> Map.put("type", ["Create", "Announce"]) |> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user) |> Map.put("muting_user", user)
|> Map.put("reply_filtering_user", user)
|> Map.put("user", user) |> Map.put("user", user)
recipients = [user.ap_id | User.following(user)] recipients = [user.ap_id | User.following(user)]
@ -109,6 +110,7 @@ def public(%{assigns: %{user: user}} = conn, params) do
|> Map.put("local_only", local_only) |> Map.put("local_only", local_only)
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user) |> Map.put("muting_user", user)
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
conn conn

View file

@ -45,7 +45,7 @@ defp get_replied_to_activities(activities) do
end) end)
end end
defp get_user(ap_id) do def get_user(ap_id, fake_record_fallback \\ true) do
cond do cond do
user = User.get_cached_by_ap_id(ap_id) -> user = User.get_cached_by_ap_id(ap_id) ->
user user
@ -53,8 +53,12 @@ defp get_user(ap_id) do
user = User.get_by_guessed_nickname(ap_id) -> user = User.get_by_guessed_nickname(ap_id) ->
user user
true -> fake_record_fallback ->
# TODO: refactor (fake records is never a good idea)
User.error_user(ap_id) User.error_user(ap_id)
true ->
nil
end end
end end
@ -97,7 +101,11 @@ def render("index.json", opts) do
UserRelationship.view_relationships_option(nil, []) UserRelationship.view_relationships_option(nil, [])
true -> 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, UserRelationship.view_relationships_option(reading_user, actors,
source_mutes_only: opts[:skip_relationships] source_mutes_only: opts[:skip_relationships]
@ -521,11 +529,9 @@ def render_content(object), do: object.data["content"] || ""
""" """
@spec build_tags(list(any())) :: list(map()) @spec build_tags(list(any())) :: list(map())
def build_tags(object_tags) when is_list(object_tags) do def build_tags(object_tags) when is_list(object_tags) do
object_tags = for tag when is_binary(tag) <- object_tags, do: tag object_tags
|> Enum.filter(&is_binary/1)
Enum.reduce(object_tags, [], fn tag, tags -> |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
end)
end end
def build_tags(_), do: [] def build_tags(_), do: []

View file

@ -14,7 +14,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password) plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
def user_exists(conn, %{"user" => username}) do 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 conn
|> json(true) |> json(true)
else else
@ -26,7 +26,7 @@ def user_exists(conn, %{"user" => username}) do
end end
def check_password(conn, %{"user" => username, "pass" => password}) do 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), Repo.get_by(User, nickname: username, local: true),
true <- Pbkdf2.checkpw(password, password_hash) do true <- Pbkdf2.checkpw(password, password_hash) do
conn conn

View file

@ -17,12 +17,8 @@ defmodule Pleroma.Web.OAuth.Scopes do
""" """
@spec fetch_scopes(map() | struct(), list()) :: list() @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 def fetch_scopes(params, default) do
parse_scopes(params["scope"] || params["scopes"], default) parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default)
end end
def parse_scopes(scopes, _default) when is_list(scopes) do def parse_scopes(scopes, _default) when is_list(scopes) do

View file

@ -12,73 +12,57 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
require Pleroma.Constants require Pleroma.Constants
def register_user(params, opts \\ []) do def register_user(params, opts \\ []) do
token = params["token"] params =
trusted_app? = params["trusted_app"] 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])
params = %{ case validate_captcha(params) do
nickname: params["nickname"], :ok ->
name: params["fullname"], if Pleroma.Config.get([:instance, :registrations_open]) do
bio: User.parse_bio(params["bio"]), create_user(params, opts)
email: params["email"], else
password: params["password"], create_user_with_invite(params, opts)
password_confirmation: params["confirm"], end
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]) {:error, error} ->
# true if captcha is disabled or enabled and valid, false otherwise # I have no idea how this error handling works
captcha_ok = {:error, %{error: Jason.encode!(%{captcha: [error]})}}
if trusted_app? || not captcha_enabled do end
end
defp validate_captcha(params) do
if params[:trusted_app] || not Pleroma.Config.get([Pleroma.Captcha, :enabled]) do
:ok :ok
else else
Pleroma.Captcha.validate( Pleroma.Captcha.validate(
params[:captcha_token], params.captcha_token,
params[:captcha_solution], params.captcha_solution,
params[:captcha_answer_data] 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
) )
end end
end end
defp registration_process(params, %{registrations_open: true}, opts) do defp create_user_with_invite(params, opts) do
create_user(params, opts) with %{token: token} when is_binary(token) <- params,
end %UserInviteToken{} = invite <- Repo.get_by(UserInviteToken, %{token: token}),
true <- UserInviteToken.valid_invite?(invite) do
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) UserInviteToken.update_usage!(invite)
create_user(params, opts) create_user(params, opts)
else
_ -> nil -> {:error, "Invalid token"}
{:error, "Expired token"} _ -> {:error, "Expired token"}
end end
end end

View file

@ -189,7 +189,9 @@ defp deps do
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
{:mox, "~> 0.5", only: :test}, {:mox, "~> 0.5", only: :test},
{:restarter, path: "./restarter"}, {: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() ] ++ oauth_deps()
end end

View file

@ -74,7 +74,7 @@
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, "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"}, "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"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"}, "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"}, "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"},

View file

@ -2,11 +2,21 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.StateTest do defmodule Pleroma.StatsTest do
use Pleroma.DataCase use Pleroma.DataCase
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Web.CommonAPI 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 describe "status visibility count" do
test "on new status" do test "on new status" do
user = insert(:user) user = insert(:user)

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

View file

@ -51,6 +51,60 @@ defp oauth_access(scopes, opts \\ []) do
%{user: user, token: token, conn: conn} %{user: user, token: token, conn: conn}
end 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 defp ensure_federating_or_authenticated(conn, url, user) do
initial_setting = Config.get([:instance, :federating]) initial_setting = Config.get([:instance, :federating])
on_exit(fn -> Config.put([:instance, :federating], initial_setting) end) on_exit(fn -> Config.put([:instance, :federating], initial_setting) end)

View file

@ -994,72 +994,6 @@ test "reverts emoji unreact on error" do
end end
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 describe "unliking" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true) Config.put([:instance, :federating], true)
@ -1071,7 +1005,8 @@ test "adds a like activity to the db" do
{:ok, object} = ActivityPub.unlike(user, object) {:ok, object} = ActivityPub.unlike(user, object)
refute called(Federator.publish()) 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 assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object) {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
@ -1082,10 +1017,10 @@ test "adds a like activity to the db" do
test "reverts unliking on error" do test "reverts unliking on error" do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user) 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 assert object.data["like_count"] == 1
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
@ -1106,7 +1041,9 @@ test "unliking a previously liked object" do
{:ok, object} = ActivityPub.unlike(user, object) {:ok, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0 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 assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object) {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
@ -1973,4 +1910,497 @@ test "old user must be in the new user's `also_known_as` list" do
ActivityPub.move(old_user, new_user) ActivityPub.move(old_user, new_user)
end end
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 end

View file

@ -224,8 +224,7 @@ test "fetches only Create activities" do
object = Object.normalize(activity) object = Object.normalize(activity)
{:ok, [vote], object} = CommonAPI.vote(other_user, object, [0]) {:ok, [vote], object} = CommonAPI.vote(other_user, object, [0])
vote_object = Object.normalize(vote) {:ok, _activity} = CommonAPI.favorite(user, activity.id)
{:ok, _activity, _object} = ActivityPub.like(user, vote_object)
[fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object) [fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object)
assert fetched_vote.id == vote.id assert fetched_vote.id == vote.id
end end
@ -346,7 +345,7 @@ test "fetches existing like" do
user = insert(:user) user = insert(:user)
refute Utils.get_existing_like(user.ap_id, object) 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) assert ^like_activity = Utils.get_existing_like(user.ap_id, object)
end end

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

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

View file

@ -21,6 +21,60 @@ defmodule Pleroma.Web.CommonAPITest do
setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :limit])
setup do: clear_config([:instance, :max_pinned_statuses]) 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 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) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
@ -256,6 +310,16 @@ test "repeating a status" do
{:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user) {:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user)
end 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 test "repeating a status privately" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -285,8 +349,8 @@ test "retweeting a status twice returns the status" do
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{} = activity, object} = CommonAPI.repeat(activity.id, user) {:ok, %Activity{} = announce, object} = CommonAPI.repeat(activity.id, user)
{:ok, ^activity, ^object} = CommonAPI.repeat(activity.id, user) {:ok, ^announce, ^object} = CommonAPI.repeat(activity.id, user)
end end
test "favoriting a status twice returns ok, but without the like activity" do test "favoriting a status twice returns ok, but without the like activity" do
@ -360,7 +424,9 @@ test "unpin status", %{user: user, activity: activity} do
user = refresh_record(user) 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) user = refresh_record(user)

View file

@ -335,26 +335,6 @@ test "for direct posts, a reply" do
end end
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 describe "to_master_date/1" do
test "removes microseconds from date (NaiveDateTime)" 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" assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z"

View file

@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
describe "updating credentials" do describe "updating credentials" do
setup do: oauth_access(["write:accounts"]) setup do: oauth_access(["write:accounts"])
setup :request_content_type
test "sets user settings in a generic way", %{conn: conn} do test "sets user settings in a generic way", %{conn: conn} do
res_conn = res_conn =
@ -25,7 +26,7 @@ test "sets user settings in a generic way", %{conn: conn} 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"}} assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}}
user = Repo.get(User, user_data["id"]) user = Repo.get(User, user_data["id"])
@ -41,7 +42,7 @@ test "sets user settings in a generic way", %{conn: conn} 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"] == assert user_data["pleroma"]["settings_store"] ==
%{ %{
@ -62,7 +63,7 @@ test "sets user settings in a generic way", %{conn: conn} 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"] == assert user_data["pleroma"]["settings_store"] ==
%{ %{
@ -79,7 +80,7 @@ test "updates the user's bio", %{conn: conn} do
"note" => "I drink #cofe with @#{user2.nickname}\n\nsuya.." "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"] == 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="#{ ~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 @@ test "updates the user's bio", %{conn: conn} do
test "updates the user's locking status", %{conn: conn} do test "updates the user's locking status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"}) 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 assert user_data["locked"] == true
end end
@ -100,21 +101,21 @@ test "updates the user's allow_following_move", %{user: user, conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"}) conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"})
assert refresh_record(user).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 assert user_data["pleroma"]["allow_following_move"] == false
end end
test "updates the user's default scope", %{conn: conn} do 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 = json_response_and_validate_schema(conn, 200)
assert user_data["source"]["privacy"] == "cofe" assert user_data["source"]["privacy"] == "unlisted"
end end
test "updates the user's hide_followers status", %{conn: conn} do test "updates the user's hide_followers status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"}) 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 assert user_data["pleroma"]["hide_followers"] == true
end end
@ -122,12 +123,12 @@ test "updates the user's discoverable status", %{conn: conn} do
assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} = assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} =
conn conn
|> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"}) |> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"})
|> json_response(:ok) |> json_response_and_validate_schema(:ok)
assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} = assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} =
conn conn
|> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"}) |> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"})
|> json_response(:ok) |> json_response_and_validate_schema(:ok)
end end
test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do
@ -137,7 +138,7 @@ test "updates the user's hide_followers_count and hide_follows_count", %{conn: c
hide_follows_count: "true" 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_followers_count"] == true
assert user_data["pleroma"]["hide_follows_count"] == true assert user_data["pleroma"]["hide_follows_count"] == true
end end
@ -146,7 +147,7 @@ test "updates the user's skip_thread_containment option", %{user: user, conn: co
response = response =
conn conn
|> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"}) |> 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 response["pleroma"]["skip_thread_containment"] == true
assert refresh_record(user).skip_thread_containment assert refresh_record(user).skip_thread_containment
@ -155,28 +156,28 @@ test "updates the user's skip_thread_containment option", %{user: user, conn: co
test "updates the user's hide_follows status", %{conn: conn} do test "updates the user's hide_follows status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"}) 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 assert user_data["pleroma"]["hide_follows"] == true
end end
test "updates the user's hide_favorites status", %{conn: conn} do test "updates the user's hide_favorites status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"}) 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 assert user_data["pleroma"]["hide_favorites"] == true
end end
test "updates the user's show_role status", %{conn: conn} do test "updates the user's show_role status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"}) 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 assert user_data["source"]["pleroma"]["show_role"] == false
end end
test "updates the user's no_rich_text status", %{conn: conn} do test "updates the user's no_rich_text status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"}) 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 assert user_data["source"]["pleroma"]["no_rich_text"] == true
end end
@ -184,7 +185,7 @@ test "updates the user's name", %{conn: conn} do
conn = conn =
patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"}) 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" assert user_data["display_name"] == "markorepairs"
end end
@ -197,7 +198,7 @@ test "updates the user's avatar", %{user: user, conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) 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) assert user_response["avatar"] != User.avatar_url(user)
end end
@ -210,7 +211,7 @@ test "updates the user's banner", %{user: user, conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header}) 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) assert user_response["header"] != User.banner_url(user)
end end
@ -226,7 +227,7 @@ test "updates the user's background", %{conn: conn} do
"pleroma_background_image" => new_header "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"] assert user_response["pleroma"]["background_image"]
end end
@ -237,14 +238,15 @@ test "requires 'write:accounts' permission" do
for token <- [token1, token2] do for token <- [token1, token2] do
conn = conn =
build_conn() build_conn()
|> put_req_header("content-type", "multipart/form-data")
|> put_req_header("authorization", "Bearer #{token.token}") |> put_req_header("authorization", "Bearer #{token.token}")
|> patch("/api/v1/accounts/update_credentials", %{}) |> patch("/api/v1/accounts/update_credentials", %{})
if token == token1 do if token == token1 do
assert %{"error" => "Insufficient permissions: write:accounts."} == assert %{"error" => "Insufficient permissions: write:accounts."} ==
json_response(conn, 403) json_response_and_validate_schema(conn, 403)
else else
assert json_response(conn, 200) assert json_response_and_validate_schema(conn, 200)
end end
end end
end end
@ -259,11 +261,11 @@ test "updates profile emojos", %{user: user, conn: conn} do
"display_name" => name "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}") 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["note"] == note
assert user_data["display_name"] == name assert user_data["display_name"] == name
@ -279,7 +281,7 @@ test "update fields", %{conn: conn} do
account_data = account_data =
conn conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(200) |> json_response_and_validate_schema(200)
assert account_data["fields"] == [ assert account_data["fields"] == [
%{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"}, %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"},
@ -312,7 +314,7 @@ test "update fields via x-www-form-urlencoded", %{conn: conn} do
conn conn
|> put_req_header("content-type", "application/x-www-form-urlencoded") |> put_req_header("content-type", "application/x-www-form-urlencoded")
|> patch("/api/v1/accounts/update_credentials", fields) |> patch("/api/v1/accounts/update_credentials", fields)
|> json_response(200) |> json_response_and_validate_schema(200)
assert account["fields"] == [ assert account["fields"] == [
%{"name" => "foo", "value" => "bar"}, %{"name" => "foo", "value" => "bar"},
@ -337,7 +339,7 @@ test "update fields with empty name", %{conn: conn} do
account = account =
conn conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(200) |> json_response_and_validate_schema(200)
assert account["fields"] == [ assert account["fields"] == [
%{"name" => "foo", "value" => ""} %{"name" => "foo", "value" => ""}
@ -356,14 +358,14 @@ test "update fields when invalid request", %{conn: conn} do
assert %{"error" => "Invalid request"} == assert %{"error" => "Invalid request"} ==
conn conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403) |> json_response_and_validate_schema(403)
fields = [%{"name" => long_name, "value" => "bar"}] fields = [%{"name" => long_name, "value" => "bar"}]
assert %{"error" => "Invalid request"} == assert %{"error" => "Invalid request"} ==
conn conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) |> 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) Pleroma.Config.put([:instance, :max_account_fields], 1)
@ -375,7 +377,7 @@ test "update fields when invalid request", %{conn: conn} do
assert %{"error" => "Invalid request"} == assert %{"error" => "Invalid request"} ==
conn conn
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403) |> json_response_and_validate_schema(403)
end end
end end
end end

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,7 @@ test "apps/verify_credentials", %{conn: conn} do
"vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) "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
test "creates an oauth app", %{conn: conn} do test "creates an oauth app", %{conn: conn} do
@ -55,6 +55,6 @@ test "creates an oauth app", %{conn: conn} do
"vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) "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
end end

View file

@ -4,16 +4,12 @@
defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do
use Pleroma.Web.ConnCase, async: true 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 test "with tags", %{conn: conn} do
assert resp = assert resp =
conn conn
|> get("/api/v1/custom_emojis") |> get("/api/v1/custom_emojis")
|> json_response(200) |> json_response_and_validate_schema(200)
assert [emoji | _body] = resp assert [emoji | _body] = resp
assert Map.has_key?(emoji, "shortcode") assert Map.has_key?(emoji, "shortcode")
@ -23,19 +19,5 @@ test "with tags", %{conn: conn} do
assert Map.has_key?(emoji, "category") assert Map.has_key?(emoji, "category")
assert Map.has_key?(emoji, "url") assert Map.has_key?(emoji, "url")
assert Map.has_key?(emoji, "visible_in_picker") 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
end end

View file

@ -6,11 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ApiSpec
alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse
import Pleroma.Factory import Pleroma.Factory
import OpenApiSpex.TestAssertions
test "blocking / unblocking a domain" do test "blocking / unblocking a domain" do
%{user: user, conn: conn} = oauth_access(["write:blocks"]) %{user: user, conn: conn} = oauth_access(["write:blocks"])
@ -21,7 +18,7 @@ test "blocking / unblocking a domain" do
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) |> 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) user = User.get_cached_by_ap_id(user.ap_id)
assert User.blocks?(user, other_user) assert User.blocks?(user, other_user)
@ -30,7 +27,7 @@ test "blocking / unblocking a domain" do
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) |> 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) user = User.get_cached_by_ap_id(user.ap_id)
refute User.blocks?(user, other_user) refute User.blocks?(user, other_user)
end end
@ -41,21 +38,10 @@ test "getting a list of domain blocks" do
{:ok, user} = User.block_domain(user, "bad.site") {:ok, user} = User.block_domain(user, "bad.site")
{:ok, user} = User.block_domain(user, "even.worse.site") {:ok, user} = User.block_domain(user, "even.worse.site")
conn = assert ["even.worse.site", "bad.site"] ==
conn conn
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/domain_blocks") |> get("/api/v1/domain_blocks")
|> json_response_and_validate_schema(200)
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)
end end
end end

View file

@ -302,6 +302,17 @@ test "creates a scheduled activity", %{conn: conn} do
assert [] == Repo.all(Activity) assert [] == Repo.all(Activity)
end 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 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) scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.MongooseIMController do
test "/user_exists", %{conn: conn} do test "/user_exists", %{conn: conn} do
_user = insert(:user, nickname: "lain") _user = insert(:user, nickname: "lain")
_remote_user = insert(:user, nickname: "alice", local: false) _remote_user = insert(:user, nickname: "alice", local: false)
_deactivated_user = insert(:user, nickname: "konata", deactivated: true)
res = res =
conn conn
@ -30,11 +31,25 @@ test "/user_exists", %{conn: conn} do
|> json_response(404) |> json_response(404)
assert res == false assert res == false
res =
conn
|> get(mongoose_im_path(conn, :user_exists), user: "konata")
|> json_response(404)
assert res == false
end end
test "/check_password", %{conn: conn} do test "/check_password", %{conn: conn} do
user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt("cool")) user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt("cool"))
_deactivated_user =
insert(:user,
nickname: "konata",
deactivated: true,
password_hash: Comeonin.Pbkdf2.hashpwsalt("cool")
)
res = res =
conn conn
|> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "cool") |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "cool")
@ -49,6 +64,13 @@ test "/check_password", %{conn: conn} do
assert res == false assert res == false
res =
conn
|> get(mongoose_im_path(conn, :check_password), user: "konata", pass: "cool")
|> json_response(404)
assert res == false
res = res =
conn conn
|> get(mongoose_im_path(conn, :check_password), user: "nobody", pass: "cool") |> get(mongoose_im_path(conn, :check_password), user: "nobody", pass: "cool")

View file

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