Merge remote-tracking branch 'remotes/upstream/develop' into 1234-mastodon-2-4-3-oauth-scopes

# Conflicts:
#	CHANGELOG.md
#	lib/pleroma/web/mastodon_api/controllers/account_controller.ex
#	lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
#	lib/pleroma/web/router.ex
This commit is contained in:
Ivan Tashkinov 2019-10-06 11:43:49 +03:00
commit 06b3bb54c5
170 changed files with 3361 additions and 2192 deletions

View file

@ -141,6 +141,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()
params =
if Map.has_key?(params, "fields_attributes") do
Map.update!(params, "fields_attributes", fn fields ->
fields
|> normalize_fields_attributes()
|> Enum.filter(fn %{"name" => n} -> n != "" end)
end)
else
params
end
info_params =
[
:no_rich_text,
@ -158,12 +169,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
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 ->
|> add_if_present(params, "fields_attributes", :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, "fields_attributes", :raw_fields)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
{:ok, Map.merge(user.info.pleroma_settings_store, value)}
end)
@ -204,6 +215,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
end
defp normalize_fields_attributes(fields) do
if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end)
else
fields
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))
@ -338,6 +357,28 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
end
@doc "POST /api/v1/follows"
def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
render(conn, "show.json", user: followed, for: follower)
else
{:followed, _} -> {:error, :not_found}
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "GET /api/v1/mutes"
def mutes(%{assigns: %{user: user}} = conn, _) do
render(conn, "index.json", users: User.muted_users(user), for: user, as: :user)
end
@doc "GET /api/v1/blocks"
def blocks(%{assigns: %{user: user}} = conn, _) do
render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user)
end
@doc "GET /api/v1/endorsements"
def endorsements(conn, params),
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)

View file

@ -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

View file

@ -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 ->
masto_fe_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

View file

@ -0,0 +1,11 @@
# 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.CustomEmojiController do
use Pleroma.Web, :controller
def index(conn, _params) do
render(conn, "index.json", custom_emojis: Pleroma.Emoji.get_all())
end
end

View file

@ -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

View file

@ -5,36 +5,9 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.HTTP
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.MastodonView
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
alias Pleroma.Plugs.OAuthScopesPlug
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
# Note: :index action handles attempt of unauthenticated access to private instance with redirect
@ -99,402 +72,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
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
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
@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
Pleroma.Emoji.get_all()
|> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
"shortcode" => shortcode,
"static_url" => url,
"visible_in_picker" => true,
"url" => url,
"tags" => tags,
# Assuming that a comma is authorized in the category name
"category" => (tags -- ["Custom"]) |> Enum.join(",")
}
end)
end
def custom_emojis(conn, _params) do
mastodon_emoji = mastodonized_emoji()
json(conn, mastodon_emoji)
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 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 follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
conn
|> put_view(AccountView)
|> render("show.json", %{user: followed, for: follower})
else
{:followed, _} ->
{:error, :not_found}
{:error, message} ->
conn
|> put_status(:forbidden)
|> json(%{error: message})
end
end
def mutes(%{assigns: %{user: user}} = conn, _) do
with muted_accounts <- User.muted_users(user) do
res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
json(conn, res)
end
end
def blocks(%{assigns: %{user: user}} = conn, _) do
with blocked_accounts <- User.blocked_users(user) do
res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
json(conn, res)
end
end
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_activities([], params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
bookmarks =
Bookmark.for_user_query(user.id)
|> Pagination.fetch_paginated(params)
activities =
bookmarks
|> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
conn
|> add_link_headers(bookmarks)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def index(%{assigns: %{user: user}} = conn, _params) do
token = get_session(conn, :oauth_token)
if user && token do
mastodon_emoji = mastodonized_emoji()
limit = Config.get([:instance, :limit])
accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
initial_state =
%{
meta: %{
streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
access_token: token,
locale: "en",
domain: Pleroma.Web.Endpoint.host(),
admin: "1",
me: "#{user.id}",
unfollow_modal: false,
boost_modal: false,
delete_modal: true,
auto_play_gif: false,
display_sensitive_media: false,
reduce_motion: false,
max_toot_chars: limit,
mascot: User.get_mascot(user)["url"]
},
poll_limits: Config.get([:instance, :poll_limits]),
rights: %{
delete_others_notice: present?(user.info.is_moderator),
admin: present?(user.info.is_admin)
},
compose: %{
me: "#{user.id}",
default_privacy: user.info.default_scope,
default_sensitive: false,
allow_content_types: Config.get([:instance, :allowed_post_formats])
},
media_attachments: %{
accept_content_types: [
".jpg",
".jpeg",
".png",
".gif",
".webm",
".mp4",
".m4v",
"image\/jpeg",
"image\/png",
"image\/gif",
"video\/webm",
"video\/mp4"
]
},
settings:
user.info.settings ||
%{
onboarded: true,
home: %{
shows: %{
reblog: true,
reply: true
}
},
notifications: %{
alerts: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
shows: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
sounds: %{
follow: true,
favourite: true,
reblog: true,
mention: true
}
}
},
push_subscription: nil,
accounts: accounts,
custom_emojis: mastodon_emoji,
char_limit: limit
}
|> Jason.encode!()
conn
|> put_layout(false)
|> put_view(MastodonView)
|> render("index.html", %{initial_state: initial_state})
else
conn
|> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login")
end
end
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
json(conn, %{})
else
e ->
conn
|> put_status(:internal_server_error)
|> json(%{error: inspect(e)})
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
#
def empty_array(conn, _) do
@ -506,83 +83,4 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
Logger.debug("Unimplemented, returning an empty object")
json(conn, %{})
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 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 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?(false), do: false
defp present?(_), do: true
end

View file

@ -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

View file

@ -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

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller
import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3]
import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2]
require Ecto.Query
@ -176,8 +176,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/reblog"
def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
%Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end
@ -293,7 +293,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{: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 =
User
|> Ecto.Query.where([u], u.ap_id in ^announces)
@ -322,4 +334,39 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
render(conn, "context.json", activity: activity, activities: activities, user: user)
end
end
@doc "GET /api/v1/favourites"
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_activities([], params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity)
end
@doc "GET /api/v1/bookmarks"
def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
bookmarks =
user.id
|> Bookmark.for_user_query()
|> Pleroma.Pagination.fetch_paginated(params)
activities =
bookmarks
|> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
conn
|> add_link_headers(bookmarks)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
end

View file

@ -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

View file

@ -167,6 +167,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_chat_token(user, opts[:for], opts)
|> maybe_put_activation_status(user, opts[:for])
|> maybe_put_follow_requests_count(user, opts[:for])
|> maybe_put_unread_conversation_count(user, opts[:for])
end
defp username_from_nickname(string) when is_binary(string) do
@ -248,6 +249,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_activation_status(data, _, _), do: data
defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in(
[:pleroma, :unread_conversation_count],
user.info.unread_conversation_count
)
end
defp maybe_put_unread_conversation_count(data, _, _), do: data
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
end

View file

@ -0,0 +1,28 @@
# 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.CustomEmojiView do
use Pleroma.Web, :view
alias Pleroma.Emoji
alias Pleroma.Web
def render("index.json", %{custom_emojis: custom_emojis}) do
render_many(custom_emojis, __MODULE__, "show.json")
end
def render("show.json", %{custom_emoji: {shortcode, %Emoji{file: relative_url, tags: tags}}}) do
url = Web.base_url() |> URI.merge(relative_url) |> to_string()
%{
"shortcode" => shortcode,
"static_url" => url,
"visible_in_picker" => true,
"url" => url,
"tags" => tags,
# Assuming that a comma is authorized in the category name
"category" => tags |> List.delete("Custom") |> Enum.join(",")
}
end
end

View file

@ -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

View file

@ -1,8 +0,0 @@
# 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.MastodonView do
use Pleroma.Web, :view
import Phoenix.HTML
end

View file

@ -25,40 +25,44 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
mastodon_type = Activity.mastodon_notification_type(activity)
response = %{
id: to_string(notification.id),
type: mastodon_type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
account: AccountView.render("show.json", %{user: actor, for: user}),
pleroma: %{
is_seen: notification.seen
with %{id: _} = account <- AccountView.render("show.json", %{user: actor, for: user}) do
response = %{
id: to_string(notification.id),
type: mastodon_type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
account: account,
pleroma: %{
is_seen: notification.seen
}
}
}
case mastodon_type do
"mention" ->
response
|> Map.merge(%{
status: StatusView.render("show.json", %{activity: activity, for: user})
})
case mastodon_type do
"mention" ->
response
|> Map.merge(%{
status: StatusView.render("show.json", %{activity: activity, for: user})
})
"favourite" ->
response
|> Map.merge(%{
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})
"favourite" ->
response
|> Map.merge(%{
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})
"reblog" ->
response
|> Map.merge(%{
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})
"reblog" ->
response
|> Map.merge(%{
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})
"follow" ->
response
"follow" ->
response
_ ->
nil
_ ->
nil
end
else
_ -> nil
end
end
end

View file

@ -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

View file

@ -18,6 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.PollView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
@ -124,7 +125,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
pinned: pinned?(activity, user),
sensitive: false,
spoiler_text: "",
visibility: "public",
visibility: get_visibility(activity),
media_attachments: reblogged[:media_attachments] || [],
mentions: mentions,
tags: reblogged[:tags] || [],
@ -277,7 +278,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
spoiler_text: summary_html,
visibility: get_visibility(object),
media_attachments: attachments,
poll: render("poll.json", %{object: object, for: opts[:for]}),
poll: render(PollView, "show.json", object: object, for: opts[:for]),
mentions: mentions,
tags: build_tags(tags),
application: %{
@ -389,75 +390,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
safe_render_many(opts.activities, StatusView, "listen.json", opts)
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
%{ancestors: ancestors, descendants: descendants} =
activities