Merge remote-tracking branch 'origin/develop' into reactions
This commit is contained in:
commit
557223b2b5
|
@ -151,6 +151,7 @@ amd64:
|
||||||
only: &release-only
|
only: &release-only
|
||||||
- master@pleroma/pleroma
|
- master@pleroma/pleroma
|
||||||
- develop@pleroma/pleroma
|
- develop@pleroma/pleroma
|
||||||
|
- /^maint/.*$/@pleroma/pleroma
|
||||||
artifacts: &release-artifacts
|
artifacts: &release-artifacts
|
||||||
name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME"
|
name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME"
|
||||||
paths:
|
paths:
|
||||||
|
|
|
@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition)
|
- Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition)
|
||||||
- Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
|
- Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
|
||||||
- Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item
|
- Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item
|
||||||
|
- Mastodon API: Add `upload_limit`, `avatar_upload_limit`, `background_upload_limit`, and `banner_upload_limit` to `/api/v1/instance`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
||||||
|
@ -117,6 +118,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
|
- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
|
||||||
- ActivityPub: Add ActivityPub actor's `discoverable` parameter.
|
- ActivityPub: Add ActivityPub actor's `discoverable` parameter.
|
||||||
- Admin API: Added moderation log filters (user/start date/end date/search/pagination)
|
- Admin API: Added moderation log filters (user/start date/end date/search/pagination)
|
||||||
|
- Reverse Proxy: Do not retry failed requests to limit pressure on the peer
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
|
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
|
||||||
|
|
|
@ -102,7 +102,8 @@ defp cachex_children do
|
||||||
build_cachex("scrubber", limit: 2500),
|
build_cachex("scrubber", limit: 2500),
|
||||||
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
|
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
|
||||||
build_cachex("web_resp", limit: 2500),
|
build_cachex("web_resp", limit: 2500),
|
||||||
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10)
|
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
|
||||||
|
build_cachex("failed_proxy_url", limit: 2500)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -84,22 +84,11 @@ def get_lists_from_activity(%Activity{actor: ap_id}) do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get lists to which the account belongs.
|
# Get lists to which the account belongs.
|
||||||
def get_lists_account_belongs(%User{} = owner, account_id) do
|
def get_lists_account_belongs(%User{} = owner, user) do
|
||||||
user = User.get_cached_by_id(account_id)
|
Pleroma.List
|
||||||
|
|> where([l], l.user_id == ^owner.id)
|
||||||
query =
|
|> where([l], fragment("? = ANY(?)", ^user.follower_address, l.following))
|
||||||
from(
|
|> Repo.all()
|
||||||
l in Pleroma.List,
|
|
||||||
where:
|
|
||||||
l.user_id == ^owner.id and
|
|
||||||
fragment(
|
|
||||||
"? = ANY(?)",
|
|
||||||
^user.follower_address,
|
|
||||||
l.following
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
Repo.all(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def rename(%Pleroma.List{} = list, title) do
|
def rename(%Pleroma.List{} = list, title) do
|
||||||
|
|
|
@ -15,6 +15,7 @@ defmodule Pleroma.ReverseProxy do
|
||||||
@valid_resp_codes [200, 206, 304]
|
@valid_resp_codes [200, 206, 304]
|
||||||
@max_read_duration :timer.seconds(30)
|
@max_read_duration :timer.seconds(30)
|
||||||
@max_body_length :infinity
|
@max_body_length :infinity
|
||||||
|
@failed_request_ttl :timer.seconds(60)
|
||||||
@methods ~w(GET HEAD)
|
@methods ~w(GET HEAD)
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
|
@ -48,6 +49,8 @@ defmodule Pleroma.ReverseProxy do
|
||||||
* `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to
|
* `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to
|
||||||
read from the remote upstream.
|
read from the remote upstream.
|
||||||
|
|
||||||
|
* `failed_request_ttl` (default `#{inspect(@failed_request_ttl)}` ms): the time the failed request is cached and cannot be retried.
|
||||||
|
|
||||||
* `inline_content_types`:
|
* `inline_content_types`:
|
||||||
* `true` will not alter `content-disposition` (up to the upstream),
|
* `true` will not alter `content-disposition` (up to the upstream),
|
||||||
* `false` will add `content-disposition: attachment` to any request,
|
* `false` will add `content-disposition: attachment` to any request,
|
||||||
|
@ -83,6 +86,7 @@ defmodule Pleroma.ReverseProxy do
|
||||||
{:keep_user_agent, boolean}
|
{:keep_user_agent, boolean}
|
||||||
| {:max_read_duration, :timer.time() | :infinity}
|
| {:max_read_duration, :timer.time() | :infinity}
|
||||||
| {:max_body_length, non_neg_integer() | :infinity}
|
| {:max_body_length, non_neg_integer() | :infinity}
|
||||||
|
| {:failed_request_ttl, :timer.time() | :infinity}
|
||||||
| {:http, []}
|
| {:http, []}
|
||||||
| {:req_headers, [{String.t(), String.t()}]}
|
| {:req_headers, [{String.t(), String.t()}]}
|
||||||
| {:resp_headers, [{String.t(), String.t()}]}
|
| {:resp_headers, [{String.t(), String.t()}]}
|
||||||
|
@ -108,7 +112,8 @@ def call(conn = %{method: method}, url, opts) when method in @methods do
|
||||||
opts
|
opts
|
||||||
end
|
end
|
||||||
|
|
||||||
with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
|
with {:ok, nil} <- Cachex.get(:failed_proxy_url_cache, url),
|
||||||
|
{:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
|
||||||
:ok <-
|
:ok <-
|
||||||
header_length_constraint(
|
header_length_constraint(
|
||||||
headers,
|
headers,
|
||||||
|
@ -116,12 +121,18 @@ def call(conn = %{method: method}, url, opts) when method in @methods do
|
||||||
) do
|
) do
|
||||||
response(conn, client, url, code, headers, opts)
|
response(conn, client, url, code, headers, opts)
|
||||||
else
|
else
|
||||||
|
{:ok, true} ->
|
||||||
|
conn
|
||||||
|
|> error_or_redirect(url, 500, "Request failed", opts)
|
||||||
|
|> halt()
|
||||||
|
|
||||||
{:ok, code, headers} ->
|
{:ok, code, headers} ->
|
||||||
head_response(conn, url, code, headers, opts)
|
head_response(conn, url, code, headers, opts)
|
||||||
|> halt()
|
|> halt()
|
||||||
|
|
||||||
{:error, {:invalid_http_response, code}} ->
|
{:error, {:invalid_http_response, code}} ->
|
||||||
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}")
|
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}")
|
||||||
|
track_failed_url(url, code, opts)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> error_or_redirect(
|
|> error_or_redirect(
|
||||||
|
@ -134,6 +145,7 @@ def call(conn = %{method: method}, url, opts) when method in @methods do
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}")
|
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}")
|
||||||
|
track_failed_url(url, error, opts)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> error_or_redirect(url, 500, "Request failed", opts)
|
|> error_or_redirect(url, 500, "Request failed", opts)
|
||||||
|
@ -388,4 +400,17 @@ defp increase_read_duration(_) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp client, do: Pleroma.ReverseProxy.Client
|
defp client, do: Pleroma.ReverseProxy.Client
|
||||||
|
|
||||||
|
defp track_failed_url(url, code, opts) do
|
||||||
|
code = to_string(code)
|
||||||
|
|
||||||
|
ttl =
|
||||||
|
if code in ["403", "404"] or String.starts_with?(code, "5") do
|
||||||
|
Keyword.get(opts, :failed_request_ttl, @failed_request_ttl)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
Cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -365,7 +365,7 @@ def announce(
|
||||||
local \\ true,
|
local \\ true,
|
||||||
public \\ true
|
public \\ true
|
||||||
) do
|
) do
|
||||||
with true <- is_public?(object),
|
with true <- is_announceable?(object, user, public),
|
||||||
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),
|
||||||
|
|
|
@ -774,6 +774,24 @@ def handle_incoming(
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# For Undos that don't have the complete object attached, try to find it in our database.
|
||||||
|
def handle_incoming(
|
||||||
|
%{
|
||||||
|
"type" => "Undo",
|
||||||
|
"object" => object
|
||||||
|
} = activity,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
when is_binary(object) do
|
||||||
|
with %Activity{data: data} <- Activity.get_by_ap_id(object) do
|
||||||
|
activity
|
||||||
|
|> Map.put("object", data)
|
||||||
|
|> handle_incoming(options)
|
||||||
|
else
|
||||||
|
_e -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def handle_incoming(_, _), do: :error
|
def handle_incoming(_, _), do: :error
|
||||||
|
|
||||||
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
|
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
|
||||||
|
@ -833,6 +851,27 @@ def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
|
||||||
|
object =
|
||||||
|
object_id
|
||||||
|
|> Object.normalize()
|
||||||
|
|
||||||
|
data =
|
||||||
|
if Visibility.is_private?(object) && object.data["actor"] == ap_id do
|
||||||
|
data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
|
||||||
|
else
|
||||||
|
data |> maybe_fix_object_url
|
||||||
|
end
|
||||||
|
|
||||||
|
data =
|
||||||
|
data
|
||||||
|
|> strip_internal_fields
|
||||||
|
|> Map.merge(Utils.make_json_ld_header())
|
||||||
|
|> Map.delete("bcc")
|
||||||
|
|
||||||
|
{:ok, data}
|
||||||
|
end
|
||||||
|
|
||||||
# Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
|
# Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
|
||||||
# because of course it does.
|
# because of course it does.
|
||||||
def prepare_outgoing(%{"type" => "Accept"} = data) do
|
def prepare_outgoing(%{"type" => "Accept"} = data) do
|
||||||
|
|
|
@ -525,7 +525,7 @@ def make_unlike_data(
|
||||||
@spec add_announce_to_object(Activity.t(), Object.t()) ::
|
@spec add_announce_to_object(Activity.t(), Object.t()) ::
|
||||||
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
||||||
def add_announce_to_object(
|
def add_announce_to_object(
|
||||||
%Activity{data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]}},
|
%Activity{data: %{"actor" => actor}},
|
||||||
object
|
object
|
||||||
) do
|
) do
|
||||||
announcements = take_announcements(object)
|
announcements = take_announcements(object)
|
||||||
|
|
|
@ -22,7 +22,7 @@ def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user})
|
||||||
def render("endpoints.json", %{user: %User{local: true} = _user}) do
|
def render("endpoints.json", %{user: %User{local: true} = _user}) do
|
||||||
%{
|
%{
|
||||||
"oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
|
"oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
|
||||||
"oauthRegistrationEndpoint" => Helpers.mastodon_api_url(Endpoint, :create_app),
|
"oauthRegistrationEndpoint" => Helpers.app_url(Endpoint, :create),
|
||||||
"oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
|
"oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
|
||||||
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),
|
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),
|
||||||
"uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media)
|
"uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media)
|
||||||
|
|
|
@ -27,6 +27,11 @@ def is_private?(activity) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def is_announceable?(activity, user, public \\ true) do
|
||||||
|
is_public?(activity) ||
|
||||||
|
(!public && is_private?(activity) && activity.data["actor"] == user.ap_id)
|
||||||
|
end
|
||||||
|
|
||||||
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
|
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
|
||||||
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
|
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ def render("show.json", %{report: report, user: user, account: account, statuses
|
||||||
end
|
end
|
||||||
|
|
||||||
defp merge_account_views(%User{} = user) do
|
defp merge_account_views(%User{} = user) do
|
||||||
Pleroma.Web.MastodonAPI.AccountView.render("account.json", %{user: user})
|
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|
||||||
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
|
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}}
|
||||||
|
|
||||||
if String.length(text) > 0 do
|
if String.length(text) > 0 do
|
||||||
author = User.get_cached_by_nickname(user_name)
|
author = User.get_cached_by_nickname(user_name)
|
||||||
author = Pleroma.Web.MastodonAPI.AccountView.render("account.json", user: author)
|
author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author)
|
||||||
message = ChatChannelState.add_message(%{text: text, author: author})
|
message = ChatChannelState.add_message(%{text: text, author: author})
|
||||||
|
|
||||||
broadcast!(socket, "new_msg", message)
|
broadcast!(socket, "new_msg", message)
|
||||||
|
|
|
@ -76,11 +76,12 @@ def delete(activity_id, user) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def repeat(id_or_ap_id, user) do
|
def repeat(id_or_ap_id, user, params \\ %{}) do
|
||||||
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||||
object <- Object.normalize(activity),
|
object <- Object.normalize(activity),
|
||||||
nil <- Utils.get_existing_announce(user.ap_id, object) do
|
nil <- Utils.get_existing_announce(user.ap_id, object),
|
||||||
ActivityPub.announce(user, object)
|
public <- public_announce?(object, params) do
|
||||||
|
ActivityPub.announce(user, object, nil, true, public)
|
||||||
else
|
else
|
||||||
_ -> {:error, dgettext("errors", "Could not repeat")}
|
_ -> {:error, dgettext("errors", "Could not repeat")}
|
||||||
end
|
end
|
||||||
|
@ -179,6 +180,14 @@ defp normalize_and_validate_choices(choices, object) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def public_announce?(_, %{"visibility" => visibility})
|
||||||
|
when visibility in ~w{public unlisted private direct},
|
||||||
|
do: visibility in ~w(public unlisted)
|
||||||
|
|
||||||
|
def public_announce?(object, _) do
|
||||||
|
Visibility.is_public?(object)
|
||||||
|
end
|
||||||
|
|
||||||
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
|
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
|
||||||
|
|
||||||
def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
|
def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
|
||||||
|
|
|
@ -68,4 +68,23 @@ def add_link_headers(conn, activities, extra_params \\ %{}) do
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do
|
||||||
|
case Pleroma.User.get_cached_by_id(id) do
|
||||||
|
%Pleroma.User{} = account -> assign(conn, :account, account)
|
||||||
|
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def try_render(conn, target, params)
|
||||||
|
when is_binary(target) do
|
||||||
|
case render(conn, target, params) do
|
||||||
|
nil -> render_error(conn, :not_implemented, "Can't display this activity")
|
||||||
|
res -> res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def try_render(conn, _, _) do
|
||||||
|
render_error(conn, :not_implemented, "Can't display this activity")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,304 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.AccountController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
import Pleroma.Web.ControllerHelper,
|
||||||
|
only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
|
||||||
|
|
||||||
|
alias Pleroma.Emoji
|
||||||
|
alias Pleroma.Plugs.RateLimiter
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
alias Pleroma.Web.MastodonAPI.ListView
|
||||||
|
alias Pleroma.Web.MastodonAPI.MastodonAPI
|
||||||
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
|
alias Pleroma.Web.OAuth.Token
|
||||||
|
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
||||||
|
|
||||||
|
@relations [:follow, :unfollow]
|
||||||
|
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
|
||||||
|
|
||||||
|
plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
|
||||||
|
plug(RateLimiter, :relations_actions when action in @relations)
|
||||||
|
plug(RateLimiter, :app_account_creation when action == :create)
|
||||||
|
plug(:assign_account_by_id when action in @needs_account)
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
@doc "POST /api/v1/accounts"
|
||||||
|
def create(
|
||||||
|
%{assigns: %{app: app}} = conn,
|
||||||
|
%{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
|
||||||
|
) do
|
||||||
|
params =
|
||||||
|
params
|
||||||
|
|> Map.take([
|
||||||
|
"email",
|
||||||
|
"captcha_solution",
|
||||||
|
"captcha_token",
|
||||||
|
"captcha_answer_data",
|
||||||
|
"token",
|
||||||
|
"password"
|
||||||
|
])
|
||||||
|
|> Map.put("nickname", nickname)
|
||||||
|
|> Map.put("fullname", params["fullname"] || nickname)
|
||||||
|
|> Map.put("bio", params["bio"] || "")
|
||||||
|
|> Map.put("confirm", params["password"])
|
||||||
|
|
||||||
|
with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
|
||||||
|
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
|
||||||
|
json(conn, %{
|
||||||
|
token_type: "Bearer",
|
||||||
|
access_token: token.token,
|
||||||
|
scope: app.scopes,
|
||||||
|
created_at: Token.Utils.format_created_at(token)
|
||||||
|
})
|
||||||
|
else
|
||||||
|
{:error, errors} -> json_response(conn, :bad_request, errors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(%{assigns: %{app: _app}} = conn, _) do
|
||||||
|
render_error(conn, :bad_request, "Missing parameters")
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(conn, _) do
|
||||||
|
render_error(conn, :forbidden, "Invalid credentials")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/accounts/verify_credentials"
|
||||||
|
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
|
||||||
|
chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
|
||||||
|
|
||||||
|
render(conn, "show.json",
|
||||||
|
user: user,
|
||||||
|
for: user,
|
||||||
|
with_pleroma_settings: true,
|
||||||
|
with_chat_token: chat_token
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "PATCH /api/v1/accounts/update_credentials"
|
||||||
|
def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
|
||||||
|
user = original_user
|
||||||
|
|
||||||
|
user_params =
|
||||||
|
%{}
|
||||||
|
|> add_if_present(params, "display_name", :name)
|
||||||
|
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
|
||||||
|
|> add_if_present(params, "avatar", :avatar, fn value ->
|
||||||
|
with %Plug.Upload{} <- value,
|
||||||
|
{:ok, object} <- ActivityPub.upload(value, type: :avatar) do
|
||||||
|
{:ok, object.data}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
|
||||||
|
|
||||||
|
user_info_emojis =
|
||||||
|
user.info
|
||||||
|
|> Map.get(:emoji, [])
|
||||||
|
|> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
|
||||||
|
|> Enum.dedup()
|
||||||
|
|
||||||
|
info_params =
|
||||||
|
[
|
||||||
|
:no_rich_text,
|
||||||
|
:locked,
|
||||||
|
:hide_followers_count,
|
||||||
|
:hide_follows_count,
|
||||||
|
:hide_followers,
|
||||||
|
:hide_follows,
|
||||||
|
:hide_favorites,
|
||||||
|
:show_role,
|
||||||
|
:skip_thread_containment,
|
||||||
|
:discoverable
|
||||||
|
]
|
||||||
|
|> Enum.reduce(%{}, fn key, acc ->
|
||||||
|
add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
|
||||||
|
end)
|
||||||
|
|> add_if_present(params, "default_scope", :default_scope)
|
||||||
|
|> add_if_present(params, "fields", :fields, fn fields ->
|
||||||
|
fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
|
||||||
|
|
||||||
|
{:ok, fields}
|
||||||
|
end)
|
||||||
|
|> add_if_present(params, "fields", :raw_fields)
|
||||||
|
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
|
||||||
|
{:ok, Map.merge(user.info.pleroma_settings_store, value)}
|
||||||
|
end)
|
||||||
|
|> add_if_present(params, "header", :banner, fn value ->
|
||||||
|
with %Plug.Upload{} <- value,
|
||||||
|
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
|
||||||
|
{:ok, object.data}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> add_if_present(params, "pleroma_background_image", :background, fn value ->
|
||||||
|
with %Plug.Upload{} <- value,
|
||||||
|
{:ok, object} <- ActivityPub.upload(value, type: :background) do
|
||||||
|
{:ok, object.data}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Map.put(:emoji, user_info_emojis)
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
user
|
||||||
|
|> User.update_changeset(user_params)
|
||||||
|
|> User.change_info(&User.Info.profile_update(&1, info_params))
|
||||||
|
|
||||||
|
with {:ok, user} <- User.update_and_set_cache(changeset) do
|
||||||
|
if original_user != user, do: CommonAPI.update(user)
|
||||||
|
|
||||||
|
render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
|
||||||
|
else
|
||||||
|
_e -> render_error(conn, :forbidden, "Invalid request")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
|
||||||
|
with true <- Map.has_key?(params, params_field),
|
||||||
|
{:ok, new_value} <- value_function.(params[params_field]) do
|
||||||
|
Map.put(map, map_field, new_value)
|
||||||
|
else
|
||||||
|
_ -> map
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/accounts/relationships"
|
||||||
|
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
|
targets = User.get_all_by_ids(List.wrap(id))
|
||||||
|
|
||||||
|
render(conn, "relationships.json", user: user, targets: targets)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
|
||||||
|
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
|
||||||
|
|
||||||
|
@doc "GET /api/v1/accounts/:id"
|
||||||
|
def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
|
||||||
|
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
|
||||||
|
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
|
||||||
|
render(conn, "show.json", user: user, for: for_user)
|
||||||
|
else
|
||||||
|
_e -> render_error(conn, :not_found, "Can't find user")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/accounts/:id/statuses"
|
||||||
|
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
|
||||||
|
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
|
||||||
|
params = Map.put(params, "tag", params["tagged"])
|
||||||
|
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> add_link_headers(activities)
|
||||||
|
|> put_view(StatusView)
|
||||||
|
|> render("index.json", activities: activities, for: reading_user, as: :activity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/accounts/:id/followers"
|
||||||
|
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
|
||||||
|
followers =
|
||||||
|
cond do
|
||||||
|
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
|
||||||
|
user.info.hide_followers -> []
|
||||||
|
true -> MastodonAPI.get_followers(user, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> add_link_headers(followers)
|
||||||
|
|> render("index.json", for: for_user, users: followers, as: :user)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/accounts/:id/following"
|
||||||
|
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
|
||||||
|
followers =
|
||||||
|
cond do
|
||||||
|
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
|
||||||
|
user.info.hide_follows -> []
|
||||||
|
true -> MastodonAPI.get_friends(user, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> add_link_headers(followers)
|
||||||
|
|> render("index.json", for: for_user, users: followers, as: :user)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/accounts/:id/lists"
|
||||||
|
def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
|
||||||
|
lists = Pleroma.List.get_lists_account_belongs(user, account)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_view(ListView)
|
||||||
|
|> render("index.json", lists: lists)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/accounts/:id/follow"
|
||||||
|
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
|
||||||
|
{:error, :not_found}
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
|
||||||
|
with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
|
||||||
|
render(conn, "relationship.json", user: follower, target: followed)
|
||||||
|
else
|
||||||
|
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/accounts/:id/unfollow"
|
||||||
|
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
|
||||||
|
{:error, :not_found}
|
||||||
|
end
|
||||||
|
|
||||||
|
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
|
||||||
|
with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
|
||||||
|
render(conn, "relationship.json", user: follower, target: followed)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/accounts/:id/mute"
|
||||||
|
def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
|
||||||
|
notifications? = params |> Map.get("notifications", true) |> truthy_param?()
|
||||||
|
|
||||||
|
with {:ok, muter} <- User.mute(muter, muted, notifications?) do
|
||||||
|
render(conn, "relationship.json", user: muter, target: muted)
|
||||||
|
else
|
||||||
|
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/accounts/:id/unmute"
|
||||||
|
def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
|
||||||
|
with {:ok, muter} <- User.unmute(muter, muted) do
|
||||||
|
render(conn, "relationship.json", user: muter, target: muted)
|
||||||
|
else
|
||||||
|
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/accounts/:id/block"
|
||||||
|
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
|
||||||
|
with {:ok, blocker} <- User.block(blocker, blocked),
|
||||||
|
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
|
||||||
|
render(conn, "relationship.json", user: blocker, target: blocked)
|
||||||
|
else
|
||||||
|
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/accounts/:id/unblock"
|
||||||
|
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
|
||||||
|
with {:ok, blocker} <- User.unblock(blocker, blocked),
|
||||||
|
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
|
||||||
|
render(conn, "relationship.json", user: blocker, target: blocked)
|
||||||
|
else
|
||||||
|
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.AppController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.Web.OAuth.App
|
||||||
|
alias Pleroma.Web.OAuth.Scopes
|
||||||
|
alias Pleroma.Web.OAuth.Token
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
@local_mastodon_name "Mastodon-Local"
|
||||||
|
|
||||||
|
@doc "POST /api/v1/apps"
|
||||||
|
def create(conn, params) do
|
||||||
|
scopes = Scopes.fetch_scopes(params, ["read"])
|
||||||
|
|
||||||
|
app_attrs =
|
||||||
|
params
|
||||||
|
|> Map.drop(["scope", "scopes"])
|
||||||
|
|> Map.put("scopes", scopes)
|
||||||
|
|
||||||
|
with cs <- App.register_changeset(%App{}, app_attrs),
|
||||||
|
false <- cs.changes[:client_name] == @local_mastodon_name,
|
||||||
|
{:ok, app} <- Repo.insert(cs) do
|
||||||
|
render(conn, "show.json", app: app)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/apps/verify_credentials"
|
||||||
|
def verify_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
|
||||||
|
with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
|
||||||
|
render(conn, "short.json", app: app)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.AuthController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.OAuth.App
|
||||||
|
alias Pleroma.Web.OAuth.Authorization
|
||||||
|
alias Pleroma.Web.OAuth.Token
|
||||||
|
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
@local_mastodon_name "Mastodon-Local"
|
||||||
|
|
||||||
|
plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset)
|
||||||
|
|
||||||
|
@doc "GET /web/login"
|
||||||
|
def login(%{assigns: %{user: %User{}}} = conn, _params) do
|
||||||
|
redirect(conn, to: local_mastodon_root_path(conn))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Local Mastodon FE login init action"
|
||||||
|
def login(conn, %{"code" => auth_token}) do
|
||||||
|
with {:ok, app} <- get_or_make_app(),
|
||||||
|
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
|
||||||
|
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||||
|
conn
|
||||||
|
|> put_session(:oauth_token, token.token)
|
||||||
|
|> redirect(to: local_mastodon_root_path(conn))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Local Mastodon FE callback action"
|
||||||
|
def login(conn, _) do
|
||||||
|
with {:ok, app} <- get_or_make_app() do
|
||||||
|
path =
|
||||||
|
o_auth_path(conn, :authorize,
|
||||||
|
response_type: "code",
|
||||||
|
client_id: app.client_id,
|
||||||
|
redirect_uri: ".",
|
||||||
|
scope: Enum.join(app.scopes, " ")
|
||||||
|
)
|
||||||
|
|
||||||
|
redirect(conn, to: path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "DELETE /auth/sign_out"
|
||||||
|
def logout(conn, _) do
|
||||||
|
conn
|
||||||
|
|> clear_session
|
||||||
|
|> redirect(to: "/")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /auth/password"
|
||||||
|
def password_reset(conn, params) do
|
||||||
|
nickname_or_email = params["email"] || params["nickname"]
|
||||||
|
|
||||||
|
with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
|
||||||
|
conn
|
||||||
|
|> put_status(:no_content)
|
||||||
|
|> json("")
|
||||||
|
else
|
||||||
|
{:error, "unknown user"} ->
|
||||||
|
send_resp(conn, :not_found, "")
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
send_resp(conn, :bad_request, "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp local_mastodon_root_path(conn) do
|
||||||
|
case get_session(conn, :return_to) do
|
||||||
|
nil ->
|
||||||
|
mastodon_api_path(conn, :index, ["getting-started"])
|
||||||
|
|
||||||
|
return_to ->
|
||||||
|
delete_session(conn, :return_to)
|
||||||
|
return_to
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
|
||||||
|
defp get_or_make_app do
|
||||||
|
%{client_name: @local_mastodon_name, redirect_uris: "."}
|
||||||
|
|> App.get_or_make(["read", "write", "follow", "push"])
|
||||||
|
end
|
||||||
|
end
|
|
@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
|
||||||
def index(%{assigns: %{user: followed}} = conn, _params) do
|
def index(%{assigns: %{user: followed}} = conn, _params) do
|
||||||
follow_requests = User.get_follow_requests(followed)
|
follow_requests = User.get_follow_requests(followed)
|
||||||
|
|
||||||
render(conn, "accounts.json", for: followed, users: follow_requests, as: :user)
|
render(conn, "index.json", for: followed, users: follow_requests, as: :user)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "POST /api/v1/follow_requests/:id/authorize"
|
@doc "POST /api/v1/follow_requests/:id/authorize"
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.InstanceController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
@doc "GET /api/v1/instance"
|
||||||
|
def show(conn, _params) do
|
||||||
|
render(conn, "show.json")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/instance/peers"
|
||||||
|
def peers(conn, _params) do
|
||||||
|
json(conn, Pleroma.Stats.get_peers())
|
||||||
|
end
|
||||||
|
end
|
|
@ -49,7 +49,7 @@ def list_accounts(%{assigns: %{user: user, list: list}} = conn, _) do
|
||||||
with {:ok, users} <- Pleroma.List.get_following(list) do
|
with {:ok, users} <- Pleroma.List.get_following(list) do
|
||||||
conn
|
conn
|
||||||
|> put_view(AccountView)
|
|> put_view(AccountView)
|
||||||
|> render("accounts.json", for: user, users: users, as: :user)
|
|> render("index.json", for: user, users: users, as: :user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,297 +5,23 @@
|
||||||
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
import Pleroma.Web.ControllerHelper,
|
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
|
||||||
only: [json_response: 3, add_link_headers: 2, truthy_param?: 1]
|
|
||||||
|
|
||||||
alias Ecto.Changeset
|
|
||||||
alias Pleroma.Activity
|
|
||||||
alias Pleroma.Bookmark
|
alias Pleroma.Bookmark
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
alias Pleroma.Emoji
|
|
||||||
alias Pleroma.HTTP
|
|
||||||
alias Pleroma.Object
|
|
||||||
alias Pleroma.Pagination
|
alias Pleroma.Pagination
|
||||||
alias Pleroma.Plugs.RateLimiter
|
|
||||||
alias Pleroma.Repo
|
|
||||||
alias Pleroma.Stats
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web
|
alias Pleroma.Web
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.MastodonAPI.AccountView
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
alias Pleroma.Web.MastodonAPI.AppView
|
|
||||||
alias Pleroma.Web.MastodonAPI.ListView
|
|
||||||
alias Pleroma.Web.MastodonAPI.MastodonAPI
|
|
||||||
alias Pleroma.Web.MastodonAPI.MastodonView
|
alias Pleroma.Web.MastodonAPI.MastodonView
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
alias Pleroma.Web.MediaProxy
|
|
||||||
alias Pleroma.Web.OAuth.App
|
|
||||||
alias Pleroma.Web.OAuth.Authorization
|
|
||||||
alias Pleroma.Web.OAuth.Scopes
|
|
||||||
alias Pleroma.Web.OAuth.Token
|
|
||||||
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
require Pleroma.Constants
|
|
||||||
|
|
||||||
@rate_limited_relations_actions ~w(follow unfollow)a
|
|
||||||
|
|
||||||
plug(
|
|
||||||
RateLimiter,
|
|
||||||
{:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
|
|
||||||
)
|
|
||||||
|
|
||||||
plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
|
|
||||||
plug(RateLimiter, :app_account_creation when action == :account_register)
|
|
||||||
plug(RateLimiter, :search when action in [:search, :search2, :account_search])
|
|
||||||
plug(RateLimiter, :password_reset when action == :password_reset)
|
|
||||||
plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
|
|
||||||
|
|
||||||
@local_mastodon_name "Mastodon-Local"
|
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
def create_app(conn, params) do
|
|
||||||
scopes = Scopes.fetch_scopes(params, ["read"])
|
|
||||||
|
|
||||||
app_attrs =
|
|
||||||
params
|
|
||||||
|> Map.drop(["scope", "scopes"])
|
|
||||||
|> Map.put("scopes", scopes)
|
|
||||||
|
|
||||||
with cs <- App.register_changeset(%App{}, app_attrs),
|
|
||||||
false <- cs.changes[:client_name] == @local_mastodon_name,
|
|
||||||
{:ok, app} <- Repo.insert(cs) do
|
|
||||||
conn
|
|
||||||
|> put_view(AppView)
|
|
||||||
|> render("show.json", %{app: app})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp add_if_present(
|
|
||||||
map,
|
|
||||||
params,
|
|
||||||
params_field,
|
|
||||||
map_field,
|
|
||||||
value_function \\ fn x -> {:ok, x} end
|
|
||||||
) do
|
|
||||||
if Map.has_key?(params, params_field) do
|
|
||||||
case value_function.(params[params_field]) do
|
|
||||||
{:ok, new_value} -> Map.put(map, map_field, new_value)
|
|
||||||
:error -> map
|
|
||||||
end
|
|
||||||
else
|
|
||||||
map
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_credentials(%{assigns: %{user: user}} = conn, params) do
|
|
||||||
original_user = user
|
|
||||||
|
|
||||||
user_params =
|
|
||||||
%{}
|
|
||||||
|> add_if_present(params, "display_name", :name)
|
|
||||||
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
|
|
||||||
|> add_if_present(params, "avatar", :avatar, fn value ->
|
|
||||||
with %Plug.Upload{} <- value,
|
|
||||||
{:ok, object} <- ActivityPub.upload(value, type: :avatar) do
|
|
||||||
{:ok, object.data}
|
|
||||||
else
|
|
||||||
_ -> :error
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
|
|
||||||
|
|
||||||
user_info_emojis =
|
|
||||||
user.info
|
|
||||||
|> Map.get(:emoji, [])
|
|
||||||
|> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
|
|
||||||
|> Enum.dedup()
|
|
||||||
|
|
||||||
info_params =
|
|
||||||
[
|
|
||||||
:no_rich_text,
|
|
||||||
:locked,
|
|
||||||
:hide_followers_count,
|
|
||||||
:hide_follows_count,
|
|
||||||
:hide_followers,
|
|
||||||
:hide_follows,
|
|
||||||
:hide_favorites,
|
|
||||||
:show_role,
|
|
||||||
:skip_thread_containment,
|
|
||||||
:discoverable
|
|
||||||
]
|
|
||||||
|> Enum.reduce(%{}, fn key, acc ->
|
|
||||||
add_if_present(acc, params, to_string(key), key, fn value ->
|
|
||||||
{:ok, truthy_param?(value)}
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|> add_if_present(params, "default_scope", :default_scope)
|
|
||||||
|> add_if_present(params, "fields", :fields, fn fields ->
|
|
||||||
fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
|
|
||||||
|
|
||||||
{:ok, fields}
|
|
||||||
end)
|
|
||||||
|> add_if_present(params, "fields", :raw_fields)
|
|
||||||
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
|
|
||||||
{:ok, Map.merge(user.info.pleroma_settings_store, value)}
|
|
||||||
end)
|
|
||||||
|> add_if_present(params, "header", :banner, fn value ->
|
|
||||||
with %Plug.Upload{} <- value,
|
|
||||||
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
|
|
||||||
{:ok, object.data}
|
|
||||||
else
|
|
||||||
_ -> :error
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|> add_if_present(params, "pleroma_background_image", :background, fn value ->
|
|
||||||
with %Plug.Upload{} <- value,
|
|
||||||
{:ok, object} <- ActivityPub.upload(value, type: :background) do
|
|
||||||
{:ok, object.data}
|
|
||||||
else
|
|
||||||
_ -> :error
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|> Map.put(:emoji, user_info_emojis)
|
|
||||||
|
|
||||||
changeset =
|
|
||||||
user
|
|
||||||
|> User.update_changeset(user_params)
|
|
||||||
|> User.change_info(&User.Info.profile_update(&1, info_params))
|
|
||||||
|
|
||||||
with {:ok, user} <- User.update_and_set_cache(changeset) do
|
|
||||||
if original_user != user, do: CommonAPI.update(user)
|
|
||||||
|
|
||||||
json(
|
|
||||||
conn,
|
|
||||||
AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
|
|
||||||
)
|
|
||||||
else
|
|
||||||
_e -> render_error(conn, :forbidden, "Invalid request")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
|
|
||||||
change = Changeset.change(user, %{avatar: nil})
|
|
||||||
{:ok, user} = User.update_and_set_cache(change)
|
|
||||||
CommonAPI.update(user)
|
|
||||||
|
|
||||||
json(conn, %{url: nil})
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_avatar(%{assigns: %{user: user}} = conn, params) do
|
|
||||||
{:ok, object} = ActivityPub.upload(params, type: :avatar)
|
|
||||||
change = Changeset.change(user, %{avatar: object.data})
|
|
||||||
{:ok, user} = User.update_and_set_cache(change)
|
|
||||||
CommonAPI.update(user)
|
|
||||||
%{"url" => [%{"href" => href} | _]} = object.data
|
|
||||||
|
|
||||||
json(conn, %{url: href})
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
|
|
||||||
new_info = %{"banner" => %{}}
|
|
||||||
|
|
||||||
with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
|
|
||||||
CommonAPI.update(user)
|
|
||||||
json(conn, %{url: nil})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_banner(%{assigns: %{user: user}} = conn, params) do
|
|
||||||
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
|
|
||||||
new_info <- %{"banner" => object.data},
|
|
||||||
{:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
|
|
||||||
CommonAPI.update(user)
|
|
||||||
%{"url" => [%{"href" => href} | _]} = object.data
|
|
||||||
|
|
||||||
json(conn, %{url: href})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
|
|
||||||
new_info = %{"background" => %{}}
|
|
||||||
|
|
||||||
with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
|
|
||||||
json(conn, %{url: nil})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_background(%{assigns: %{user: user}} = conn, params) do
|
|
||||||
with {:ok, object} <- ActivityPub.upload(params, type: :background),
|
|
||||||
new_info <- %{"background" => object.data},
|
|
||||||
{:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
|
|
||||||
%{"url" => [%{"href" => href} | _]} = object.data
|
|
||||||
|
|
||||||
json(conn, %{url: href})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
|
|
||||||
chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
|
|
||||||
|
|
||||||
account =
|
|
||||||
AccountView.render("account.json", %{
|
|
||||||
user: user,
|
|
||||||
for: user,
|
|
||||||
with_pleroma_settings: true,
|
|
||||||
with_chat_token: chat_token
|
|
||||||
})
|
|
||||||
|
|
||||||
json(conn, account)
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
|
|
||||||
with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
|
|
||||||
conn
|
|
||||||
|> put_view(AppView)
|
|
||||||
|> render("short.json", %{app: app})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
|
|
||||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
|
|
||||||
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
|
|
||||||
account = AccountView.render("account.json", %{user: user, for: for_user})
|
|
||||||
json(conn, account)
|
|
||||||
else
|
|
||||||
_e -> render_error(conn, :not_found, "Can't find user")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@mastodon_api_level "2.7.2"
|
|
||||||
|
|
||||||
def masto_instance(conn, _params) do
|
|
||||||
instance = Config.get(:instance)
|
|
||||||
|
|
||||||
response = %{
|
|
||||||
uri: Web.base_url(),
|
|
||||||
title: Keyword.get(instance, :name),
|
|
||||||
description: Keyword.get(instance, :description),
|
|
||||||
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
|
|
||||||
email: Keyword.get(instance, :email),
|
|
||||||
urls: %{
|
|
||||||
streaming_api: Pleroma.Web.Endpoint.websocket_url()
|
|
||||||
},
|
|
||||||
stats: Stats.get_stats(),
|
|
||||||
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
|
|
||||||
languages: ["en"],
|
|
||||||
registrations: Pleroma.Config.get([:instance, :registrations_open]),
|
|
||||||
# Extra (not present in Mastodon):
|
|
||||||
max_toot_chars: Keyword.get(instance, :limit),
|
|
||||||
poll_limits: Keyword.get(instance, :poll_limits)
|
|
||||||
}
|
|
||||||
|
|
||||||
json(conn, response)
|
|
||||||
end
|
|
||||||
|
|
||||||
def peers(conn, _params) do
|
|
||||||
json(conn, Stats.get_peers())
|
|
||||||
end
|
|
||||||
|
|
||||||
defp mastodonized_emoji do
|
defp mastodonized_emoji do
|
||||||
Pleroma.Emoji.get_all()
|
Pleroma.Emoji.get_all()
|
||||||
|> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
|
|> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
|
||||||
|
@ -318,200 +44,13 @@ def custom_emojis(conn, _params) do
|
||||||
json(conn, mastodon_emoji)
|
json(conn, mastodon_emoji)
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
|
def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
|
||||||
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
|
|
||||||
params =
|
|
||||||
params
|
|
||||||
|> Map.put("tag", params["tagged"])
|
|
||||||
|
|
||||||
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> add_link_headers(activities)
|
|
||||||
|> put_view(StatusView)
|
|
||||||
|> render("index.json", %{
|
|
||||||
activities: activities,
|
|
||||||
for: reading_user,
|
|
||||||
as: :activity
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
|
||||||
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
|
|
||||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
|
||||||
true <- Visibility.visible_for_user?(activity, user) do
|
|
||||||
conn
|
|
||||||
|> put_view(StatusView)
|
|
||||||
|> try_render("poll.json", %{object: object, for: user})
|
|
||||||
else
|
|
||||||
error when is_nil(error) or error == false ->
|
|
||||||
render_error(conn, :not_found, "Record not found")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_cached_vote_or_vote(user, object, choices) do
|
|
||||||
idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
|
|
||||||
|
|
||||||
{_, res} =
|
|
||||||
Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
|
|
||||||
case CommonAPI.vote(user, object, choices) do
|
|
||||||
{:error, _message} = res -> {:ignore, res}
|
|
||||||
res -> {:commit, res}
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
res
|
|
||||||
end
|
|
||||||
|
|
||||||
def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
|
|
||||||
with %Object{} = object <- Object.get_by_id(id),
|
|
||||||
true <- object.data["type"] == "Question",
|
|
||||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
|
||||||
true <- Visibility.visible_for_user?(activity, user),
|
|
||||||
{:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
|
|
||||||
conn
|
|
||||||
|> put_view(StatusView)
|
|
||||||
|> try_render("poll.json", %{object: object, for: user})
|
|
||||||
else
|
|
||||||
nil ->
|
|
||||||
render_error(conn, :not_found, "Record not found")
|
|
||||||
|
|
||||||
false ->
|
|
||||||
render_error(conn, :not_found, "Record not found")
|
|
||||||
|
|
||||||
{:error, message} ->
|
|
||||||
conn
|
|
||||||
|> put_status(:unprocessable_entity)
|
|
||||||
|> json(%{error: message})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
|
||||||
targets = User.get_all_by_ids(List.wrap(id))
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("relationships.json", %{user: user, targets: targets})
|
|
||||||
end
|
|
||||||
|
|
||||||
# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
|
|
||||||
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
|
|
||||||
|
|
||||||
def update_media(
|
|
||||||
%{assigns: %{user: user}} = conn,
|
|
||||||
%{"id" => id, "description" => description} = _
|
|
||||||
)
|
|
||||||
when is_binary(description) do
|
|
||||||
with %Object{} = object <- Repo.get(Object, id),
|
|
||||||
true <- Object.authorize_mutation(object, user),
|
|
||||||
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
|
|
||||||
attachment_data = Map.put(data, "id", object.id)
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> put_view(StatusView)
|
|
||||||
|> render("attachment.json", %{attachment: attachment_data})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_media(_conn, _data), do: {:error, :bad_request}
|
|
||||||
|
|
||||||
def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
|
|
||||||
with {:ok, object} <-
|
|
||||||
ActivityPub.upload(
|
|
||||||
file,
|
|
||||||
actor: User.ap_id(user),
|
|
||||||
description: Map.get(data, "description")
|
|
||||||
) do
|
|
||||||
attachment_data = Map.put(object.data, "id", object.id)
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> put_view(StatusView)
|
|
||||||
|> render("attachment.json", %{attachment: attachment_data})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
|
|
||||||
with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
|
|
||||||
%{} = attachment_data <- Map.put(object.data, "id", object.id),
|
|
||||||
# Reject if not an image
|
|
||||||
%{type: "image"} = rendered <-
|
|
||||||
StatusView.render("attachment.json", %{attachment: attachment_data}) do
|
|
||||||
# Sure!
|
|
||||||
# Save to the user's info
|
|
||||||
{:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
|
|
||||||
|
|
||||||
json(conn, rendered)
|
|
||||||
else
|
|
||||||
%{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_mascot(%{assigns: %{user: user}} = conn, _params) do
|
|
||||||
mascot = User.get_mascot(user)
|
|
||||||
|
|
||||||
json(conn, mascot)
|
|
||||||
end
|
|
||||||
|
|
||||||
def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
|
|
||||||
with %User{} = user <- User.get_cached_by_id(id),
|
|
||||||
followers <- MastodonAPI.get_followers(user, params) do
|
|
||||||
followers =
|
|
||||||
cond do
|
|
||||||
for_user && user.id == for_user.id -> followers
|
|
||||||
user.info.hide_followers -> []
|
|
||||||
true -> followers
|
|
||||||
end
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> add_link_headers(followers)
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("accounts.json", %{for: for_user, users: followers, as: :user})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
|
|
||||||
with %User{} = user <- User.get_cached_by_id(id),
|
|
||||||
followers <- MastodonAPI.get_friends(user, params) do
|
|
||||||
followers =
|
|
||||||
cond do
|
|
||||||
for_user && user.id == for_user.id -> followers
|
|
||||||
user.info.hide_follows -> []
|
|
||||||
true -> followers
|
|
||||||
end
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> add_link_headers(followers)
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("accounts.json", %{for: for_user, users: followers, as: :user})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
|
|
||||||
with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
|
|
||||||
{_, true} <- {:followed, follower.id != followed.id},
|
|
||||||
{:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
|
|
||||||
conn
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("relationship.json", %{user: follower, target: followed})
|
|
||||||
else
|
|
||||||
{:followed, _} ->
|
|
||||||
{:error, :not_found}
|
|
||||||
|
|
||||||
{:error, message} ->
|
|
||||||
conn
|
|
||||||
|> put_status(:forbidden)
|
|
||||||
|> json(%{error: message})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
|
|
||||||
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
|
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
|
||||||
{_, true} <- {:followed, follower.id != followed.id},
|
{_, true} <- {:followed, follower.id != followed.id},
|
||||||
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
|
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
|
||||||
conn
|
conn
|
||||||
|> put_view(AccountView)
|
|> put_view(AccountView)
|
||||||
|> render("account.json", %{user: followed, for: follower})
|
|> render("show.json", %{user: followed, for: follower})
|
||||||
else
|
else
|
||||||
{:followed, _} ->
|
{:followed, _} ->
|
||||||
{:error, :not_found}
|
{:error, :not_found}
|
||||||
|
@ -523,123 +62,20 @@ def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
|
|
||||||
with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
|
|
||||||
{_, true} <- {:followed, follower.id != followed.id},
|
|
||||||
{:ok, follower} <- CommonAPI.unfollow(follower, followed) do
|
|
||||||
conn
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("relationship.json", %{user: follower, target: followed})
|
|
||||||
else
|
|
||||||
{:followed, _} ->
|
|
||||||
{:error, :not_found}
|
|
||||||
|
|
||||||
error ->
|
|
||||||
error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
|
|
||||||
notifications =
|
|
||||||
if Map.has_key?(params, "notifications"),
|
|
||||||
do: params["notifications"] in [true, "True", "true", "1"],
|
|
||||||
else: true
|
|
||||||
|
|
||||||
with %User{} = muted <- User.get_cached_by_id(id),
|
|
||||||
{:ok, muter} <- User.mute(muter, muted, notifications) do
|
|
||||||
conn
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("relationship.json", %{user: muter, target: muted})
|
|
||||||
else
|
|
||||||
{:error, message} ->
|
|
||||||
conn
|
|
||||||
|> put_status(:forbidden)
|
|
||||||
|> json(%{error: message})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
|
|
||||||
with %User{} = muted <- User.get_cached_by_id(id),
|
|
||||||
{:ok, muter} <- User.unmute(muter, muted) do
|
|
||||||
conn
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("relationship.json", %{user: muter, target: muted})
|
|
||||||
else
|
|
||||||
{:error, message} ->
|
|
||||||
conn
|
|
||||||
|> put_status(:forbidden)
|
|
||||||
|> json(%{error: message})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def mutes(%{assigns: %{user: user}} = conn, _) do
|
def mutes(%{assigns: %{user: user}} = conn, _) do
|
||||||
with muted_accounts <- User.muted_users(user) do
|
with muted_accounts <- User.muted_users(user) do
|
||||||
res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
|
res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
|
||||||
json(conn, res)
|
json(conn, res)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
|
|
||||||
with %User{} = blocked <- User.get_cached_by_id(id),
|
|
||||||
{:ok, blocker} <- User.block(blocker, blocked),
|
|
||||||
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
|
|
||||||
conn
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("relationship.json", %{user: blocker, target: blocked})
|
|
||||||
else
|
|
||||||
{:error, message} ->
|
|
||||||
conn
|
|
||||||
|> put_status(:forbidden)
|
|
||||||
|> json(%{error: message})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
|
|
||||||
with %User{} = blocked <- User.get_cached_by_id(id),
|
|
||||||
{:ok, blocker} <- User.unblock(blocker, blocked),
|
|
||||||
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
|
|
||||||
conn
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("relationship.json", %{user: blocker, target: blocked})
|
|
||||||
else
|
|
||||||
{:error, message} ->
|
|
||||||
conn
|
|
||||||
|> put_status(:forbidden)
|
|
||||||
|> json(%{error: message})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def blocks(%{assigns: %{user: user}} = conn, _) do
|
def blocks(%{assigns: %{user: user}} = conn, _) do
|
||||||
with blocked_accounts <- User.blocked_users(user) do
|
with blocked_accounts <- User.blocked_users(user) do
|
||||||
res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
|
res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
|
||||||
json(conn, res)
|
json(conn, res)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
|
||||||
with %User{} = subscription_target <- User.get_cached_by_id(id),
|
|
||||||
{:ok, subscription_target} = User.subscribe(user, subscription_target) do
|
|
||||||
conn
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("relationship.json", %{user: user, target: subscription_target})
|
|
||||||
else
|
|
||||||
nil -> {:error, :not_found}
|
|
||||||
e -> e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
|
||||||
with %User{} = subscription_target <- User.get_cached_by_id(id),
|
|
||||||
{:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
|
|
||||||
conn
|
|
||||||
|> put_view(AccountView)
|
|
||||||
|> render("relationship.json", %{user: user, target: subscription_target})
|
|
||||||
else
|
|
||||||
nil -> {:error, :not_found}
|
|
||||||
e -> e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def favourites(%{assigns: %{user: user}} = conn, params) do
|
def favourites(%{assigns: %{user: user}} = conn, params) do
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|
@ -657,37 +93,6 @@ def favourites(%{assigns: %{user: user}} = conn, params) do
|
||||||
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
|
|
||||||
with %User{} = user <- User.get_by_id(id),
|
|
||||||
false <- user.info.hide_favorites do
|
|
||||||
params =
|
|
||||||
params
|
|
||||||
|> Map.put("type", "Create")
|
|
||||||
|> Map.put("favorited_by", user.ap_id)
|
|
||||||
|> Map.put("blocking_user", for_user)
|
|
||||||
|
|
||||||
recipients =
|
|
||||||
if for_user do
|
|
||||||
[Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
|
|
||||||
else
|
|
||||||
[Pleroma.Constants.as_public()]
|
|
||||||
end
|
|
||||||
|
|
||||||
activities =
|
|
||||||
recipients
|
|
||||||
|> ActivityPub.fetch_activities(params)
|
|
||||||
|> Enum.reverse()
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> add_link_headers(activities)
|
|
||||||
|> put_view(StatusView)
|
|
||||||
|> render("index.json", %{activities: activities, for: for_user, as: :activity})
|
|
||||||
else
|
|
||||||
nil -> {:error, :not_found}
|
|
||||||
true -> render_error(conn, :forbidden, "Can't get favorites")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def bookmarks(%{assigns: %{user: user}} = conn, params) do
|
def bookmarks(%{assigns: %{user: user}} = conn, params) do
|
||||||
user = User.get_cached_by_id(user.id)
|
user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
|
@ -705,14 +110,6 @@ def bookmarks(%{assigns: %{user: user}} = conn, params) do
|
||||||
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
|
|
||||||
lists = Pleroma.List.get_lists_account_belongs(user, account_id)
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> put_view(ListView)
|
|
||||||
|> render("index.json", %{lists: lists})
|
|
||||||
end
|
|
||||||
|
|
||||||
def index(%{assigns: %{user: user}} = conn, _params) do
|
def index(%{assigns: %{user: user}} = conn, _params) do
|
||||||
token = get_session(conn, :oauth_token)
|
token = get_session(conn, :oauth_token)
|
||||||
|
|
||||||
|
@ -721,8 +118,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do
|
||||||
|
|
||||||
limit = Config.get([:instance, :limit])
|
limit = Config.get([:instance, :limit])
|
||||||
|
|
||||||
accounts =
|
accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
|
||||||
Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
|
|
||||||
|
|
||||||
initial_state =
|
initial_state =
|
||||||
%{
|
%{
|
||||||
|
@ -829,61 +225,6 @@ def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _para
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def login(%{assigns: %{user: %User{}}} = conn, _params) do
|
|
||||||
redirect(conn, to: local_mastodon_root_path(conn))
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc "Local Mastodon FE login init action"
|
|
||||||
def login(conn, %{"code" => auth_token}) do
|
|
||||||
with {:ok, app} <- get_or_make_app(),
|
|
||||||
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
|
|
||||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
|
||||||
conn
|
|
||||||
|> put_session(:oauth_token, token.token)
|
|
||||||
|> redirect(to: local_mastodon_root_path(conn))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc "Local Mastodon FE callback action"
|
|
||||||
def login(conn, _) do
|
|
||||||
with {:ok, app} <- get_or_make_app() do
|
|
||||||
path =
|
|
||||||
o_auth_path(conn, :authorize,
|
|
||||||
response_type: "code",
|
|
||||||
client_id: app.client_id,
|
|
||||||
redirect_uri: ".",
|
|
||||||
scope: Enum.join(app.scopes, " ")
|
|
||||||
)
|
|
||||||
|
|
||||||
redirect(conn, to: path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp local_mastodon_root_path(conn) do
|
|
||||||
case get_session(conn, :return_to) do
|
|
||||||
nil ->
|
|
||||||
mastodon_api_path(conn, :index, ["getting-started"])
|
|
||||||
|
|
||||||
return_to ->
|
|
||||||
delete_session(conn, :return_to)
|
|
||||||
return_to
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
|
|
||||||
defp get_or_make_app do
|
|
||||||
App.get_or_make(
|
|
||||||
%{client_name: @local_mastodon_name, redirect_uris: "."},
|
|
||||||
["read", "write", "follow", "push"]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def logout(conn, _) do
|
|
||||||
conn
|
|
||||||
|> clear_session
|
|
||||||
|> redirect(to: "/")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Stubs for unimplemented mastodon api
|
# Stubs for unimplemented mastodon api
|
||||||
#
|
#
|
||||||
def empty_array(conn, _) do
|
def empty_array(conn, _) do
|
||||||
|
@ -896,134 +237,6 @@ def empty_object(conn, _) do
|
||||||
json(conn, %{})
|
json(conn, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
def suggestions(%{assigns: %{user: user}} = conn, _) do
|
|
||||||
suggestions = Config.get(:suggestions)
|
|
||||||
|
|
||||||
if Keyword.get(suggestions, :enabled, false) do
|
|
||||||
api = Keyword.get(suggestions, :third_party_engine, "")
|
|
||||||
timeout = Keyword.get(suggestions, :timeout, 5000)
|
|
||||||
limit = Keyword.get(suggestions, :limit, 23)
|
|
||||||
|
|
||||||
host = Config.get([Pleroma.Web.Endpoint, :url, :host])
|
|
||||||
|
|
||||||
user = user.nickname
|
|
||||||
|
|
||||||
url =
|
|
||||||
api
|
|
||||||
|> String.replace("{{host}}", host)
|
|
||||||
|> String.replace("{{user}}", user)
|
|
||||||
|
|
||||||
with {:ok, %{status: 200, body: body}} <-
|
|
||||||
HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
|
|
||||||
{:ok, data} <- Jason.decode(body) do
|
|
||||||
data =
|
|
||||||
data
|
|
||||||
|> Enum.slice(0, limit)
|
|
||||||
|> Enum.map(fn x ->
|
|
||||||
x
|
|
||||||
|> Map.put("id", fetch_suggestion_id(x))
|
|
||||||
|> Map.put("avatar", MediaProxy.url(x["avatar"]))
|
|
||||||
|> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
|
|
||||||
end)
|
|
||||||
|
|
||||||
json(conn, data)
|
|
||||||
else
|
|
||||||
e ->
|
|
||||||
Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
|
|
||||||
end
|
|
||||||
else
|
|
||||||
json(conn, [])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fetch_suggestion_id(attrs) do
|
|
||||||
case User.get_or_fetch(attrs["acct"]) do
|
|
||||||
{:ok, %User{id: id}} -> id
|
|
||||||
_ -> 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_register(
|
|
||||||
%{assigns: %{app: app}} = conn,
|
|
||||||
%{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
|
|
||||||
) do
|
|
||||||
params =
|
|
||||||
params
|
|
||||||
|> Map.take([
|
|
||||||
"email",
|
|
||||||
"captcha_solution",
|
|
||||||
"captcha_token",
|
|
||||||
"captcha_answer_data",
|
|
||||||
"token",
|
|
||||||
"password"
|
|
||||||
])
|
|
||||||
|> Map.put("nickname", nickname)
|
|
||||||
|> Map.put("fullname", params["fullname"] || nickname)
|
|
||||||
|> Map.put("bio", params["bio"] || "")
|
|
||||||
|> Map.put("confirm", params["password"])
|
|
||||||
|
|
||||||
with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
|
|
||||||
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
|
|
||||||
json(conn, %{
|
|
||||||
token_type: "Bearer",
|
|
||||||
access_token: token.token,
|
|
||||||
scope: app.scopes,
|
|
||||||
created_at: Token.Utils.format_created_at(token)
|
|
||||||
})
|
|
||||||
else
|
|
||||||
{:error, errors} ->
|
|
||||||
conn
|
|
||||||
|> put_status(:bad_request)
|
|
||||||
|> json(errors)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_register(%{assigns: %{app: _app}} = conn, _) do
|
|
||||||
render_error(conn, :bad_request, "Missing parameters")
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_register(conn, _) do
|
|
||||||
render_error(conn, :forbidden, "Invalid credentials")
|
|
||||||
end
|
|
||||||
|
|
||||||
def password_reset(conn, params) do
|
|
||||||
nickname_or_email = params["email"] || params["nickname"]
|
|
||||||
|
|
||||||
with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
|
|
||||||
conn
|
|
||||||
|> put_status(:no_content)
|
|
||||||
|> json("")
|
|
||||||
else
|
|
||||||
{:error, "unknown user"} ->
|
|
||||||
send_resp(conn, :not_found, "")
|
|
||||||
|
|
||||||
{:error, _} ->
|
|
||||||
send_resp(conn, :bad_request, "")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_confirmation_resend(conn, params) do
|
|
||||||
nickname_or_email = params["email"] || params["nickname"]
|
|
||||||
|
|
||||||
with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
|
|
||||||
{:ok, _} <- User.try_send_confirmation_email(user) do
|
|
||||||
conn
|
|
||||||
|> json_response(:no_content, "")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def try_render(conn, target, params)
|
|
||||||
when is_binary(target) do
|
|
||||||
case render(conn, target, params) do
|
|
||||||
nil -> render_error(conn, :not_implemented, "Can't display this activity")
|
|
||||||
res -> res
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def try_render(conn, _, _) do
|
|
||||||
render_error(conn, :not_implemented, "Can't display this activity")
|
|
||||||
end
|
|
||||||
|
|
||||||
defp present?(nil), do: false
|
defp present?(nil), do: false
|
||||||
defp present?(false), do: false
|
defp present?(false), do: false
|
||||||
defp present?(_), do: true
|
defp present?(_), do: true
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.MediaController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
|
||||||
|
|
||||||
|
@doc "POST /api/v1/media"
|
||||||
|
def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
|
||||||
|
with {:ok, object} <-
|
||||||
|
ActivityPub.upload(
|
||||||
|
file,
|
||||||
|
actor: User.ap_id(user),
|
||||||
|
description: Map.get(data, "description")
|
||||||
|
) do
|
||||||
|
attachment_data = Map.put(object.data, "id", object.id)
|
||||||
|
|
||||||
|
render(conn, "attachment.json", %{attachment: attachment_data})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "PUT /api/v1/media/:id"
|
||||||
|
def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => description})
|
||||||
|
when is_binary(description) do
|
||||||
|
with %Object{} = object <- Object.get_by_id(id),
|
||||||
|
true <- Object.authorize_mutation(object, user),
|
||||||
|
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
|
||||||
|
attachment_data = Map.put(data, "id", object.id)
|
||||||
|
|
||||||
|
render(conn, "attachment.json", %{attachment: attachment_data})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(_conn, _data), do: {:error, :bad_request}
|
||||||
|
end
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.PollController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
import Pleroma.Web.ControllerHelper, only: [try_render: 3, json_response: 3]
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
@doc "GET /api/v1/polls/:id"
|
||||||
|
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
|
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
|
||||||
|
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
||||||
|
true <- Visibility.visible_for_user?(activity, user) do
|
||||||
|
try_render(conn, "show.json", %{object: object, for: user})
|
||||||
|
else
|
||||||
|
error when is_nil(error) or error == false ->
|
||||||
|
render_error(conn, :not_found, "Record not found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/polls/:id/votes"
|
||||||
|
def vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
|
||||||
|
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
|
||||||
|
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
||||||
|
true <- Visibility.visible_for_user?(activity, user),
|
||||||
|
{:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
|
||||||
|
try_render(conn, "show.json", %{object: object, for: user})
|
||||||
|
else
|
||||||
|
nil -> render_error(conn, :not_found, "Record not found")
|
||||||
|
false -> render_error(conn, :not_found, "Record not found")
|
||||||
|
{:error, message} -> json_response(conn, :unprocessable_entity, %{error: message})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_cached_vote_or_vote(user, object, choices) do
|
||||||
|
idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
|
||||||
|
|
||||||
|
Cachex.fetch!(:idempotency_cache, idempotency_key, fn ->
|
||||||
|
case CommonAPI.vote(user, object, choices) do
|
||||||
|
{:error, _message} = res -> {:ignore, res}
|
||||||
|
res -> {:commit, res}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,7 +22,7 @@ def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) d
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_view(AccountView)
|
|> put_view(AccountView)
|
||||||
|> render("accounts.json", users: accounts, for: user, as: :user)
|
|> render("index.json", users: accounts, for: user, as: :user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search2(conn, params), do: do_search(:v2, conn, params)
|
def search2(conn, params), do: do_search(:v2, conn, params)
|
||||||
|
@ -72,7 +72,7 @@ defp search_options(params, user) do
|
||||||
|
|
||||||
defp resource_search(_, "accounts", query, options) do
|
defp resource_search(_, "accounts", query, options) do
|
||||||
accounts = with_fallback(fn -> User.search(query, options) end)
|
accounts = with_fallback(fn -> User.search(query, options) end)
|
||||||
AccountView.render("accounts.json", users: accounts, for: options[:for_user], as: :user)
|
AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp resource_search(_, "statuses", query, options) do
|
defp resource_search(_, "statuses", query, options) do
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
defmodule Pleroma.Web.MastodonAPI.StatusController do
|
defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3]
|
import Pleroma.Web.ControllerHelper, only: [try_render: 3]
|
||||||
|
|
||||||
require Ecto.Query
|
require Ecto.Query
|
||||||
|
|
||||||
|
@ -125,8 +125,8 @@ def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "POST /api/v1/statuses/:id/reblog"
|
@doc "POST /api/v1/statuses/:id/reblog"
|
||||||
def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
|
def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
|
||||||
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
|
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
|
||||||
%Activity{} = announce <- Activity.normalize(announce.data) do
|
%Activity{} = announce <- Activity.normalize(announce.data) do
|
||||||
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
|
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
|
||||||
end
|
end
|
||||||
|
@ -231,7 +231,7 @@ def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_view(AccountView)
|
|> put_view(AccountView)
|
||||||
|> render("accounts.json", for: user, users: users, as: :user)
|
|> render("index.json", for: user, users: users, as: :user)
|
||||||
else
|
else
|
||||||
{:visible, false} -> {:error, :not_found}
|
{:visible, false} -> {:error, :not_found}
|
||||||
_ -> json(conn, [])
|
_ -> json(conn, [])
|
||||||
|
@ -242,7 +242,19 @@ def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
||||||
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
|
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
|
||||||
%Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
|
%Object{data: %{"announcements" => announces, "id" => ap_id}} <-
|
||||||
|
Object.normalize(activity) do
|
||||||
|
announces =
|
||||||
|
"Announce"
|
||||||
|
|> Activity.Queries.by_type()
|
||||||
|
|> Ecto.Query.where([a], a.actor in ^announces)
|
||||||
|
# this is to use the index
|
||||||
|
|> Activity.Queries.by_object_id(ap_id)
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
|
||||||
|
|> Enum.map(& &1.actor)
|
||||||
|
|> Enum.uniq()
|
||||||
|
|
||||||
users =
|
users =
|
||||||
User
|
User
|
||||||
|> Ecto.Query.where([u], u.ap_id in ^announces)
|
|> Ecto.Query.where([u], u.ap_id in ^announces)
|
||||||
|
@ -251,7 +263,7 @@ def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_view(AccountView)
|
|> put_view(AccountView)
|
||||||
|> render("accounts.json", for: user, users: users, as: :user)
|
|> render("index.json", for: user, users: users, as: :user)
|
||||||
else
|
else
|
||||||
{:visible, false} -> {:error, :not_found}
|
{:visible, false} -> {:error, :not_found}
|
||||||
_ -> json(conn, [])
|
_ -> json(conn, [])
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.MediaProxy
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
@doc "GET /api/v1/suggestions"
|
||||||
|
def index(%{assigns: %{user: user}} = conn, _) do
|
||||||
|
if Config.get([:suggestions, :enabled], false) do
|
||||||
|
with {:ok, data} <- fetch_suggestions(user) do
|
||||||
|
limit = Config.get([:suggestions, :limit], 23)
|
||||||
|
|
||||||
|
data =
|
||||||
|
data
|
||||||
|
|> Enum.slice(0, limit)
|
||||||
|
|> Enum.map(fn x ->
|
||||||
|
x
|
||||||
|
|> Map.put("id", fetch_suggestion_id(x))
|
||||||
|
|> Map.put("avatar", MediaProxy.url(x["avatar"]))
|
||||||
|
|> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
|
||||||
|
end)
|
||||||
|
|
||||||
|
json(conn, data)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
json(conn, [])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_suggestions(user) do
|
||||||
|
api = Config.get([:suggestions, :third_party_engine], "")
|
||||||
|
timeout = Config.get([:suggestions, :timeout], 5000)
|
||||||
|
host = Config.get([Pleroma.Web.Endpoint, :url, :host])
|
||||||
|
|
||||||
|
url =
|
||||||
|
api
|
||||||
|
|> String.replace("{{host}}", host)
|
||||||
|
|> String.replace("{{user}}", user.nickname)
|
||||||
|
|
||||||
|
with {:ok, %{status: 200, body: body}} <-
|
||||||
|
Pleroma.HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]) do
|
||||||
|
Jason.decode(body)
|
||||||
|
else
|
||||||
|
e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_suggestion_id(attrs) do
|
||||||
|
case User.get_or_fetch(attrs["acct"]) do
|
||||||
|
{:ok, %User{id: id}} -> id
|
||||||
|
_ -> 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,15 +11,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
||||||
alias Pleroma.Web.MastodonAPI.AccountView
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
alias Pleroma.Web.MediaProxy
|
alias Pleroma.Web.MediaProxy
|
||||||
|
|
||||||
def render("accounts.json", %{users: users} = opts) do
|
def render("index.json", %{users: users} = opts) do
|
||||||
users
|
users
|
||||||
|> render_many(AccountView, "account.json", opts)
|
|> render_many(AccountView, "show.json", opts)
|
||||||
|> Enum.filter(&Enum.any?/1)
|
|> Enum.filter(&Enum.any?/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("account.json", %{user: user} = opts) do
|
def render("show.json", %{user: user} = opts) do
|
||||||
if User.visible_for?(user, opts[:for]),
|
if User.visible_for?(user, opts[:for]),
|
||||||
do: do_render("account.json", opts),
|
do: do_render("show.json", opts),
|
||||||
else: %{}
|
else: %{}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ def render("relationships.json", %{user: user, targets: targets}) do
|
||||||
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
|
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_render("account.json", %{user: user} = opts) do
|
defp do_render("show.json", %{user: user} = opts) do
|
||||||
display_name = HTML.strip_tags(user.name || user.nickname)
|
display_name = HTML.strip_tags(user.name || user.nickname)
|
||||||
|
|
||||||
image = User.avatar_url(user) |> MediaProxy.url()
|
image = User.avatar_url(user) |> MediaProxy.url()
|
||||||
|
|
|
@ -32,7 +32,7 @@ def render("participation.json", %{participation: participation, for: user}) do
|
||||||
|
|
||||||
%{
|
%{
|
||||||
id: participation.id |> to_string(),
|
id: participation.id |> to_string(),
|
||||||
accounts: render(AccountView, "accounts.json", users: users, as: :user),
|
accounts: render(AccountView, "index.json", users: users, as: :user),
|
||||||
unread: !participation.read,
|
unread: !participation.read,
|
||||||
last_status: render(StatusView, "show.json", activity: activity, for: user)
|
last_status: render(StatusView, "show.json", activity: activity, for: user)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
||||||
|
use Pleroma.Web, :view
|
||||||
|
|
||||||
|
@mastodon_api_level "2.7.2"
|
||||||
|
|
||||||
|
def render("show.json", _) do
|
||||||
|
instance = Pleroma.Config.get(:instance)
|
||||||
|
|
||||||
|
%{
|
||||||
|
uri: Pleroma.Web.base_url(),
|
||||||
|
title: Keyword.get(instance, :name),
|
||||||
|
description: Keyword.get(instance, :description),
|
||||||
|
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
|
||||||
|
email: Keyword.get(instance, :email),
|
||||||
|
urls: %{
|
||||||
|
streaming_api: Pleroma.Web.Endpoint.websocket_url()
|
||||||
|
},
|
||||||
|
stats: Pleroma.Stats.get_stats(),
|
||||||
|
thumbnail: Pleroma.Web.base_url() <> "/instance/thumbnail.jpeg",
|
||||||
|
languages: ["en"],
|
||||||
|
registrations: Keyword.get(instance, :registrations_open),
|
||||||
|
# Extra (not present in Mastodon):
|
||||||
|
max_toot_chars: Keyword.get(instance, :limit),
|
||||||
|
poll_limits: Keyword.get(instance, :poll_limits),
|
||||||
|
upload_limit: Keyword.get(instance, :upload_limit),
|
||||||
|
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
|
||||||
|
background_upload_limit: Keyword.get(instance, :background_upload_limit),
|
||||||
|
banner_upload_limit: Keyword.get(instance, :banner_upload_limit)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -29,7 +29,7 @@ def render("show.json", %{
|
||||||
id: to_string(notification.id),
|
id: to_string(notification.id),
|
||||||
type: mastodon_type,
|
type: mastodon_type,
|
||||||
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
|
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
|
||||||
account: AccountView.render("account.json", %{user: actor, for: user}),
|
account: AccountView.render("show.json", %{user: actor, for: user}),
|
||||||
pleroma: %{
|
pleroma: %{
|
||||||
is_seen: notification.seen
|
is_seen: notification.seen
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.PollView do
|
||||||
|
use Pleroma.Web, :view
|
||||||
|
|
||||||
|
alias Pleroma.HTML
|
||||||
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
|
|
||||||
|
def render("show.json", %{object: object, multiple: multiple, options: options} = params) do
|
||||||
|
{end_time, expired} = end_time_and_expired(object)
|
||||||
|
{options, votes_count} = options_and_votes_count(options)
|
||||||
|
|
||||||
|
%{
|
||||||
|
# Mastodon uses separate ids for polls, but an object can't have
|
||||||
|
# more than one poll embedded so object id is fine
|
||||||
|
id: to_string(object.id),
|
||||||
|
expires_at: end_time,
|
||||||
|
expired: expired,
|
||||||
|
multiple: multiple,
|
||||||
|
votes_count: votes_count,
|
||||||
|
options: options,
|
||||||
|
voted: voted?(params),
|
||||||
|
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render("show.json", %{object: object} = params) do
|
||||||
|
case object.data do
|
||||||
|
%{"anyOf" => options} when is_list(options) ->
|
||||||
|
render(__MODULE__, "show.json", Map.merge(params, %{multiple: true, options: options}))
|
||||||
|
|
||||||
|
%{"oneOf" => options} when is_list(options) ->
|
||||||
|
render(__MODULE__, "show.json", Map.merge(params, %{multiple: false, options: options}))
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp end_time_and_expired(object) do
|
||||||
|
case object.data["closed"] || object.data["endTime"] do
|
||||||
|
end_time when is_binary(end_time) ->
|
||||||
|
end_time = NaiveDateTime.from_iso8601!(end_time)
|
||||||
|
expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt
|
||||||
|
|
||||||
|
{Utils.to_masto_date(end_time), expired}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{nil, false}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp options_and_votes_count(options) do
|
||||||
|
Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
|
||||||
|
current_count = option["replies"]["totalItems"] || 0
|
||||||
|
|
||||||
|
{%{
|
||||||
|
title: HTML.strip_tags(name),
|
||||||
|
votes_count: current_count
|
||||||
|
}, current_count + count}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp voted?(%{object: object} = opts) do
|
||||||
|
if opts[:for] do
|
||||||
|
existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
|
||||||
|
existing_votes != [] or opts[:for].ap_id == object.data["actor"]
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,6 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.CommonAPI.Utils
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
alias Pleroma.Web.MastodonAPI.AccountView
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
|
alias Pleroma.Web.MastodonAPI.PollView
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
alias Pleroma.Web.MediaProxy
|
alias Pleroma.Web.MediaProxy
|
||||||
|
|
||||||
|
@ -108,7 +109,7 @@ def render(
|
||||||
id: to_string(activity.id),
|
id: to_string(activity.id),
|
||||||
uri: activity_object.data["id"],
|
uri: activity_object.data["id"],
|
||||||
url: activity_object.data["id"],
|
url: activity_object.data["id"],
|
||||||
account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
|
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
|
||||||
in_reply_to_id: nil,
|
in_reply_to_id: nil,
|
||||||
in_reply_to_account_id: nil,
|
in_reply_to_account_id: nil,
|
||||||
reblog: reblogged,
|
reblog: reblogged,
|
||||||
|
@ -124,7 +125,7 @@ def render(
|
||||||
pinned: pinned?(activity, user),
|
pinned: pinned?(activity, user),
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
spoiler_text: "",
|
spoiler_text: "",
|
||||||
visibility: "public",
|
visibility: get_visibility(activity),
|
||||||
media_attachments: reblogged[:media_attachments] || [],
|
media_attachments: reblogged[:media_attachments] || [],
|
||||||
mentions: mentions,
|
mentions: mentions,
|
||||||
tags: reblogged[:tags] || [],
|
tags: reblogged[:tags] || [],
|
||||||
|
@ -258,7 +259,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
id: to_string(activity.id),
|
id: to_string(activity.id),
|
||||||
uri: object.data["id"],
|
uri: object.data["id"],
|
||||||
url: url,
|
url: url,
|
||||||
account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
|
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
|
||||||
in_reply_to_id: reply_to && to_string(reply_to.id),
|
in_reply_to_id: reply_to && to_string(reply_to.id),
|
||||||
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
|
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
|
||||||
reblog: nil,
|
reblog: nil,
|
||||||
|
@ -277,7 +278,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
spoiler_text: summary_html,
|
spoiler_text: summary_html,
|
||||||
visibility: get_visibility(object),
|
visibility: get_visibility(object),
|
||||||
media_attachments: attachments,
|
media_attachments: attachments,
|
||||||
poll: render("poll.json", %{object: object, for: opts[:for]}),
|
poll: render(PollView, "show.json", object: object, for: opts[:for]),
|
||||||
mentions: mentions,
|
mentions: mentions,
|
||||||
tags: build_tags(tags),
|
tags: build_tags(tags),
|
||||||
application: %{
|
application: %{
|
||||||
|
@ -376,7 +377,7 @@ def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = a
|
||||||
|
|
||||||
%{
|
%{
|
||||||
id: activity.id,
|
id: activity.id,
|
||||||
account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
|
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
|
||||||
created_at: created_at,
|
created_at: created_at,
|
||||||
title: object.data["title"] |> HTML.strip_tags(),
|
title: object.data["title"] |> HTML.strip_tags(),
|
||||||
artist: object.data["artist"] |> HTML.strip_tags(),
|
artist: object.data["artist"] |> HTML.strip_tags(),
|
||||||
|
@ -389,75 +390,6 @@ def render("listens.json", opts) do
|
||||||
safe_render_many(opts.activities, StatusView, "listen.json", opts)
|
safe_render_many(opts.activities, StatusView, "listen.json", opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("poll.json", %{object: object} = opts) do
|
|
||||||
{multiple, options} =
|
|
||||||
case object.data do
|
|
||||||
%{"anyOf" => options} when is_list(options) -> {true, options}
|
|
||||||
%{"oneOf" => options} when is_list(options) -> {false, options}
|
|
||||||
_ -> {nil, nil}
|
|
||||||
end
|
|
||||||
|
|
||||||
if options do
|
|
||||||
{end_time, expired} =
|
|
||||||
case object.data["closed"] || object.data["endTime"] do
|
|
||||||
end_time when is_binary(end_time) ->
|
|
||||||
end_time =
|
|
||||||
(object.data["closed"] || object.data["endTime"])
|
|
||||||
|> NaiveDateTime.from_iso8601!()
|
|
||||||
|
|
||||||
expired =
|
|
||||||
end_time
|
|
||||||
|> NaiveDateTime.compare(NaiveDateTime.utc_now())
|
|
||||||
|> case do
|
|
||||||
:lt -> true
|
|
||||||
_ -> false
|
|
||||||
end
|
|
||||||
|
|
||||||
end_time = Utils.to_masto_date(end_time)
|
|
||||||
|
|
||||||
{end_time, expired}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{nil, false}
|
|
||||||
end
|
|
||||||
|
|
||||||
voted =
|
|
||||||
if opts[:for] do
|
|
||||||
existing_votes =
|
|
||||||
Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
|
|
||||||
|
|
||||||
existing_votes != [] or opts[:for].ap_id == object.data["actor"]
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
{options, votes_count} =
|
|
||||||
Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
|
|
||||||
current_count = option["replies"]["totalItems"] || 0
|
|
||||||
|
|
||||||
{%{
|
|
||||||
title: HTML.strip_tags(name),
|
|
||||||
votes_count: current_count
|
|
||||||
}, current_count + count}
|
|
||||||
end)
|
|
||||||
|
|
||||||
%{
|
|
||||||
# Mastodon uses separate ids for polls, but an object can't have
|
|
||||||
# more than one poll embedded so object id is fine
|
|
||||||
id: to_string(object.id),
|
|
||||||
expires_at: end_time,
|
|
||||||
expired: expired,
|
|
||||||
multiple: multiple,
|
|
||||||
votes_count: votes_count,
|
|
||||||
options: options,
|
|
||||||
voted: voted,
|
|
||||||
emojis: build_emojis(object.data["emoji"])
|
|
||||||
}
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def render("context.json", %{activity: activity, activities: activities, user: user}) do
|
def render("context.json", %{activity: activity, activities: activities, user: user}) do
|
||||||
%{ancestors: ancestors, descendants: descendants} =
|
%{ancestors: ancestors, descendants: descendants} =
|
||||||
activities
|
activities
|
||||||
|
|
|
@ -212,13 +212,31 @@ def token_exchange(
|
||||||
{:auth_active, false} ->
|
{:auth_active, false} ->
|
||||||
# Per https://github.com/tootsuite/mastodon/blob/
|
# Per https://github.com/tootsuite/mastodon/blob/
|
||||||
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
|
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
|
||||||
render_error(conn, :forbidden, "Your login is missing a confirmed e-mail address")
|
render_error(
|
||||||
|
conn,
|
||||||
|
:forbidden,
|
||||||
|
"Your login is missing a confirmed e-mail address",
|
||||||
|
%{},
|
||||||
|
"missing_confirmed_email"
|
||||||
|
)
|
||||||
|
|
||||||
{:user_active, false} ->
|
{:user_active, false} ->
|
||||||
render_error(conn, :forbidden, "Your account is currently disabled")
|
render_error(
|
||||||
|
conn,
|
||||||
|
:forbidden,
|
||||||
|
"Your account is currently disabled",
|
||||||
|
%{},
|
||||||
|
"account_is_disabled"
|
||||||
|
)
|
||||||
|
|
||||||
{:password_reset_pending, true} ->
|
{:password_reset_pending, true} ->
|
||||||
render_error(conn, :forbidden, "Password reset is required")
|
render_error(
|
||||||
|
conn,
|
||||||
|
:forbidden,
|
||||||
|
"Password reset is required",
|
||||||
|
%{},
|
||||||
|
"password_reset_required"
|
||||||
|
)
|
||||||
|
|
||||||
_error ->
|
_error ->
|
||||||
render_invalid_credentials_error(conn)
|
render_invalid_credentials_error(conn)
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.PleromaAPI.AccountController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
import Pleroma.Web.ControllerHelper,
|
||||||
|
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
|
||||||
|
|
||||||
|
alias Ecto.Changeset
|
||||||
|
alias Pleroma.Plugs.RateLimiter
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend)
|
||||||
|
plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
|
||||||
|
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
|
||||||
|
|
||||||
|
@doc "POST /api/v1/pleroma/accounts/confirmation_resend"
|
||||||
|
def confirmation_resend(conn, params) do
|
||||||
|
nickname_or_email = params["email"] || params["nickname"]
|
||||||
|
|
||||||
|
with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
|
||||||
|
{:ok, _} <- User.try_send_confirmation_email(user) do
|
||||||
|
json_response(conn, :no_content, "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "PATCH /api/v1/pleroma/accounts/update_avatar"
|
||||||
|
def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
|
||||||
|
{:ok, user} =
|
||||||
|
user
|
||||||
|
|> Changeset.change(%{avatar: nil})
|
||||||
|
|> User.update_and_set_cache()
|
||||||
|
|
||||||
|
CommonAPI.update(user)
|
||||||
|
|
||||||
|
json(conn, %{url: nil})
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_avatar(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
{:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
|
||||||
|
{:ok, user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
|
||||||
|
%{"url" => [%{"href" => href} | _]} = data
|
||||||
|
|
||||||
|
CommonAPI.update(user)
|
||||||
|
|
||||||
|
json(conn, %{url: href})
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "PATCH /api/v1/pleroma/accounts/update_banner"
|
||||||
|
def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
|
||||||
|
new_info = %{"banner" => %{}}
|
||||||
|
|
||||||
|
with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
|
||||||
|
CommonAPI.update(user)
|
||||||
|
json(conn, %{url: nil})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_banner(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
|
||||||
|
new_info <- %{"banner" => object.data},
|
||||||
|
{:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
|
||||||
|
CommonAPI.update(user)
|
||||||
|
%{"url" => [%{"href" => href} | _]} = object.data
|
||||||
|
|
||||||
|
json(conn, %{url: href})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "PATCH /api/v1/pleroma/accounts/update_background"
|
||||||
|
def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
|
||||||
|
new_info = %{"background" => %{}}
|
||||||
|
|
||||||
|
with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
|
||||||
|
json(conn, %{url: nil})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_background(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
with {:ok, object} <- ActivityPub.upload(params, type: :background),
|
||||||
|
new_info <- %{"background" => object.data},
|
||||||
|
{:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
|
||||||
|
%{"url" => [%{"href" => href} | _]} = object.data
|
||||||
|
|
||||||
|
json(conn, %{url: href})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/pleroma/accounts/:id/favourites"
|
||||||
|
def favourites(%{assigns: %{account: %{info: %{hide_favorites: true}}}} = conn, _params) do
|
||||||
|
render_error(conn, :forbidden, "Can't get favorites")
|
||||||
|
end
|
||||||
|
|
||||||
|
def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
|
||||||
|
params =
|
||||||
|
params
|
||||||
|
|> Map.put("type", "Create")
|
||||||
|
|> Map.put("favorited_by", user.ap_id)
|
||||||
|
|> Map.put("blocking_user", for_user)
|
||||||
|
|
||||||
|
recipients =
|
||||||
|
if for_user do
|
||||||
|
[Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
|
||||||
|
else
|
||||||
|
[Pleroma.Constants.as_public()]
|
||||||
|
end
|
||||||
|
|
||||||
|
activities =
|
||||||
|
recipients
|
||||||
|
|> ActivityPub.fetch_activities(params)
|
||||||
|
|> Enum.reverse()
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> add_link_headers(activities)
|
||||||
|
|> put_view(StatusView)
|
||||||
|
|> render("index.json", activities: activities, for: for_user, as: :activity)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/pleroma/accounts/:id/subscribe"
|
||||||
|
def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
|
||||||
|
with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do
|
||||||
|
render(conn, "relationship.json", user: user, target: subscription_target)
|
||||||
|
else
|
||||||
|
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/pleroma/accounts/:id/unsubscribe"
|
||||||
|
def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
|
||||||
|
with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do
|
||||||
|
render(conn, "relationship.json", user: user, target: subscription_target)
|
||||||
|
else
|
||||||
|
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.PleromaAPI.MascotController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
|
||||||
|
@doc "GET /api/v1/pleroma/mascot"
|
||||||
|
def show(%{assigns: %{user: user}} = conn, _params) do
|
||||||
|
json(conn, User.get_mascot(user))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "PUT /api/v1/pleroma/mascot"
|
||||||
|
def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do
|
||||||
|
with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
|
||||||
|
# Reject if not an image
|
||||||
|
%{type: "image"} = attachment <- render_attachment(object) do
|
||||||
|
# Sure!
|
||||||
|
# Save to the user's info
|
||||||
|
{:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, attachment))
|
||||||
|
|
||||||
|
json(conn, attachment)
|
||||||
|
else
|
||||||
|
%{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_attachment(object) do
|
||||||
|
attachment_data = Map.put(object.data, "id", object.id)
|
||||||
|
Pleroma.Web.MastodonAPI.StatusView.render("attachment.json", %{attachment: attachment_data})
|
||||||
|
end
|
||||||
|
end
|
|
@ -296,22 +296,44 @@ defmodule Pleroma.Web.Router do
|
||||||
pipe_through(:authenticated_api)
|
pipe_through(:authenticated_api)
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
|
pipe_through(:authenticated_api)
|
||||||
pipe_through(:oauth_read)
|
pipe_through(:oauth_read)
|
||||||
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
|
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
|
||||||
get("/conversations/:id", PleromaAPIController, :conversation)
|
get("/conversations/:id", PleromaAPIController, :conversation)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
|
pipe_through(:authenticated_api)
|
||||||
pipe_through(:oauth_write)
|
pipe_through(:oauth_write)
|
||||||
patch("/conversations/:id", PleromaAPIController, :update_conversation)
|
patch("/conversations/:id", PleromaAPIController, :update_conversation)
|
||||||
post("/statuses/:id/react_with_emoji", PleromaAPIController, :react_with_emoji)
|
post("/statuses/:id/react_with_emoji", PleromaAPIController, :react_with_emoji)
|
||||||
post("/notifications/read", PleromaAPIController, :read_notification)
|
post("/notifications/read", PleromaAPIController, :read_notification)
|
||||||
|
|
||||||
|
patch("/accounts/update_avatar", AccountController, :update_avatar)
|
||||||
|
patch("/accounts/update_banner", AccountController, :update_banner)
|
||||||
|
patch("/accounts/update_background", AccountController, :update_background)
|
||||||
|
|
||||||
|
get("/mascot", MascotController, :show)
|
||||||
|
put("/mascot", MascotController, :update)
|
||||||
|
|
||||||
|
post("/scrobble", ScrobbleController, :new_scrobble)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
pipe_through(:oauth_write)
|
pipe_through(:api)
|
||||||
post("/scrobble", ScrobbleController, :new_scrobble)
|
pipe_through(:oauth_read_or_public)
|
||||||
|
get("/accounts/:id/favourites", AccountController, :favourites)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope [] do
|
||||||
|
pipe_through(:authenticated_api)
|
||||||
|
pipe_through(:oauth_follow)
|
||||||
|
|
||||||
|
post("/accounts/:id/subscribe", AccountController, :subscribe)
|
||||||
|
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
|
||||||
|
end
|
||||||
|
|
||||||
|
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
|
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
|
||||||
|
@ -326,11 +348,11 @@ defmodule Pleroma.Web.Router do
|
||||||
scope [] do
|
scope [] do
|
||||||
pipe_through(:oauth_read)
|
pipe_through(:oauth_read)
|
||||||
|
|
||||||
get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
|
get("/accounts/verify_credentials", AccountController, :verify_credentials)
|
||||||
|
|
||||||
get("/accounts/relationships", MastodonAPIController, :relationships)
|
get("/accounts/relationships", AccountController, :relationships)
|
||||||
|
|
||||||
get("/accounts/:id/lists", MastodonAPIController, :account_lists)
|
get("/accounts/:id/lists", AccountController, :lists)
|
||||||
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
|
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
|
||||||
|
|
||||||
get("/follow_requests", FollowRequestController, :index)
|
get("/follow_requests", FollowRequestController, :index)
|
||||||
|
@ -360,7 +382,7 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
get("/filters", FilterController, :index)
|
get("/filters", FilterController, :index)
|
||||||
|
|
||||||
get("/suggestions", MastodonAPIController, :suggestions)
|
get("/suggestions", SuggestionController, :index)
|
||||||
|
|
||||||
get("/conversations", ConversationController, :index)
|
get("/conversations", ConversationController, :index)
|
||||||
post("/conversations/:id/read", ConversationController, :read)
|
post("/conversations/:id/read", ConversationController, :read)
|
||||||
|
@ -371,7 +393,7 @@ defmodule Pleroma.Web.Router do
|
||||||
scope [] do
|
scope [] do
|
||||||
pipe_through(:oauth_write)
|
pipe_through(:oauth_write)
|
||||||
|
|
||||||
patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
|
patch("/accounts/update_credentials", AccountController, :update_credentials)
|
||||||
|
|
||||||
post("/statuses", StatusController, :create)
|
post("/statuses", StatusController, :create)
|
||||||
delete("/statuses/:id", StatusController, :delete)
|
delete("/statuses/:id", StatusController, :delete)
|
||||||
|
@ -390,10 +412,10 @@ defmodule Pleroma.Web.Router do
|
||||||
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
|
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
|
||||||
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
|
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
|
||||||
|
|
||||||
post("/polls/:id/votes", MastodonAPIController, :poll_vote)
|
post("/polls/:id/votes", PollController, :vote)
|
||||||
|
|
||||||
post("/media", MastodonAPIController, :upload)
|
post("/media", MediaController, :create)
|
||||||
put("/media/:id", MastodonAPIController, :update_media)
|
put("/media/:id", MediaController, :update)
|
||||||
|
|
||||||
delete("/lists/:id", ListController, :delete)
|
delete("/lists/:id", ListController, :delete)
|
||||||
post("/lists", ListController, :create)
|
post("/lists", ListController, :create)
|
||||||
|
@ -407,36 +429,25 @@ defmodule Pleroma.Web.Router do
|
||||||
put("/filters/:id", FilterController, :update)
|
put("/filters/:id", FilterController, :update)
|
||||||
delete("/filters/:id", FilterController, :delete)
|
delete("/filters/:id", FilterController, :delete)
|
||||||
|
|
||||||
patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar)
|
|
||||||
patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner)
|
|
||||||
patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background)
|
|
||||||
|
|
||||||
get("/pleroma/mascot", MastodonAPIController, :get_mascot)
|
|
||||||
put("/pleroma/mascot", MastodonAPIController, :set_mascot)
|
|
||||||
|
|
||||||
post("/reports", ReportController, :create)
|
post("/reports", ReportController, :create)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
pipe_through(:oauth_follow)
|
pipe_through(:oauth_follow)
|
||||||
|
|
||||||
post("/follows", MastodonAPIController, :follow)
|
post("/follows", MastodonAPIController, :follows)
|
||||||
post("/accounts/:id/follow", MastodonAPIController, :follow)
|
post("/accounts/:id/follow", AccountController, :follow)
|
||||||
|
post("/accounts/:id/unfollow", AccountController, :unfollow)
|
||||||
post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
|
post("/accounts/:id/block", AccountController, :block)
|
||||||
post("/accounts/:id/block", MastodonAPIController, :block)
|
post("/accounts/:id/unblock", AccountController, :unblock)
|
||||||
post("/accounts/:id/unblock", MastodonAPIController, :unblock)
|
post("/accounts/:id/mute", AccountController, :mute)
|
||||||
post("/accounts/:id/mute", MastodonAPIController, :mute)
|
post("/accounts/:id/unmute", AccountController, :unmute)
|
||||||
post("/accounts/:id/unmute", MastodonAPIController, :unmute)
|
|
||||||
|
|
||||||
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
|
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
|
||||||
post("/follow_requests/:id/reject", FollowRequestController, :reject)
|
post("/follow_requests/:id/reject", FollowRequestController, :reject)
|
||||||
|
|
||||||
post("/domain_blocks", DomainBlockController, :create)
|
post("/domain_blocks", DomainBlockController, :create)
|
||||||
delete("/domain_blocks", DomainBlockController, :delete)
|
delete("/domain_blocks", DomainBlockController, :delete)
|
||||||
|
|
||||||
post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
|
|
||||||
post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
|
@ -458,16 +469,17 @@ defmodule Pleroma.Web.Router do
|
||||||
scope "/api/v1", Pleroma.Web.MastodonAPI do
|
scope "/api/v1", Pleroma.Web.MastodonAPI do
|
||||||
pipe_through(:api)
|
pipe_through(:api)
|
||||||
|
|
||||||
post("/accounts", MastodonAPIController, :account_register)
|
post("/accounts", AccountController, :create)
|
||||||
|
|
||||||
|
get("/instance", InstanceController, :show)
|
||||||
|
get("/instance/peers", InstanceController, :peers)
|
||||||
|
|
||||||
|
post("/apps", AppController, :create)
|
||||||
|
get("/apps/verify_credentials", AppController, :verify_credentials)
|
||||||
|
|
||||||
get("/instance", MastodonAPIController, :masto_instance)
|
|
||||||
get("/instance/peers", MastodonAPIController, :peers)
|
|
||||||
post("/apps", MastodonAPIController, :create_app)
|
|
||||||
get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials)
|
|
||||||
get("/custom_emojis", MastodonAPIController, :custom_emojis)
|
get("/custom_emojis", MastodonAPIController, :custom_emojis)
|
||||||
|
|
||||||
get("/statuses/:id/card", StatusController, :card)
|
get("/statuses/:id/card", StatusController, :card)
|
||||||
|
|
||||||
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
|
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
|
||||||
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
|
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
|
||||||
|
|
||||||
|
@ -475,12 +487,6 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
get("/accounts/search", SearchController, :account_search)
|
get("/accounts/search", SearchController, :account_search)
|
||||||
|
|
||||||
post(
|
|
||||||
"/pleroma/accounts/confirmation_resend",
|
|
||||||
MastodonAPIController,
|
|
||||||
:account_confirmation_resend
|
|
||||||
)
|
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
pipe_through(:oauth_read_or_public)
|
pipe_through(:oauth_read_or_public)
|
||||||
|
|
||||||
|
@ -492,16 +498,14 @@ defmodule Pleroma.Web.Router do
|
||||||
get("/statuses/:id", StatusController, :show)
|
get("/statuses/:id", StatusController, :show)
|
||||||
get("/statuses/:id/context", StatusController, :context)
|
get("/statuses/:id/context", StatusController, :context)
|
||||||
|
|
||||||
get("/polls/:id", MastodonAPIController, :get_poll)
|
get("/polls/:id", PollController, :show)
|
||||||
|
|
||||||
get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
|
get("/accounts/:id/statuses", AccountController, :statuses)
|
||||||
get("/accounts/:id/followers", MastodonAPIController, :followers)
|
get("/accounts/:id/followers", AccountController, :followers)
|
||||||
get("/accounts/:id/following", MastodonAPIController, :following)
|
get("/accounts/:id/following", AccountController, :following)
|
||||||
get("/accounts/:id", MastodonAPIController, :user)
|
get("/accounts/:id", AccountController, :show)
|
||||||
|
|
||||||
get("/search", SearchController, :search)
|
get("/search", SearchController, :search)
|
||||||
|
|
||||||
get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -667,10 +671,10 @@ defmodule Pleroma.Web.Router do
|
||||||
scope "/", Pleroma.Web.MastodonAPI do
|
scope "/", Pleroma.Web.MastodonAPI do
|
||||||
pipe_through(:mastodon_html)
|
pipe_through(:mastodon_html)
|
||||||
|
|
||||||
get("/web/login", MastodonAPIController, :login)
|
get("/web/login", AuthController, :login)
|
||||||
delete("/auth/sign_out", MastodonAPIController, :logout)
|
delete("/auth/sign_out", AuthController, :logout)
|
||||||
|
|
||||||
post("/auth/password", MastodonAPIController, :password_reset)
|
post("/auth/password", AuthController, :password_reset)
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
pipe_through(:oauth_read)
|
pipe_through(:oauth_read)
|
||||||
|
|
|
@ -3,15 +3,27 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Web.TranslationHelpers do
|
defmodule Pleroma.Web.TranslationHelpers do
|
||||||
defmacro render_error(conn, status, msgid, bindings \\ Macro.escape(%{})) do
|
defmacro render_error(
|
||||||
|
conn,
|
||||||
|
status,
|
||||||
|
msgid,
|
||||||
|
bindings \\ Macro.escape(%{}),
|
||||||
|
identifier \\ Macro.escape("")
|
||||||
|
) do
|
||||||
quote do
|
quote do
|
||||||
require Pleroma.Web.Gettext
|
require Pleroma.Web.Gettext
|
||||||
|
|
||||||
|
error_map =
|
||||||
|
%{
|
||||||
|
error: Pleroma.Web.Gettext.dgettext("errors", unquote(msgid), unquote(bindings)),
|
||||||
|
identifier: unquote(identifier)
|
||||||
|
}
|
||||||
|
|> Enum.reject(fn {_k, v} -> v == "" end)
|
||||||
|
|> Map.new()
|
||||||
|
|
||||||
unquote(conn)
|
unquote(conn)
|
||||||
|> Plug.Conn.put_status(unquote(status))
|
|> Plug.Conn.put_status(unquote(status))
|
||||||
|> Phoenix.Controller.json(%{
|
|> Phoenix.Controller.json(error_map)
|
||||||
error: Pleroma.Web.Gettext.dgettext("errors", unquote(msgid), unquote(bindings))
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1 +1 @@
|
||||||
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.f35d8ab1.css rel=stylesheet><link href=chunk-libs.00388c73.css rel=stylesheet><link href=app.40438ff5.css rel=stylesheet></head><body><script src=/pleroma/admin/static/tinymce4.7.5/tinymce.min.js></script><div id=app></div><script type=text/javascript src=static/js/runtime.e85850af.js></script><script type=text/javascript src=static/js/chunk-elementUI.708d6b68.js></script><script type=text/javascript src=static/js/chunk-libs.14514767.js></script><script type=text/javascript src=static/js/app.90c455c5.js></script></body></html>
|
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.a842fb0a.css rel=stylesheet><link href=chunk-libs.57fe98a3.css rel=stylesheet><link href=app.8589ec81.css rel=stylesheet></head><body><script src=/pleroma/admin/static/tinymce4.7.5/tinymce.min.js></script><div id=app></div><script type=text/javascript src=static/js/runtime.46db235c.js></script><script type=text/javascript src=static/js/chunk-elementUI.fa319e7b.js></script><script type=text/javascript src=static/js/chunk-libs.35c18287.js></script><script type=text/javascript src=static/js/app.9c4316f1.js></script></body></html>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"type": "Undo",
|
||||||
|
"signature": {
|
||||||
|
"type": "RsaSignature2017",
|
||||||
|
"signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==",
|
||||||
|
"creator": "http://mastodon.example.org/users/admin#main-key",
|
||||||
|
"created": "2018-05-19T16:36:58Z"
|
||||||
|
},
|
||||||
|
"object": "http://mastodon.example.org/users/admin#likes/2",
|
||||||
|
"nickname": "lain",
|
||||||
|
"id": "http://mastodon.example.org/users/admin#likes/2/undo",
|
||||||
|
"actor": "http://mastodon.example.org/users/admin",
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"ostatus": "http://ostatus.org#",
|
||||||
|
"movedTo": "as:movedTo",
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
||||||
|
"conversation": "ostatus:conversation",
|
||||||
|
"atomUri": "ostatus:atomUri",
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
|
"Emoji": "toot:Emoji"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -113,10 +113,10 @@ test "getting own lists a given user belongs to" do
|
||||||
{:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1)
|
{:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1)
|
||||||
{:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_2)
|
{:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_2)
|
||||||
|
|
||||||
lists_1 = Pleroma.List.get_lists_account_belongs(owner, member_1.id)
|
lists_1 = Pleroma.List.get_lists_account_belongs(owner, member_1)
|
||||||
assert owned_list in lists_1
|
assert owned_list in lists_1
|
||||||
refute not_owned_list in lists_1
|
refute not_owned_list in lists_1
|
||||||
lists_2 = Pleroma.List.get_lists_account_belongs(owner, member_2.id)
|
lists_2 = Pleroma.List.get_lists_account_belongs(owner, member_2)
|
||||||
assert owned_list in lists_2
|
assert owned_list in lists_2
|
||||||
refute not_owned_list in lists_2
|
refute not_owned_list in lists_2
|
||||||
end
|
end
|
||||||
|
|
|
@ -42,6 +42,18 @@ defp user_agent_mock(user_agent, invokes) do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "reverse proxy" do
|
||||||
|
test "do not track successful request", %{conn: conn} do
|
||||||
|
user_agent_mock("hackney/1.15.1", 2)
|
||||||
|
url = "/success"
|
||||||
|
|
||||||
|
conn = ReverseProxy.call(conn, url)
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "user-agent" do
|
describe "user-agent" do
|
||||||
test "don't keep", %{conn: conn} do
|
test "don't keep", %{conn: conn} do
|
||||||
user_agent_mock("hackney/1.15.1", 2)
|
user_agent_mock("hackney/1.15.1", 2)
|
||||||
|
@ -71,9 +83,15 @@ test "length returns error if content-length more than option", %{conn: conn} do
|
||||||
user_agent_mock("hackney/1.15.1", 0)
|
user_agent_mock("hackney/1.15.1", 0)
|
||||||
|
|
||||||
assert capture_log(fn ->
|
assert capture_log(fn ->
|
||||||
ReverseProxy.call(conn, "/user-agent", max_body_length: 4)
|
ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
|
||||||
end) =~
|
end) =~
|
||||||
"[error] Elixir.Pleroma.ReverseProxy: request to \"/user-agent\" failed: :body_too_large"
|
"[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large"
|
||||||
|
|
||||||
|
assert {:ok, true} == Cachex.get(:failed_proxy_url_cache, "/huge-file")
|
||||||
|
|
||||||
|
assert capture_log(fn ->
|
||||||
|
ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
|
||||||
|
end) == ""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp stream_mock(invokes, with_close? \\ false) do
|
defp stream_mock(invokes, with_close? \\ false) do
|
||||||
|
@ -140,28 +158,54 @@ defp error_mock(status) when is_integer(status) do
|
||||||
describe "returns error on" do
|
describe "returns error on" do
|
||||||
test "500", %{conn: conn} do
|
test "500", %{conn: conn} do
|
||||||
error_mock(500)
|
error_mock(500)
|
||||||
|
url = "/status/500"
|
||||||
|
|
||||||
capture_log(fn -> ReverseProxy.call(conn, "/status/500") end) =~
|
capture_log(fn -> ReverseProxy.call(conn, url) end) =~
|
||||||
"[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500"
|
"[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500"
|
||||||
|
|
||||||
|
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
|
||||||
|
|
||||||
|
{:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url)
|
||||||
|
assert ttl <= 60_000
|
||||||
end
|
end
|
||||||
|
|
||||||
test "400", %{conn: conn} do
|
test "400", %{conn: conn} do
|
||||||
error_mock(400)
|
error_mock(400)
|
||||||
|
url = "/status/400"
|
||||||
|
|
||||||
capture_log(fn -> ReverseProxy.call(conn, "/status/400") end) =~
|
capture_log(fn -> ReverseProxy.call(conn, url) end) =~
|
||||||
"[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400"
|
"[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400"
|
||||||
|
|
||||||
|
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
|
||||||
|
assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "403", %{conn: conn} do
|
||||||
|
error_mock(403)
|
||||||
|
url = "/status/403"
|
||||||
|
|
||||||
|
capture_log(fn ->
|
||||||
|
ReverseProxy.call(conn, url, failed_request_ttl: :timer.seconds(120))
|
||||||
|
end) =~
|
||||||
|
"[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403"
|
||||||
|
|
||||||
|
{:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url)
|
||||||
|
assert ttl > 100_000
|
||||||
end
|
end
|
||||||
|
|
||||||
test "204", %{conn: conn} do
|
test "204", %{conn: conn} do
|
||||||
ClientMock
|
url = "/status/204"
|
||||||
|> expect(:request, fn :get, "/status/204", _, _, _ -> {:ok, 204, [], %{}} end)
|
expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
|
||||||
|
|
||||||
capture_log(fn ->
|
capture_log(fn ->
|
||||||
conn = ReverseProxy.call(conn, "/status/204")
|
conn = ReverseProxy.call(conn, url)
|
||||||
assert conn.resp_body == "Request failed: No Content"
|
assert conn.resp_body == "Request failed: No Content"
|
||||||
assert conn.halted
|
assert conn.halted
|
||||||
end) =~
|
end) =~
|
||||||
"[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204"
|
"[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204"
|
||||||
|
|
||||||
|
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
|
||||||
|
assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -887,6 +887,39 @@ test "adds an announce activity to the db" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "announcing a private object" do
|
||||||
|
test "adds an announce activity to the db if the audience is not widened" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
|
||||||
|
object = Object.normalize(note_activity)
|
||||||
|
|
||||||
|
{:ok, announce_activity, object} = ActivityPub.announce(user, object, nil, true, false)
|
||||||
|
|
||||||
|
assert announce_activity.data["to"] == [User.ap_followers(user)]
|
||||||
|
|
||||||
|
assert announce_activity.data["object"] == object.data["id"]
|
||||||
|
assert announce_activity.data["actor"] == user.ap_id
|
||||||
|
assert announce_activity.data["context"] == object.data["context"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not add an announce activity to the db if the audience is widened" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
|
||||||
|
object = Object.normalize(note_activity)
|
||||||
|
|
||||||
|
assert {:error, _} = ActivityPub.announce(user, object, nil, true, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not add an announce activity to the db if the announcer is not the author" do
|
||||||
|
user = insert(:user)
|
||||||
|
announcer = insert(:user)
|
||||||
|
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
|
||||||
|
object = Object.normalize(note_activity)
|
||||||
|
|
||||||
|
assert {:error, _} = ActivityPub.announce(announcer, object, nil, true, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "unannouncing an object" do
|
describe "unannouncing an object" do
|
||||||
test "unannouncing a previously announced object" do
|
test "unannouncing a previously announced object" do
|
||||||
note_activity = insert(:note_activity)
|
note_activity = insert(:note_activity)
|
||||||
|
|
|
@ -396,6 +396,31 @@ test "it works for incoming unlikes with an existing like activity" do
|
||||||
assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
|
assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it works for incoming unlikes with an existing like activity and a compact object" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
|
||||||
|
|
||||||
|
like_data =
|
||||||
|
File.read!("test/fixtures/mastodon-like.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", activity.data["object"])
|
||||||
|
|
||||||
|
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
|
||||||
|
|
||||||
|
data =
|
||||||
|
File.read!("test/fixtures/mastodon-undo-like.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", like_data["id"])
|
||||||
|
|> Map.put("actor", like_data["actor"])
|
||||||
|
|
||||||
|
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||||
|
|
||||||
|
assert data["actor"] == "http://mastodon.example.org/users/admin"
|
||||||
|
assert data["type"] == "Undo"
|
||||||
|
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
|
||||||
|
assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
|
||||||
|
end
|
||||||
|
|
||||||
test "it works for incoming announces" do
|
test "it works for incoming announces" do
|
||||||
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
|
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
|
||||||
|
|
||||||
|
@ -1083,6 +1108,20 @@ test "it accepts Flag activities" do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "prepare outgoing" do
|
describe "prepare outgoing" do
|
||||||
|
test "it inlines private announced objects" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey", "visibility" => "private"})
|
||||||
|
|
||||||
|
{:ok, announce_activity, _} = CommonAPI.repeat(activity.id, user)
|
||||||
|
|
||||||
|
{:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data)
|
||||||
|
object = modified["object"]
|
||||||
|
|
||||||
|
assert modified["object"]["content"] == "hey"
|
||||||
|
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
|
||||||
|
end
|
||||||
|
|
||||||
test "it turns mentions into tags" do
|
test "it turns mentions into tags" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
other_user = insert(:user)
|
other_user = insert(:user)
|
||||||
|
|
|
@ -21,12 +21,12 @@ test "renders a report" do
|
||||||
content: nil,
|
content: nil,
|
||||||
actor:
|
actor:
|
||||||
Map.merge(
|
Map.merge(
|
||||||
AccountView.render("account.json", %{user: user}),
|
AccountView.render("show.json", %{user: user}),
|
||||||
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})
|
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})
|
||||||
),
|
),
|
||||||
account:
|
account:
|
||||||
Map.merge(
|
Map.merge(
|
||||||
AccountView.render("account.json", %{user: other_user}),
|
AccountView.render("show.json", %{user: other_user}),
|
||||||
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
|
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
|
||||||
),
|
),
|
||||||
statuses: [],
|
statuses: [],
|
||||||
|
@ -53,12 +53,12 @@ test "includes reported statuses" do
|
||||||
content: nil,
|
content: nil,
|
||||||
actor:
|
actor:
|
||||||
Map.merge(
|
Map.merge(
|
||||||
AccountView.render("account.json", %{user: user}),
|
AccountView.render("show.json", %{user: user}),
|
||||||
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})
|
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})
|
||||||
),
|
),
|
||||||
account:
|
account:
|
||||||
Map.merge(
|
Map.merge(
|
||||||
AccountView.render("account.json", %{user: other_user}),
|
AccountView.render("show.json", %{user: other_user}),
|
||||||
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
|
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
|
||||||
),
|
),
|
||||||
statuses: [StatusView.render("show.json", %{activity: activity})],
|
statuses: [StatusView.render("show.json", %{activity: activity})],
|
||||||
|
|
|
@ -255,6 +255,18 @@ test "repeating a status" do
|
||||||
{:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user)
|
{:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "repeating a status privately" do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
|
||||||
|
|
||||||
|
{:ok, %Activity{} = announce_activity, _} =
|
||||||
|
CommonAPI.repeat(activity.id, user, %{"visibility" => "private"})
|
||||||
|
|
||||||
|
assert Visibility.is_private?(announce_activity)
|
||||||
|
end
|
||||||
|
|
||||||
test "favoriting a status" do
|
test "favoriting a status" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
other_user = insert(:user)
|
other_user = insert(:user)
|
||||||
|
|
|
@ -0,0 +1,852 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
alias Pleroma.Web.OAuth.Token
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
describe "account fetching" do
|
||||||
|
test "works by id" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> get("/api/v1/accounts/#{user.id}")
|
||||||
|
|
||||||
|
assert %{"id" => id} = json_response(conn, 200)
|
||||||
|
assert id == to_string(user.id)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> get("/api/v1/accounts/-1")
|
||||||
|
|
||||||
|
assert %{"error" => "Can't find user"} = json_response(conn, 404)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "works by nickname" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> get("/api/v1/accounts/#{user.nickname}")
|
||||||
|
|
||||||
|
assert %{"id" => id} = json_response(conn, 200)
|
||||||
|
assert id == user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "works by nickname for remote users" do
|
||||||
|
limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
|
||||||
|
Pleroma.Config.put([:instance, :limit_to_local_content], false)
|
||||||
|
user = insert(:user, nickname: "user@example.com", local: false)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> get("/api/v1/accounts/#{user.nickname}")
|
||||||
|
|
||||||
|
Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
|
||||||
|
assert %{"id" => id} = json_response(conn, 200)
|
||||||
|
assert id == user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "respects limit_to_local_content == :all for remote user nicknames" do
|
||||||
|
limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
|
||||||
|
Pleroma.Config.put([:instance, :limit_to_local_content], :all)
|
||||||
|
|
||||||
|
user = insert(:user, nickname: "user@example.com", local: false)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> get("/api/v1/accounts/#{user.nickname}")
|
||||||
|
|
||||||
|
Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
|
||||||
|
assert json_response(conn, 404)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do
|
||||||
|
limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
|
||||||
|
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
|
||||||
|
|
||||||
|
user = insert(:user, nickname: "user@example.com", local: false)
|
||||||
|
reading_user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> get("/api/v1/accounts/#{user.nickname}")
|
||||||
|
|
||||||
|
assert json_response(conn, 404)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, reading_user)
|
||||||
|
|> get("/api/v1/accounts/#{user.nickname}")
|
||||||
|
|
||||||
|
Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
|
||||||
|
assert %{"id" => id} = json_response(conn, 200)
|
||||||
|
assert id == user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do
|
||||||
|
# Need to set an old-style integer ID to reproduce the problem
|
||||||
|
# (these are no longer assigned to new accounts but were preserved
|
||||||
|
# for existing accounts during the migration to flakeIDs)
|
||||||
|
user_one = insert(:user, %{id: 1212})
|
||||||
|
user_two = insert(:user, %{nickname: "#{user_one.id}garbage"})
|
||||||
|
|
||||||
|
resp_one =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user_one.id}")
|
||||||
|
|
||||||
|
resp_two =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user_two.nickname}")
|
||||||
|
|
||||||
|
resp_three =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user_two.id}")
|
||||||
|
|
||||||
|
acc_one = json_response(resp_one, 200)
|
||||||
|
acc_two = json_response(resp_two, 200)
|
||||||
|
acc_three = json_response(resp_three, 200)
|
||||||
|
refute acc_one == acc_two
|
||||||
|
assert acc_two == acc_three
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "user timelines" do
|
||||||
|
test "gets a users statuses", %{conn: conn} do
|
||||||
|
user_one = insert(:user)
|
||||||
|
user_two = insert(:user)
|
||||||
|
user_three = insert(:user)
|
||||||
|
|
||||||
|
{:ok, user_three} = User.follow(user_three, user_one)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user_one, %{"status" => "HI!!!"})
|
||||||
|
|
||||||
|
{:ok, direct_activity} =
|
||||||
|
CommonAPI.post(user_one, %{
|
||||||
|
"status" => "Hi, @#{user_two.nickname}.",
|
||||||
|
"visibility" => "direct"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, private_activity} =
|
||||||
|
CommonAPI.post(user_one, %{"status" => "private", "visibility" => "private"})
|
||||||
|
|
||||||
|
resp =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user_one.id}/statuses")
|
||||||
|
|
||||||
|
assert [%{"id" => id}] = json_response(resp, 200)
|
||||||
|
assert id == to_string(activity.id)
|
||||||
|
|
||||||
|
resp =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user_two)
|
||||||
|
|> get("/api/v1/accounts/#{user_one.id}/statuses")
|
||||||
|
|
||||||
|
assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
|
||||||
|
assert id_one == to_string(direct_activity.id)
|
||||||
|
assert id_two == to_string(activity.id)
|
||||||
|
|
||||||
|
resp =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user_three)
|
||||||
|
|> get("/api/v1/accounts/#{user_one.id}/statuses")
|
||||||
|
|
||||||
|
assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
|
||||||
|
assert id_one == to_string(private_activity.id)
|
||||||
|
assert id_two == to_string(activity.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "unimplemented pinned statuses feature", %{conn: conn} do
|
||||||
|
note = insert(:note_activity)
|
||||||
|
user = User.get_cached_by_ap_id(note.data["actor"])
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|
||||||
|
|
||||||
|
assert json_response(conn, 200) == []
|
||||||
|
end
|
||||||
|
|
||||||
|
test "gets an users media", %{conn: conn} do
|
||||||
|
note = insert(:note_activity)
|
||||||
|
user = User.get_cached_by_ap_id(note.data["actor"])
|
||||||
|
|
||||||
|
file = %Plug.Upload{
|
||||||
|
content_type: "image/jpg",
|
||||||
|
path: Path.absname("test/fixtures/image.jpg"),
|
||||||
|
filename: "an_image.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id)
|
||||||
|
|
||||||
|
{:ok, image_post} = CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media_id]})
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"})
|
||||||
|
|
||||||
|
assert [%{"id" => id}] = json_response(conn, 200)
|
||||||
|
assert id == to_string(image_post.id)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"})
|
||||||
|
|
||||||
|
assert [%{"id" => id}] = json_response(conn, 200)
|
||||||
|
assert id == to_string(image_post.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "gets a user's statuses without reblogs", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, post} = CommonAPI.post(user, %{"status" => "HI!!!"})
|
||||||
|
{:ok, _, _} = CommonAPI.repeat(post.id, user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"})
|
||||||
|
|
||||||
|
assert [%{"id" => id}] = json_response(conn, 200)
|
||||||
|
assert id == to_string(post.id)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"})
|
||||||
|
|
||||||
|
assert [%{"id" => id}] = json_response(conn, 200)
|
||||||
|
assert id == to_string(post.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filters user's statuses by a hashtag", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, post} = CommonAPI.post(user, %{"status" => "#hashtag"})
|
||||||
|
{:ok, _post} = CommonAPI.post(user, %{"status" => "hashtag"})
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/statuses", %{"tagged" => "hashtag"})
|
||||||
|
|
||||||
|
assert [%{"id" => id}] = json_response(conn, 200)
|
||||||
|
assert id == to_string(post.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "followers" do
|
||||||
|
test "getting followers", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
{:ok, user} = User.follow(user, other_user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{other_user.id}/followers")
|
||||||
|
|
||||||
|
assert [%{"id" => id}] = json_response(conn, 200)
|
||||||
|
assert id == to_string(user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "getting followers, hide_followers", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user, %{info: %{hide_followers: true}})
|
||||||
|
{:ok, _user} = User.follow(user, other_user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{other_user.id}/followers")
|
||||||
|
|
||||||
|
assert [] == json_response(conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "getting followers, hide_followers, same user requesting", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user, %{info: %{hide_followers: true}})
|
||||||
|
{:ok, _user} = User.follow(user, other_user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> get("/api/v1/accounts/#{other_user.id}/followers")
|
||||||
|
|
||||||
|
refute [] == json_response(conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "getting followers, pagination", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
follower1 = insert(:user)
|
||||||
|
follower2 = insert(:user)
|
||||||
|
follower3 = insert(:user)
|
||||||
|
{:ok, _} = User.follow(follower1, user)
|
||||||
|
{:ok, _} = User.follow(follower2, user)
|
||||||
|
{:ok, _} = User.follow(follower3, user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|
||||||
|
res_conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1.id}")
|
||||||
|
|
||||||
|
assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
|
||||||
|
assert id3 == follower3.id
|
||||||
|
assert id2 == follower2.id
|
||||||
|
|
||||||
|
res_conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3.id}")
|
||||||
|
|
||||||
|
assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
|
||||||
|
assert id2 == follower2.id
|
||||||
|
assert id1 == follower1.id
|
||||||
|
|
||||||
|
res_conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3.id}")
|
||||||
|
|
||||||
|
assert [%{"id" => id2}] = json_response(res_conn, 200)
|
||||||
|
assert id2 == follower2.id
|
||||||
|
|
||||||
|
assert [link_header] = get_resp_header(res_conn, "link")
|
||||||
|
assert link_header =~ ~r/min_id=#{follower2.id}/
|
||||||
|
assert link_header =~ ~r/max_id=#{follower2.id}/
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "following" do
|
||||||
|
test "getting following", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
{:ok, user} = User.follow(user, other_user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/following")
|
||||||
|
|
||||||
|
assert [%{"id" => id}] = json_response(conn, 200)
|
||||||
|
assert id == to_string(other_user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "getting following, hide_follows", %{conn: conn} do
|
||||||
|
user = insert(:user, %{info: %{hide_follows: true}})
|
||||||
|
other_user = insert(:user)
|
||||||
|
{:ok, user} = User.follow(user, other_user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/following")
|
||||||
|
|
||||||
|
assert [] == json_response(conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "getting following, hide_follows, same user requesting", %{conn: conn} do
|
||||||
|
user = insert(:user, %{info: %{hide_follows: true}})
|
||||||
|
other_user = insert(:user)
|
||||||
|
{:ok, user} = User.follow(user, other_user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/following")
|
||||||
|
|
||||||
|
refute [] == json_response(conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "getting following, pagination", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
following1 = insert(:user)
|
||||||
|
following2 = insert(:user)
|
||||||
|
following3 = insert(:user)
|
||||||
|
{:ok, _} = User.follow(user, following1)
|
||||||
|
{:ok, _} = User.follow(user, following2)
|
||||||
|
{:ok, _} = User.follow(user, following3)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|
||||||
|
res_conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}")
|
||||||
|
|
||||||
|
assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
|
||||||
|
assert id3 == following3.id
|
||||||
|
assert id2 == following2.id
|
||||||
|
|
||||||
|
res_conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}")
|
||||||
|
|
||||||
|
assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
|
||||||
|
assert id2 == following2.id
|
||||||
|
assert id1 == following1.id
|
||||||
|
|
||||||
|
res_conn =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}")
|
||||||
|
|
||||||
|
assert [%{"id" => id2}] = json_response(res_conn, 200)
|
||||||
|
assert id2 == following2.id
|
||||||
|
|
||||||
|
assert [link_header] = get_resp_header(res_conn, "link")
|
||||||
|
assert link_header =~ ~r/min_id=#{following2.id}/
|
||||||
|
assert link_header =~ ~r/max_id=#{following2.id}/
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "follow/unfollow" do
|
||||||
|
test "following / unfollowing a user", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/accounts/#{other_user.id}/follow")
|
||||||
|
|
||||||
|
assert %{"id" => _id, "following" => true} = json_response(conn, 200)
|
||||||
|
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/accounts/#{other_user.id}/unfollow")
|
||||||
|
|
||||||
|
assert %{"id" => _id, "following" => false} = json_response(conn, 200)
|
||||||
|
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/follows", %{"uri" => other_user.nickname})
|
||||||
|
|
||||||
|
assert %{"id" => id} = json_response(conn, 200)
|
||||||
|
assert id == to_string(other_user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "following without reblogs" do
|
||||||
|
follower = insert(:user)
|
||||||
|
followed = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, follower)
|
||||||
|
|> post("/api/v1/accounts/#{followed.id}/follow?reblogs=false")
|
||||||
|
|
||||||
|
assert %{"showing_reblogs" => false} = json_response(conn, 200)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"})
|
||||||
|
{:ok, reblog, _} = CommonAPI.repeat(activity.id, followed)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, User.get_cached_by_id(follower.id))
|
||||||
|
|> get("/api/v1/timelines/home")
|
||||||
|
|
||||||
|
assert [] == json_response(conn, 200)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, follower)
|
||||||
|
|> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true")
|
||||||
|
|
||||||
|
assert %{"showing_reblogs" => true} = json_response(conn, 200)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, User.get_cached_by_id(follower.id))
|
||||||
|
|> get("/api/v1/timelines/home")
|
||||||
|
|
||||||
|
expected_activity_id = reblog.id
|
||||||
|
assert [%{"id" => ^expected_activity_id}] = json_response(conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "following / unfollowing errors" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, user)
|
||||||
|
|
||||||
|
# self follow
|
||||||
|
conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
|
||||||
|
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
|
||||||
|
|
||||||
|
# self unfollow
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow")
|
||||||
|
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
|
||||||
|
|
||||||
|
# self follow via uri
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
conn_res = post(conn, "/api/v1/follows", %{"uri" => user.nickname})
|
||||||
|
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
|
||||||
|
|
||||||
|
# follow non existing user
|
||||||
|
conn_res = post(conn, "/api/v1/accounts/doesntexist/follow")
|
||||||
|
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
|
||||||
|
|
||||||
|
# follow non existing user via uri
|
||||||
|
conn_res = post(conn, "/api/v1/follows", %{"uri" => "doesntexist"})
|
||||||
|
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
|
||||||
|
|
||||||
|
# unfollow non existing user
|
||||||
|
conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow")
|
||||||
|
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "mute/unmute" do
|
||||||
|
test "with notifications", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/accounts/#{other_user.id}/mute")
|
||||||
|
|
||||||
|
response = json_response(conn, 200)
|
||||||
|
|
||||||
|
assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = response
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/accounts/#{other_user.id}/unmute")
|
||||||
|
|
||||||
|
response = json_response(conn, 200)
|
||||||
|
assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
|
||||||
|
end
|
||||||
|
|
||||||
|
test "without notifications", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"})
|
||||||
|
|
||||||
|
response = json_response(conn, 200)
|
||||||
|
|
||||||
|
assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = response
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/accounts/#{other_user.id}/unmute")
|
||||||
|
|
||||||
|
response = json_response(conn, 200)
|
||||||
|
assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "pinned statuses" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
|
||||||
|
|
||||||
|
[user: user, activity: activity]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do
|
||||||
|
{:ok, _} = CommonAPI.pin(activity.id, user)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
id_str = to_string(activity.id)
|
||||||
|
|
||||||
|
assert [%{"id" => ^id_str, "pinned" => true}] = result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "blocking / unblocking a user", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/accounts/#{other_user.id}/block")
|
||||||
|
|
||||||
|
assert %{"id" => _id, "blocking" => true} = json_response(conn, 200)
|
||||||
|
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/accounts/#{other_user.id}/unblock")
|
||||||
|
|
||||||
|
assert %{"id" => _id, "blocking" => false} = json_response(conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "create account by app" do
|
||||||
|
setup do
|
||||||
|
valid_params = %{
|
||||||
|
username: "lain",
|
||||||
|
email: "lain@example.org",
|
||||||
|
password: "PlzDontHackLain",
|
||||||
|
agreement: true
|
||||||
|
}
|
||||||
|
|
||||||
|
[valid_params: valid_params]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Account registration via Application", %{conn: conn} do
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/apps", %{
|
||||||
|
client_name: "client_name",
|
||||||
|
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
|
||||||
|
scopes: "read, write, follow"
|
||||||
|
})
|
||||||
|
|
||||||
|
%{
|
||||||
|
"client_id" => client_id,
|
||||||
|
"client_secret" => client_secret,
|
||||||
|
"id" => _,
|
||||||
|
"name" => "client_name",
|
||||||
|
"redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
|
||||||
|
"vapid_key" => _,
|
||||||
|
"website" => nil
|
||||||
|
} = json_response(conn, 200)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> post("/oauth/token", %{
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
client_id: client_id,
|
||||||
|
client_secret: client_secret
|
||||||
|
})
|
||||||
|
|
||||||
|
assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
|
||||||
|
json_response(conn, 200)
|
||||||
|
|
||||||
|
assert token
|
||||||
|
token_from_db = Repo.get_by(Token, token: token)
|
||||||
|
assert token_from_db
|
||||||
|
assert refresh
|
||||||
|
assert scope == "read write follow"
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> put_req_header("authorization", "Bearer " <> token)
|
||||||
|
|> post("/api/v1/accounts", %{
|
||||||
|
username: "lain",
|
||||||
|
email: "lain@example.org",
|
||||||
|
password: "PlzDontHackLain",
|
||||||
|
bio: "Test Bio",
|
||||||
|
agreement: true
|
||||||
|
})
|
||||||
|
|
||||||
|
%{
|
||||||
|
"access_token" => token,
|
||||||
|
"created_at" => _created_at,
|
||||||
|
"scope" => _scope,
|
||||||
|
"token_type" => "Bearer"
|
||||||
|
} = json_response(conn, 200)
|
||||||
|
|
||||||
|
token_from_db = Repo.get_by(Token, token: token)
|
||||||
|
assert token_from_db
|
||||||
|
token_from_db = Repo.preload(token_from_db, :user)
|
||||||
|
assert token_from_db.user
|
||||||
|
|
||||||
|
assert token_from_db.user.info.confirmation_pending
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do
|
||||||
|
_user = insert(:user, email: "lain@example.org")
|
||||||
|
app_token = insert(:oauth_token, user: nil)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("authorization", "Bearer " <> app_token.token)
|
||||||
|
|
||||||
|
res = post(conn, "/api/v1/accounts", valid_params)
|
||||||
|
assert json_response(res, 400) == %{"error" => "{\"email\":[\"has already been taken\"]}"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rate limit", %{conn: conn} do
|
||||||
|
app_token = insert(:oauth_token, user: nil)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
put_req_header(conn, "authorization", "Bearer " <> app_token.token)
|
||||||
|
|> Map.put(:remote_ip, {15, 15, 15, 15})
|
||||||
|
|
||||||
|
for i <- 1..5 do
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/accounts", %{
|
||||||
|
username: "#{i}lain",
|
||||||
|
email: "#{i}lain@example.org",
|
||||||
|
password: "PlzDontHackLain",
|
||||||
|
agreement: true
|
||||||
|
})
|
||||||
|
|
||||||
|
%{
|
||||||
|
"access_token" => token,
|
||||||
|
"created_at" => _created_at,
|
||||||
|
"scope" => _scope,
|
||||||
|
"token_type" => "Bearer"
|
||||||
|
} = json_response(conn, 200)
|
||||||
|
|
||||||
|
token_from_db = Repo.get_by(Token, token: token)
|
||||||
|
assert token_from_db
|
||||||
|
token_from_db = Repo.preload(token_from_db, :user)
|
||||||
|
assert token_from_db.user
|
||||||
|
|
||||||
|
assert token_from_db.user.info.confirmation_pending
|
||||||
|
end
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/accounts", %{
|
||||||
|
username: "6lain",
|
||||||
|
email: "6lain@example.org",
|
||||||
|
password: "PlzDontHackLain",
|
||||||
|
agreement: true
|
||||||
|
})
|
||||||
|
|
||||||
|
assert json_response(conn, :too_many_requests) == %{"error" => "Throttled"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns bad_request if missing required params", %{
|
||||||
|
conn: conn,
|
||||||
|
valid_params: valid_params
|
||||||
|
} do
|
||||||
|
app_token = insert(:oauth_token, user: nil)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("authorization", "Bearer " <> app_token.token)
|
||||||
|
|
||||||
|
res = post(conn, "/api/v1/accounts", valid_params)
|
||||||
|
assert json_response(res, 200)
|
||||||
|
|
||||||
|
[{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}]
|
||||||
|
|> Stream.zip(valid_params)
|
||||||
|
|> Enum.each(fn {ip, {attr, _}} ->
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> Map.put(:remote_ip, ip)
|
||||||
|
|> post("/api/v1/accounts", Map.delete(valid_params, attr))
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
assert res == %{"error" => "Missing parameters"}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("authorization", "Bearer " <> "invalid-token")
|
||||||
|
|
||||||
|
res = post(conn, "/api/v1/accounts", valid_params)
|
||||||
|
assert json_response(res, 403) == %{"error" => "Invalid credentials"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/v1/accounts/:id/lists - account_lists" do
|
||||||
|
test "returns lists to which the account belongs", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
assert {:ok, %Pleroma.List{} = list} = Pleroma.List.create("Test List", user)
|
||||||
|
{:ok, %{following: _following}} = Pleroma.List.follow(list, other_user)
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/api/v1/accounts/#{other_user.id}/lists")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
assert res == [%{"id" => to_string(list.id), "title" => "Test List"}]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "verify_credentials" do
|
||||||
|
test "verify_credentials", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/api/v1/accounts/verify_credentials")
|
||||||
|
|
||||||
|
response = json_response(conn, 200)
|
||||||
|
|
||||||
|
assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
|
||||||
|
assert response["pleroma"]["chat_token"]
|
||||||
|
assert id == to_string(user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "verify_credentials default scope unlisted", %{conn: conn} do
|
||||||
|
user = insert(:user, %{info: %User.Info{default_scope: "unlisted"}})
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/api/v1/accounts/verify_credentials")
|
||||||
|
|
||||||
|
assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = json_response(conn, 200)
|
||||||
|
assert id == to_string(user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "locked accounts", %{conn: conn} do
|
||||||
|
user = insert(:user, %{info: %User.Info{default_scope: "private"}})
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/api/v1/accounts/verify_credentials")
|
||||||
|
|
||||||
|
assert %{"id" => id, "source" => %{"privacy" => "private"}} = json_response(conn, 200)
|
||||||
|
assert id == to_string(user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "user relationships" do
|
||||||
|
test "returns the relationships for the current user", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
{:ok, user} = User.follow(user, other_user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/api/v1/accounts/relationships", %{"id" => [other_user.id]})
|
||||||
|
|
||||||
|
assert [relationship] = json_response(conn, 200)
|
||||||
|
|
||||||
|
assert to_string(other_user.id) == relationship["id"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an empty list on a bad request", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/api/v1/accounts/relationships", %{})
|
||||||
|
|
||||||
|
assert [] = json_response(conn, 200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase, async: true
|
||||||
|
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.Web.OAuth.App
|
||||||
|
alias Pleroma.Web.Push
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
test "apps/verify_credentials", %{conn: conn} do
|
||||||
|
token = insert(:oauth_token)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, token.user)
|
||||||
|
|> assign(:token, token)
|
||||||
|
|> get("/api/v1/apps/verify_credentials")
|
||||||
|
|
||||||
|
app = Repo.preload(token, :app).app
|
||||||
|
|
||||||
|
expected = %{
|
||||||
|
"name" => app.client_name,
|
||||||
|
"website" => app.website,
|
||||||
|
"vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert expected == json_response(conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "creates an oauth app", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
app_attrs = build(:oauth_app)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/apps", %{
|
||||||
|
client_name: app_attrs.client_name,
|
||||||
|
redirect_uris: app_attrs.redirect_uris
|
||||||
|
})
|
||||||
|
|
||||||
|
[app] = Repo.all(App)
|
||||||
|
|
||||||
|
expected = %{
|
||||||
|
"name" => app.client_name,
|
||||||
|
"website" => app.website,
|
||||||
|
"client_id" => app.client_id,
|
||||||
|
"client_secret" => app.client_secret,
|
||||||
|
"id" => app.id |> to_string(),
|
||||||
|
"redirect_uri" => app.redirect_uris,
|
||||||
|
"vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert expected == json_response(conn, 200)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,121 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.AuthControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.Tests.ObanHelpers
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
import Swoosh.TestAssertions
|
||||||
|
|
||||||
|
describe "GET /web/login" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
session_opts = [
|
||||||
|
store: :cookie,
|
||||||
|
key: "_test",
|
||||||
|
signing_salt: "cooldude"
|
||||||
|
]
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> Plug.Session.call(Plug.Session.init(session_opts))
|
||||||
|
|> fetch_session()
|
||||||
|
|
||||||
|
test_path = "/web/statuses/test"
|
||||||
|
%{conn: conn, path: test_path}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects to the saved path after log in", %{conn: conn, path: path} do
|
||||||
|
app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
|
||||||
|
auth = insert(:oauth_authorization, app: app)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_session(:return_to, path)
|
||||||
|
|> get("/web/login", %{code: auth.token})
|
||||||
|
|
||||||
|
assert conn.status == 302
|
||||||
|
assert redirected_to(conn) == path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects to the getting-started page when referer is not present", %{conn: conn} do
|
||||||
|
app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
|
||||||
|
auth = insert(:oauth_authorization, app: app)
|
||||||
|
|
||||||
|
conn = get(conn, "/web/login", %{code: auth.token})
|
||||||
|
|
||||||
|
assert conn.status == 302
|
||||||
|
assert redirected_to(conn) == "/web/getting-started"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /auth/password, with valid parameters" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
conn = post(conn, "/auth/password?email=#{user.email}")
|
||||||
|
%{conn: conn, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns 204", %{conn: conn} do
|
||||||
|
assert json_response(conn, :no_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it creates a PasswordResetToken record for user", %{user: user} do
|
||||||
|
token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
|
||||||
|
assert token_record
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it sends an email to user", %{user: user} do
|
||||||
|
ObanHelpers.perform_all()
|
||||||
|
token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
|
||||||
|
|
||||||
|
email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
|
||||||
|
notify_email = Config.get([:instance, :notify_email])
|
||||||
|
instance_name = Config.get([:instance, :name])
|
||||||
|
|
||||||
|
assert_email_sent(
|
||||||
|
from: {instance_name, notify_email},
|
||||||
|
to: {user.name, user.email},
|
||||||
|
html_body: email.html_body
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /auth/password, with invalid parameters" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns 404 when user is not found", %{conn: conn, user: user} do
|
||||||
|
conn = post(conn, "/auth/password?email=nonexisting_#{user.email}")
|
||||||
|
assert conn.status == 404
|
||||||
|
assert conn.resp_body == ""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns 400 when user is not local", %{conn: conn, user: user} do
|
||||||
|
{:ok, user} = Repo.update(Ecto.Changeset.change(user, local: false))
|
||||||
|
conn = post(conn, "/auth/password?email=#{user.email}")
|
||||||
|
assert conn.status == 400
|
||||||
|
assert conn.resp_body == ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "DELETE /auth/sign_out" do
|
||||||
|
test "redirect to root page", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> delete("/auth/sign_out")
|
||||||
|
|
||||||
|
assert conn.status == 302
|
||||||
|
assert redirected_to(conn) == "/"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,7 +3,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
|
defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
|
||||||
use Pleroma.Web.ConnCase, async: true
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
alias Pleroma.User
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
test "get instance information", %{conn: conn} do
|
||||||
|
conn = get(conn, "/api/v1/instance")
|
||||||
|
assert result = json_response(conn, 200)
|
||||||
|
|
||||||
|
email = Pleroma.Config.get([:instance, :email])
|
||||||
|
# Note: not checking for "max_toot_chars" since it's optional
|
||||||
|
assert %{
|
||||||
|
"uri" => _,
|
||||||
|
"title" => _,
|
||||||
|
"description" => _,
|
||||||
|
"version" => _,
|
||||||
|
"email" => from_config_email,
|
||||||
|
"urls" => %{
|
||||||
|
"streaming_api" => _
|
||||||
|
},
|
||||||
|
"stats" => _,
|
||||||
|
"thumbnail" => _,
|
||||||
|
"languages" => _,
|
||||||
|
"registrations" => _,
|
||||||
|
"poll_limits" => _,
|
||||||
|
"upload_limit" => _,
|
||||||
|
"avatar_upload_limit" => _,
|
||||||
|
"background_upload_limit" => _,
|
||||||
|
"banner_upload_limit" => _
|
||||||
|
} = result
|
||||||
|
|
||||||
|
assert email == from_config_email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "get instance stats", %{conn: conn} do
|
||||||
|
user = insert(:user, %{local: true})
|
||||||
|
|
||||||
|
user2 = insert(:user, %{local: true})
|
||||||
|
{:ok, _user2} = User.deactivate(user2, !user2.info.deactivated)
|
||||||
|
|
||||||
|
insert(:user, %{local: false, nickname: "u@peer1.com"})
|
||||||
|
insert(:user, %{local: false, nickname: "u@peer2.com"})
|
||||||
|
|
||||||
|
{:ok, _} = Pleroma.Web.CommonAPI.post(user, %{"status" => "cofe"})
|
||||||
|
|
||||||
|
# Stats should count users with missing or nil `info.deactivated` value
|
||||||
|
|
||||||
|
{:ok, _user} =
|
||||||
|
user.id
|
||||||
|
|> User.get_cached_by_id()
|
||||||
|
|> User.update_info(&Ecto.Changeset.change(&1, %{deactivated: nil}))
|
||||||
|
|
||||||
|
Pleroma.Stats.force_update()
|
||||||
|
|
||||||
|
conn = get(conn, "/api/v1/instance")
|
||||||
|
|
||||||
|
assert result = json_response(conn, 200)
|
||||||
|
|
||||||
|
stats = result["stats"]
|
||||||
|
|
||||||
|
assert stats
|
||||||
|
assert stats["user_count"] == 1
|
||||||
|
assert stats["status_count"] == 1
|
||||||
|
assert stats["domain_count"] == 2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "get peers", %{conn: conn} do
|
||||||
|
insert(:user, %{local: false, nickname: "u@peer1.com"})
|
||||||
|
insert(:user, %{local: false, nickname: "u@peer2.com"})
|
||||||
|
|
||||||
|
Pleroma.Stats.force_update()
|
||||||
|
|
||||||
|
conn = get(conn, "/api/v1/instance/peers")
|
||||||
|
|
||||||
|
assert result = json_response(conn, 200)
|
||||||
|
|
||||||
|
assert ["peer1.com", "peer2.com"] == Enum.sort(result)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,92 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
describe "media upload" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, user)
|
||||||
|
|
||||||
|
image = %Plug.Upload{
|
||||||
|
content_type: "image/jpg",
|
||||||
|
path: Path.absname("test/fixtures/image.jpg"),
|
||||||
|
filename: "an_image.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
[conn: conn, image: image]
|
||||||
|
end
|
||||||
|
|
||||||
|
clear_config([:media_proxy])
|
||||||
|
clear_config([Pleroma.Upload])
|
||||||
|
|
||||||
|
test "returns uploaded image", %{conn: conn, image: image} do
|
||||||
|
desc = "Description of the image"
|
||||||
|
|
||||||
|
media =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/media", %{"file" => image, "description" => desc})
|
||||||
|
|> json_response(:ok)
|
||||||
|
|
||||||
|
assert media["type"] == "image"
|
||||||
|
assert media["description"] == desc
|
||||||
|
assert media["id"]
|
||||||
|
|
||||||
|
object = Object.get_by_id(media["id"])
|
||||||
|
assert object.data["actor"] == User.ap_id(conn.assigns[:user])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "PUT /api/v1/media/:id" do
|
||||||
|
setup do
|
||||||
|
actor = insert(:user)
|
||||||
|
|
||||||
|
file = %Plug.Upload{
|
||||||
|
content_type: "image/jpg",
|
||||||
|
path: Path.absname("test/fixtures/image.jpg"),
|
||||||
|
filename: "an_image.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, %Object{} = object} =
|
||||||
|
ActivityPub.upload(
|
||||||
|
file,
|
||||||
|
actor: User.ap_id(actor),
|
||||||
|
description: "test-m"
|
||||||
|
)
|
||||||
|
|
||||||
|
[actor: actor, object: object]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates name of media", %{conn: conn, actor: actor, object: object} do
|
||||||
|
media =
|
||||||
|
conn
|
||||||
|
|> assign(:user, actor)
|
||||||
|
|> put("/api/v1/media/#{object.id}", %{"description" => "test-media"})
|
||||||
|
|> json_response(:ok)
|
||||||
|
|
||||||
|
assert media["description"] == "test-media"
|
||||||
|
assert refresh_record(object).data["name"] == "test-media"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error wheb request is bad", %{conn: conn, actor: actor, object: object} do
|
||||||
|
media =
|
||||||
|
conn
|
||||||
|
|> assign(:user, actor)
|
||||||
|
|> put("/api/v1/media/#{object.id}", %{})
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
assert media == %{"error" => "bad_request"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,184 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
describe "GET /api/v1/polls/:id" do
|
||||||
|
test "returns poll entity for object id", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Pleroma does",
|
||||||
|
"poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/api/v1/polls/#{object.id}")
|
||||||
|
|
||||||
|
response = json_response(conn, 200)
|
||||||
|
id = to_string(object.id)
|
||||||
|
assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not expose polls for private statuses", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Pleroma does",
|
||||||
|
"poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20},
|
||||||
|
"visibility" => "private"
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> get("/api/v1/polls/#{object.id}")
|
||||||
|
|
||||||
|
assert json_response(conn, 404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /api/v1/polls/:id/votes" do
|
||||||
|
test "votes are added to the poll", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "A very delicious sandwich",
|
||||||
|
"poll" => %{
|
||||||
|
"options" => ["Lettuce", "Grilled Bacon", "Tomato"],
|
||||||
|
"expires_in" => 20,
|
||||||
|
"multiple" => true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
|
||||||
|
|
||||||
|
assert json_response(conn, 200)
|
||||||
|
object = Object.get_by_id(object.id)
|
||||||
|
|
||||||
|
assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
|
||||||
|
total_items == 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "author can't vote", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Am I cute?",
|
||||||
|
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
assert conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
|
||||||
|
|> json_response(422) == %{"error" => "Poll's author can't vote"}
|
||||||
|
|
||||||
|
object = Object.get_by_id(object.id)
|
||||||
|
|
||||||
|
refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not allow multiple choices on a single-choice question", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "The glass is",
|
||||||
|
"poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
assert conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
|
||||||
|
|> json_response(422) == %{"error" => "Too many choices"}
|
||||||
|
|
||||||
|
object = Object.get_by_id(object.id)
|
||||||
|
|
||||||
|
refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
|
||||||
|
total_items == 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not allow choice index to be greater than options count", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Am I cute?",
|
||||||
|
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
|
||||||
|
|
||||||
|
assert json_response(conn, 422) == %{"error" => "Invalid indices"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 404 error when object is not exist", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/polls/1/votes", %{"choices" => [0]})
|
||||||
|
|
||||||
|
assert json_response(conn, 404) == %{"error" => "Record not found"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 404 when poll is private and not available for user", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Am I cute?",
|
||||||
|
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20},
|
||||||
|
"visibility" => "private"
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
|
||||||
|
|
||||||
|
assert json_response(conn, 404) == %{"error" => "Record not found"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue