aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma')
-rw-r--r--lib/pleroma/activity.ex15
-rw-r--r--lib/pleroma/application.ex3
-rw-r--r--lib/pleroma/bbs/handler.ex2
-rw-r--r--lib/pleroma/list.ex21
-rw-r--r--lib/pleroma/moderation_log.ex265
-rw-r--r--lib/pleroma/object.ex7
-rw-r--r--lib/pleroma/plugs/remote_ip.ex54
-rw-r--r--lib/pleroma/reverse_proxy/reverse_proxy.ex27
-rw-r--r--lib/pleroma/user.ex20
-rw-r--r--lib/pleroma/user/info.ex4
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex39
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex28
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex72
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex19
-rw-r--r--lib/pleroma/web/activity_pub/views/object_view.ex3
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex5
-rw-r--r--lib/pleroma/web/activity_pub/visibility.ex5
-rw-r--r--lib/pleroma/web/admin_api/admin_api_controller.ex14
-rw-r--r--lib/pleroma/web/admin_api/views/moderation_log_view.ex5
-rw-r--r--lib/pleroma/web/admin_api/views/report_view.ex2
-rw-r--r--lib/pleroma/web/chat_channel.ex2
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex219
-rw-r--r--lib/pleroma/web/common_api/common_api.ex294
-rw-r--r--lib/pleroma/web/common_api/utils.ex138
-rw-r--r--lib/pleroma/web/controller_helper.ex21
-rw-r--r--lib/pleroma/web/endpoint.ex5
-rw-r--r--lib/pleroma/web/masto_fe_controller.ex36
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex304
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/app_controller.ex39
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/auth_controller.ex91
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex32
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex11
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex26
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex72
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex49
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/instance_controller.ex17
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex1573
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/media_controller.ex42
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/poll_controller.ex53
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/report_controller.ex16
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex51
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex286
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex63
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex136
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex26
-rw-r--r--lib/pleroma/web/mastodon_api/views/conversation_view.ex21
-rw-r--r--lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex28
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex35
-rw-r--r--lib/pleroma/web/mastodon_api/views/mastodon_view.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex60
-rw-r--r--lib/pleroma/web/mastodon_api/views/poll_view.ex74
-rw-r--r--lib/pleroma/web/mastodon_api/views/report_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex23
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex127
-rw-r--r--lib/pleroma/web/oauth/app.ex26
-rw-r--r--lib/pleroma/web/oauth/oauth_controller.ex26
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/account_controller.ex143
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex35
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex52
-rw-r--r--lib/pleroma/web/router.ex207
-rw-r--r--lib/pleroma/web/templates/masto_fe/index.html.eex (renamed from lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex)2
-rw-r--r--lib/pleroma/web/translation_helpers.ex20
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex2
-rw-r--r--lib/pleroma/web/views/masto_fe_view.ex102
-rw-r--r--lib/pleroma/web/views/streamer_view.ex4
67 files changed, 3020 insertions, 2195 deletions
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 2c04a26f9..c1065611b 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -137,11 +137,18 @@ defmodule Pleroma.Activity do
|> Repo.one()
end
+ @spec get_by_id(String.t()) :: Activity.t() | nil
def get_by_id(id) do
- Activity
- |> where([a], a.id == ^id)
- |> restrict_deactivated_users()
- |> Repo.one()
+ case FlakeId.flake_id?(id) do
+ true ->
+ Activity
+ |> where([a], a.id == ^id)
+ |> restrict_deactivated_users()
+ |> Repo.one()
+
+ _ ->
+ nil
+ end
end
def get_by_id_with_object(id) do
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 3e21d4403..0bf218bc7 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -103,7 +103,8 @@ defmodule Pleroma.Application do
build_cachex("scrubber", limit: 2500),
build_cachex("idempotency", expiration: idempotency_expiration(), 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
diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex
index 0a381f592..fa838a4e4 100644
--- a/lib/pleroma/bbs/handler.ex
+++ b/lib/pleroma/bbs/handler.ex
@@ -42,7 +42,7 @@ defmodule Pleroma.BBS.Handler do
end
def puts_activity(activity) do
- status = Pleroma.Web.MastodonAPI.StatusView.render("status.json", %{activity: activity})
+ status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity})
IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})")
IO.puts(HtmlSanitizeEx.strip_tags(status.content))
IO.puts("")
diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex
index c5db1cb62..08a94c62c 100644
--- a/lib/pleroma/list.ex
+++ b/lib/pleroma/list.ex
@@ -84,22 +84,11 @@ defmodule Pleroma.List do
end
# Get lists to which the account belongs.
- def get_lists_account_belongs(%User{} = owner, account_id) do
- user = User.get_cached_by_id(account_id)
-
- query =
- from(
- l in Pleroma.List,
- where:
- l.user_id == ^owner.id and
- fragment(
- "? = ANY(?)",
- ^user.follower_address,
- l.following
- )
- )
-
- Repo.all(query)
+ def get_lists_account_belongs(%User{} = owner, user) do
+ Pleroma.List
+ |> where([l], l.user_id == ^owner.id)
+ |> where([l], fragment("? = ANY(?)", ^user.follower_address, l.following))
+ |> Repo.all()
end
def rename(%Pleroma.List{} = list, title) do
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index 1ef6fe67a..352cad433 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -14,61 +14,143 @@ defmodule Pleroma.ModerationLog do
timestamps()
end
- def get_all(page, page_size) do
- from(q in __MODULE__,
- order_by: [desc: q.inserted_at],
+ def get_all(params) do
+ base_query =
+ get_all_query()
+ |> maybe_filter_by_date(params)
+ |> maybe_filter_by_user(params)
+ |> maybe_filter_by_search(params)
+
+ query_with_pagination = base_query |> paginate_query(params)
+
+ %{
+ items: Repo.all(query_with_pagination),
+ count: Repo.aggregate(base_query, :count, :id)
+ }
+ end
+
+ defp maybe_filter_by_date(query, %{start_date: nil, end_date: nil}), do: query
+
+ defp maybe_filter_by_date(query, %{start_date: start_date, end_date: nil}) do
+ from(q in query,
+ where: q.inserted_at >= ^parse_datetime(start_date)
+ )
+ end
+
+ defp maybe_filter_by_date(query, %{start_date: nil, end_date: end_date}) do
+ from(q in query,
+ where: q.inserted_at <= ^parse_datetime(end_date)
+ )
+ end
+
+ defp maybe_filter_by_date(query, %{start_date: start_date, end_date: end_date}) do
+ from(q in query,
+ where: q.inserted_at >= ^parse_datetime(start_date),
+ where: q.inserted_at <= ^parse_datetime(end_date)
+ )
+ end
+
+ defp maybe_filter_by_user(query, %{user_id: nil}), do: query
+
+ defp maybe_filter_by_user(query, %{user_id: user_id}) do
+ from(q in query,
+ where: fragment("(?)->'actor'->>'id' = ?", q.data, ^user_id)
+ )
+ end
+
+ defp maybe_filter_by_search(query, %{search: search}) when is_nil(search) or search == "",
+ do: query
+
+ defp maybe_filter_by_search(query, %{search: search}) do
+ from(q in query,
+ where: fragment("(?)->>'message' ILIKE ?", q.data, ^"%#{search}%")
+ )
+ end
+
+ defp paginate_query(query, %{page: page, page_size: page_size}) do
+ from(q in query,
limit: ^page_size,
offset: ^((page - 1) * page_size)
)
- |> Repo.all()
end
+ defp get_all_query do
+ from(q in __MODULE__,
+ order_by: [desc: q.inserted_at]
+ )
+ end
+
+ defp parse_datetime(datetime) do
+ {:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime)
+
+ parsed_datetime
+ end
+
+ @spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) ::
+ {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
subject: %User{} = subject,
action: action,
permission: permission
}) do
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- subject: user_to_map(subject),
- action: action,
- permission: permission
+ "actor" => user_to_map(actor),
+ "subject" => user_to_map(subject),
+ "action" => action,
+ "permission" => permission,
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
end
+ @spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
+ {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_update",
subject: %Activity{data: %{"type" => "Flag"}} = subject
}) do
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- action: "report_update",
- subject: report_to_map(subject)
+ "actor" => user_to_map(actor),
+ "action" => "report_update",
+ "subject" => report_to_map(subject),
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
end
+ @spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
+ {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_response",
subject: %Activity{} = subject,
text: text
}) do
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- action: "report_response",
- subject: report_to_map(subject),
- text: text
+ "actor" => user_to_map(actor),
+ "action" => "report_response",
+ "subject" => report_to_map(subject),
+ "text" => text,
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
end
+ @spec insert_log(%{
+ actor: User,
+ subject: Activity,
+ action: String.t(),
+ sensitive: String.t(),
+ visibility: String.t()
+ }) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "status_update",
@@ -76,41 +158,49 @@ defmodule Pleroma.ModerationLog do
sensitive: sensitive,
visibility: visibility
}) do
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- action: "status_update",
- subject: status_to_map(subject),
- sensitive: sensitive,
- visibility: visibility
+ "actor" => user_to_map(actor),
+ "action" => "status_update",
+ "subject" => status_to_map(subject),
+ "sensitive" => sensitive,
+ "visibility" => visibility,
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
end
+ @spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
+ {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "status_delete",
subject_id: subject_id
}) do
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- action: "status_delete",
- subject_id: subject_id
+ "actor" => user_to_map(actor),
+ "action" => "status_delete",
+ "subject_id" => subject_id,
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- action: action,
- subject: user_to_map(subject)
+ "actor" => user_to_map(actor),
+ "action" => action,
+ "subject" => user_to_map(subject),
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, subjects: [User], action: String.t()}) ::
@@ -118,97 +208,128 @@ defmodule Pleroma.ModerationLog do
def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do
subjects = Enum.map(subjects, &user_to_map/1)
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- action: action,
- subjects: subjects
+ "actor" => user_to_map(actor),
+ "action" => action,
+ "subjects" => subjects,
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
end
+ @spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
+ {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
followed: %User{} = followed,
follower: %User{} = follower,
action: "follow"
}) do
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- action: "follow",
- followed: user_to_map(followed),
- follower: user_to_map(follower)
+ "actor" => user_to_map(actor),
+ "action" => "follow",
+ "followed" => user_to_map(followed),
+ "follower" => user_to_map(follower),
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
end
+ @spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
+ {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
followed: %User{} = followed,
follower: %User{} = follower,
action: "unfollow"
}) do
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- action: "unfollow",
- followed: user_to_map(followed),
- follower: user_to_map(follower)
+ "actor" => user_to_map(actor),
+ "action" => "unfollow",
+ "followed" => user_to_map(followed),
+ "follower" => user_to_map(follower),
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
end
+ @spec insert_log(%{
+ actor: User,
+ action: String.t(),
+ nicknames: [String.t()],
+ tags: [String.t()]
+ }) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
nicknames: nicknames,
tags: tags,
action: action
}) do
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- nicknames: nicknames,
- tags: tags,
- action: action
+ "actor" => user_to_map(actor),
+ "nicknames" => nicknames,
+ "tags" => tags,
+ "action" => action,
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
end
+ @spec insert_log(%{actor: User, action: String.t(), target: String.t()}) ::
+ {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: action,
target: target
})
when action in ["relay_follow", "relay_unfollow"] do
- Repo.insert(%ModerationLog{
+ %ModerationLog{
data: %{
- actor: user_to_map(actor),
- action: action,
- target: target
+ "actor" => user_to_map(actor),
+ "action" => action,
+ "target" => target,
+ "message" => ""
}
- })
+ }
+ |> insert_log_entry_with_message()
+ end
+
+ @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
+
+ defp insert_log_entry_with_message(entry) do
+ entry.data["message"]
+ |> put_in(get_log_entry_message(entry))
+ |> Repo.insert()
end
defp user_to_map(%User{} = user) do
user
|> Map.from_struct()
|> Map.take([:id, :nickname])
- |> Map.put(:type, "user")
+ |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
+ |> Map.put("type", "user")
end
defp report_to_map(%Activity{} = report) do
%{
- type: "report",
- id: report.id,
- state: report.data["state"]
+ "type" => "report",
+ "id" => report.id,
+ "state" => report.data["state"]
}
end
defp status_to_map(%Activity{} = status) do
%{
- type: "status",
- id: status.id
+ "type" => "status",
+ "id" => status.id
}
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 3fa407931..cdfbacb0e 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -248,4 +248,11 @@ defmodule Pleroma.Object do
_ -> :noop
end
end
+
+ @doc "Updates data field of an object"
+ def update_data(%Object{data: data} = object, attrs \\ %{}) do
+ object
+ |> Object.change(%{data: Map.merge(data || %{}, attrs)})
+ |> Repo.update()
+ end
end
diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex
new file mode 100644
index 000000000..fdedc27ee
--- /dev/null
+++ b/lib/pleroma/plugs/remote_ip.ex
@@ -0,0 +1,54 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.RemoteIp do
+ @moduledoc """
+ This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
+ """
+
+ @behaviour Plug
+
+ @headers ~w[
+ forwarded
+ x-forwarded-for
+ x-client-ip
+ x-real-ip
+ ]
+
+ # https://en.wikipedia.org/wiki/Localhost
+ # https://en.wikipedia.org/wiki/Private_network
+ @reserved ~w[
+ 127.0.0.0/8
+ ::1/128
+ fc00::/7
+ 10.0.0.0/8
+ 172.16.0.0/12
+ 192.168.0.0/16
+ ]
+
+ def init(_), do: nil
+
+ def call(conn, _) do
+ config = Pleroma.Config.get(__MODULE__, [])
+
+ if Keyword.get(config, :enabled, false) do
+ RemoteIp.call(conn, remote_ip_opts(config))
+ else
+ conn
+ end
+ end
+
+ defp remote_ip_opts(config) do
+ headers = config |> Keyword.get(:headers, @headers) |> MapSet.new()
+ reserved = Keyword.get(config, :reserved, @reserved)
+
+ proxies =
+ config
+ |> Keyword.get(:proxies, [])
+ |> Enum.concat(reserved)
+ |> Enum.map(&InetCidr.parse/1)
+
+ {headers, proxies}
+ end
+end
diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex
index 03efad30a..78144cae3 100644
--- a/lib/pleroma/reverse_proxy/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex
@@ -15,6 +15,7 @@ defmodule Pleroma.ReverseProxy do
@valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30)
@max_body_length :infinity
+ @failed_request_ttl :timer.seconds(60)
@methods ~w(GET HEAD)
@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
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`:
* `true` will not alter `content-disposition` (up to the upstream),
* `false` will add `content-disposition: attachment` to any request,
@@ -83,6 +86,7 @@ defmodule Pleroma.ReverseProxy do
{:keep_user_agent, boolean}
| {:max_read_duration, :timer.time() | :infinity}
| {:max_body_length, non_neg_integer() | :infinity}
+ | {:failed_request_ttl, :timer.time() | :infinity}
| {:http, []}
| {:req_headers, [{String.t(), String.t()}]}
| {:resp_headers, [{String.t(), String.t()}]}
@@ -108,7 +112,8 @@ defmodule Pleroma.ReverseProxy do
opts
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 <-
header_length_constraint(
headers,
@@ -116,12 +121,18 @@ defmodule Pleroma.ReverseProxy do
) do
response(conn, client, url, code, headers, opts)
else
+ {:ok, true} ->
+ conn
+ |> error_or_redirect(url, 500, "Request failed", opts)
+ |> halt()
+
{:ok, code, headers} ->
head_response(conn, url, code, headers, opts)
|> halt()
{:error, {:invalid_http_response, code}} ->
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}")
+ track_failed_url(url, code, opts)
conn
|> error_or_redirect(
@@ -134,6 +145,7 @@ defmodule Pleroma.ReverseProxy do
{:error, error} ->
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}")
+ track_failed_url(url, error, opts)
conn
|> error_or_redirect(url, 500, "Request failed", opts)
@@ -388,4 +400,17 @@ defmodule Pleroma.ReverseProxy do
end
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
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index f3dcf7ad4..c2f8fa0d7 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -505,6 +505,11 @@ defmodule Pleroma.User do
|> Repo.all()
end
+ def get_all_by_ids(ids) do
+ from(u in __MODULE__, where: u.id in ^ids)
+ |> Repo.all()
+ end
+
# This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
# of the ap_id and the domain and tries to get that user
def get_by_guessed_nickname(ap_id) do
@@ -578,7 +583,7 @@ defmodule Pleroma.User do
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
- restrict_to_local == false ->
+ restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
get_cached_by_nickname(nickname_or_id)
restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
@@ -765,6 +770,19 @@ defmodule Pleroma.User do
update_info(user, &User.Info.set_note_count(&1, note_count))
end
+ def update_mascot(user, url) do
+ info_changeset =
+ User.Info.mascot_update(
+ user.info,
+ url
+ )
+
+ user
+ |> change()
+ |> put_embed(:info, info_changeset)
+ |> update_and_set_cache()
+ end
+
@spec maybe_fetch_follow_information(User.t()) :: User.t()
def maybe_fetch_follow_information(user) do
with {:ok, user} <- fetch_follow_information(user) do
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index eef985d0d..ebd4ddebf 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -338,9 +338,7 @@ defmodule Pleroma.User.Info do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
- is_binary(name) &&
- is_binary(value) &&
- String.length(name) <= name_limit &&
+ is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 8d0a57623..c52efb578 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -248,6 +248,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
+ def listen(%{to: to, actor: actor, context: context, object: object} = params) do
+ additional = params[:additional] || %{}
+ # only accept false as false value
+ local = !(params[:local] == false)
+ published = params[:published]
+
+ with listen_data <-
+ make_listen_data(
+ %{to: to, actor: actor, published: published, context: context, object: object},
+ additional
+ ),
+ {:ok, activity} <- insert(listen_data, local),
+ :ok <- maybe_federate(activity) do
+ {:ok, activity}
+ else
+ {:error, message} ->
+ {:error, message}
+ end
+ end
+
def accept(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
@@ -326,7 +346,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
local \\ true,
public \\ true
) do
- with true <- is_public?(object),
+ with true <- is_announceable?(object, user, public),
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
@@ -588,6 +608,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_thread_visibility(query, _, _), do: query
+ def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do
+ params =
+ params
+ |> Map.put("user", reading_user)
+ |> Map.put("actor_id", user.ap_id)
+ |> Map.put("whole_db", true)
+
+ recipients =
+ user_activities_recipients(%{
+ "godmode" => params["godmode"],
+ "reading_user" => reading_user
+ })
+
+ fetch_activities(recipients, params)
+ |> Enum.reverse()
+ end
+
def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
params
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 8112f6642..7cd13b4b8 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -334,6 +334,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> represent_service_actor(conn)
end
+ @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
@@ -509,4 +510,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{new_user, for_user}
end
+
+ # TODO: Add support for "object" field
+ @doc """
+ Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
+
+ Parameters:
+ - (required) `file`: data of the media
+ - (optionnal) `description`: description of the media, intended for accessibility
+
+ Response:
+ - HTTP Code: 201 Created
+ - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
+ """
+ def upload_media(%{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
+ Logger.debug(inspect(object))
+
+ conn
+ |> put_status(:created)
+ |> json(object.data)
+ end
+ end
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index dad2fead8..64c470fc8 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -431,6 +431,36 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def handle_incoming(
+ %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
+ options
+ ) do
+ actor = Containment.get_actor(data)
+
+ data =
+ Map.put(data, "actor", actor)
+ |> fix_addressing
+
+ with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
+ options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
+ object = fix_object(object, options)
+
+ params = %{
+ to: data["to"],
+ object: object,
+ actor: user,
+ context: nil,
+ local: false,
+ published: data["published"],
+ additional: Map.take(data, ["cc", "id"])
+ }
+
+ ActivityPub.listen(params)
+ else
+ _e -> :error
+ end
+ end
+
+ def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
_options
) do
@@ -723,6 +753,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
@@ -765,7 +813,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# internal -> Mastodon
# """
- def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
+ def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
+ when activity_type in ["Create", "Listen"] do
object =
object_id
|> Object.normalize()
@@ -781,6 +830,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data}
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,
# because of course it does.
def prepare_outgoing(%{"type" => "Accept"} = data) do
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 30628a793..0828591ee 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
require Logger
require Pleroma.Constants
- @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]
+ @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer", "Audio"]
@supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct)
@@ -494,7 +494,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@spec add_announce_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_announce_to_object(
- %Activity{data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]}},
+ %Activity{data: %{"actor" => actor}},
object
) do
announcements = take_announcements(object)
@@ -581,6 +581,21 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Map.merge(additional)
end
+ #### Listen-related helpers
+ def make_listen_data(params, additional) do
+ published = params.published || make_date()
+
+ %{
+ "type" => "Listen",
+ "to" => params.to |> Enum.uniq(),
+ "actor" => params.actor.ap_id,
+ "object" => params.object,
+ "published" => published,
+ "context" => params.context
+ }
+ |> Map.merge(additional)
+ end
+
#### Flag-related helpers
@spec make_flag_data(map(), map()) :: map()
def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
index 0d63f0707..88c55acdd 100644
--- a/lib/pleroma/web/activity_pub/views/object_view.ex
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -15,7 +15,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
Map.merge(base, additional)
end
- def render("object.json", %{object: %Activity{data: %{"type" => "Create"}} = activity}) do
+ def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
+ when activity_type in ["Create", "Listen"] do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
object = Object.normalize(activity)
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 993307287..6bc55c85b 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -22,9 +22,10 @@ defmodule Pleroma.Web.ActivityPub.UserView do
def render("endpoints.json", %{user: %User{local: true} = _user}) do
%{
"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),
- "sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)
+ "sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),
+ "uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media)
}
end
diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
index dfb166b65..270d0fa02 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -27,6 +27,11 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
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?(%Object{data: %{"directMessage" => true}}), do: true
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index e9a048b9b..21da8a7ff 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -513,7 +513,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
conn
|> put_view(StatusView)
- |> render("status.json", %{activity: activity})
+ |> render("show.json", %{activity: activity})
else
true ->
{:param_cast, nil}
@@ -537,7 +537,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
conn
|> put_view(StatusView)
- |> render("status.json", %{activity: activity})
+ |> render("show.json", %{activity: activity})
end
end
@@ -556,7 +556,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
def list_log(conn, params) do
{page, page_size} = page_params(params)
- log = ModerationLog.get_all(page, page_size)
+ log =
+ ModerationLog.get_all(%{
+ page: page,
+ page_size: page_size,
+ start_date: params["start_date"],
+ end_date: params["end_date"],
+ user_id: params["user_id"],
+ search: params["search"]
+ })
conn
|> put_view(ModerationLogView)
diff --git a/lib/pleroma/web/admin_api/views/moderation_log_view.ex b/lib/pleroma/web/admin_api/views/moderation_log_view.ex
index b3fc7cfe5..e7752d1f3 100644
--- a/lib/pleroma/web/admin_api/views/moderation_log_view.ex
+++ b/lib/pleroma/web/admin_api/views/moderation_log_view.ex
@@ -8,7 +8,10 @@ defmodule Pleroma.Web.AdminAPI.ModerationLogView do
alias Pleroma.ModerationLog
def render("index.json", %{log: log}) do
- render_many(log, __MODULE__, "show.json", as: :log_entry)
+ %{
+ items: render_many(log.items, __MODULE__, "show.json", as: :log_entry),
+ total: log.count
+ }
end
def render("show.json", %{log_entry: log_entry}) do
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
index 8c06364a3..101a74c63 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -43,7 +43,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
end
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}))
end
diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex
index b543909f1..08841a3e8 100644
--- a/lib/pleroma/web/chat_channel.ex
+++ b/lib/pleroma/web/chat_channel.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.ChatChannel do
if String.length(text) > 0 do
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})
broadcast!(socket, "new_msg", message)
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
new file mode 100644
index 000000000..f7da81b34
--- /dev/null
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -0,0 +1,219 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.CommonAPI.ActivityDraft do
+ alias Pleroma.Activity
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.CommonAPI.Utils
+
+ import Pleroma.Web.Gettext
+
+ defstruct valid?: true,
+ errors: [],
+ user: nil,
+ params: %{},
+ status: nil,
+ summary: nil,
+ full_payload: nil,
+ attachments: [],
+ in_reply_to: nil,
+ in_reply_to_conversation: nil,
+ visibility: nil,
+ expires_at: nil,
+ poll: nil,
+ emoji: %{},
+ content_html: nil,
+ mentions: [],
+ tags: [],
+ to: [],
+ cc: [],
+ context: nil,
+ sensitive: false,
+ object: nil,
+ preview?: false,
+ changes: %{}
+
+ def create(user, params) do
+ %__MODULE__{user: user}
+ |> put_params(params)
+ |> status()
+ |> summary()
+ |> with_valid(&attachments/1)
+ |> full_payload()
+ |> expires_at()
+ |> poll()
+ |> with_valid(&in_reply_to/1)
+ |> with_valid(&in_reply_to_conversation/1)
+ |> with_valid(&visibility/1)
+ |> content()
+ |> with_valid(&to_and_cc/1)
+ |> with_valid(&context/1)
+ |> sensitive()
+ |> with_valid(&object/1)
+ |> preview?()
+ |> with_valid(&changes/1)
+ |> validate()
+ end
+
+ defp put_params(draft, params) do
+ params = Map.put_new(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ %__MODULE__{draft | params: params}
+ end
+
+ defp status(%{params: %{"status" => status}} = draft) do
+ %__MODULE__{draft | status: String.trim(status)}
+ end
+
+ defp summary(%{params: params} = draft) do
+ %__MODULE__{draft | summary: Map.get(params, "spoiler_text", "")}
+ end
+
+ defp full_payload(%{status: status, summary: summary} = draft) do
+ full_payload = String.trim(status <> summary)
+
+ case Utils.validate_character_limit(full_payload, draft.attachments) do
+ :ok -> %__MODULE__{draft | full_payload: full_payload}
+ {:error, message} -> add_error(draft, message)
+ end
+ end
+
+ defp attachments(%{params: params} = draft) do
+ attachments = Utils.attachments_from_ids(params)
+ %__MODULE__{draft | attachments: attachments}
+ end
+
+ defp in_reply_to(draft) do
+ case Map.get(draft.params, "in_reply_to_status_id") do
+ "" -> draft
+ nil -> draft
+ id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
+ end
+ end
+
+ defp in_reply_to_conversation(draft) do
+ in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"])
+ %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
+ end
+
+ defp visibility(%{params: params} = draft) do
+ case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do
+ {visibility, "direct"} when visibility != "direct" ->
+ add_error(draft, dgettext("errors", "The message visibility must be direct"))
+
+ {visibility, _} ->
+ %__MODULE__{draft | visibility: visibility}
+ end
+ end
+
+ defp expires_at(draft) do
+ case CommonAPI.check_expiry_date(draft.params["expires_in"]) do
+ {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
+ {:error, message} -> add_error(draft, message)
+ end
+ end
+
+ defp poll(draft) do
+ case Utils.make_poll_data(draft.params) do
+ {:ok, {poll, poll_emoji}} ->
+ %__MODULE__{draft | poll: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
+
+ {:error, message} ->
+ add_error(draft, message)
+ end
+ end
+
+ defp content(draft) do
+ {content_html, mentions, tags} =
+ Utils.make_content_html(
+ draft.status,
+ draft.attachments,
+ draft.params,
+ draft.visibility
+ )
+
+ %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
+ end
+
+ defp to_and_cc(draft) do
+ addressed_users =
+ draft.mentions
+ |> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
+ |> Utils.get_addressed_users(draft.params["to"])
+
+ {to, cc} =
+ Utils.get_to_and_cc(
+ draft.user,
+ addressed_users,
+ draft.in_reply_to,
+ draft.visibility,
+ draft.in_reply_to_conversation
+ )
+
+ %__MODULE__{draft | to: to, cc: cc}
+ end
+
+ defp context(draft) do
+ context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation)
+ %__MODULE__{draft | context: context}
+ end
+
+ defp sensitive(draft) do
+ sensitive = draft.params["sensitive"] || Enum.member?(draft.tags, {"#nsfw", "nsfw"})
+ %__MODULE__{draft | sensitive: sensitive}
+ end
+
+ defp object(draft) do
+ emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
+
+ object =
+ Utils.make_note_data(
+ draft.user.ap_id,
+ draft.to,
+ draft.context,
+ draft.content_html,
+ draft.attachments,
+ draft.in_reply_to,
+ draft.tags,
+ draft.summary,
+ draft.cc,
+ draft.sensitive,
+ draft.poll
+ )
+ |> Map.put("emoji", emoji)
+
+ %__MODULE__{draft | object: object}
+ end
+
+ defp preview?(draft) do
+ preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) || false
+ %__MODULE__{draft | preview?: preview?}
+ end
+
+ defp changes(draft) do
+ direct? = draft.visibility == "direct"
+
+ changes =
+ %{
+ to: draft.to,
+ actor: draft.user,
+ context: draft.context,
+ object: draft.object,
+ additional: %{"cc" => draft.cc, "directMessage" => direct?}
+ }
+ |> Utils.maybe_add_list_data(draft.user, draft.visibility)
+
+ %__MODULE__{draft | changes: changes}
+ end
+
+ defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
+ defp with_valid(draft, _func), do: draft
+
+ defp add_error(draft, message) do
+ %__MODULE__{draft | valid?: false, errors: [message | draft.errors]}
+ end
+
+ defp validate(%{valid?: true} = draft), do: {:ok, draft}
+ defp validate(%{errors: [message | _]}), do: {:error, message}
+end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 4a74dc16f..ce73b3270 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -6,7 +6,6 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
- alias Pleroma.Emoji
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
@@ -18,14 +17,11 @@ defmodule Pleroma.Web.CommonAPI do
import Pleroma.Web.CommonAPI.Utils
def follow(follower, followed) do
+ timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
+
with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed),
- {:ok, follower, followed} <-
- User.wait_and_refresh(
- Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
- follower,
- followed
- ) do
+ {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
{:ok, follower, followed, activity}
end
end
@@ -76,29 +72,27 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
else
- _ ->
- {:error, dgettext("errors", "Could not delete")}
+ _ -> {:error, dgettext("errors", "Could not delete")}
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),
object <- Object.normalize(activity),
- nil <- Utils.get_existing_announce(user.ap_id, object) do
- ActivityPub.announce(user, object)
+ nil <- Utils.get_existing_announce(user.ap_id, object),
+ public <- public_announce?(object, params) do
+ ActivityPub.announce(user, object, nil, true, public)
else
- _ ->
- {:error, dgettext("errors", "Could not repeat")}
+ _ -> {:error, dgettext("errors", "Could not repeat")}
end
end
def unrepeat(id_or_ap_id, user) do
- with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- object <- Object.normalize(activity) do
+ with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
+ object = Object.normalize(activity)
ActivityPub.unannounce(user, object)
else
- _ ->
- {:error, dgettext("errors", "Could not unrepeat")}
+ _ -> {:error, dgettext("errors", "Could not unrepeat")}
end
end
@@ -108,30 +102,23 @@ defmodule Pleroma.Web.CommonAPI do
nil <- Utils.get_existing_like(user.ap_id, object) do
ActivityPub.like(user, object)
else
- _ ->
- {:error, dgettext("errors", "Could not favorite")}
+ _ -> {:error, dgettext("errors", "Could not favorite")}
end
end
def unfavorite(id_or_ap_id, user) do
- with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- object <- Object.normalize(activity) do
+ with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
+ object = Object.normalize(activity)
ActivityPub.unlike(user, object)
else
- _ ->
- {:error, dgettext("errors", "Could not unfavorite")}
+ _ -> {:error, dgettext("errors", "Could not unfavorite")}
end
end
- def vote(user, object, choices) do
- with "Question" <- object.data["type"],
- {:author, false} <- {:author, object.data["actor"] == user.ap_id},
- {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
- {options, max_count} <- get_options_and_max_count(object),
- option_count <- Enum.count(options),
- {:choice_check, {choices, true}} <-
- {:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
- {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
+ def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
+ with :ok <- validate_not_author(object, user),
+ :ok <- validate_existing_votes(user, object),
+ {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
answer_activities =
Enum.map(choices, fn index ->
answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
@@ -150,33 +137,49 @@ defmodule Pleroma.Web.CommonAPI do
object = Object.get_cached_by_ap_id(object.data["id"])
{:ok, answer_activities, object}
- else
- {:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")}
- {:existing_votes, _} -> {:error, dgettext("errors", "Already voted")}
- {:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")}
- {:count_check, false} -> {:error, dgettext("errors", "Too many choices")}
end
end
- defp get_options_and_max_count(object) do
- if Map.has_key?(object.data, "anyOf") do
- {object.data["anyOf"], Enum.count(object.data["anyOf"])}
+ defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
+ do: {:error, dgettext("errors", "Poll's author can't vote")}
+
+ defp validate_not_author(_, _), do: :ok
+
+ defp validate_existing_votes(%{ap_id: ap_id}, object) do
+ if Utils.get_existing_votes(ap_id, object) == [] do
+ :ok
else
- {object.data["oneOf"], 1}
+ {:error, dgettext("errors", "Already voted")}
end
end
- defp normalize_and_validate_choice_indices(choices, count) do
- Enum.map_reduce(choices, true, fn index, valid ->
- index = if is_binary(index), do: String.to_integer(index), else: index
- {index, if(valid, do: index < count, else: valid)}
- end)
+ defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
+ defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
+
+ defp normalize_and_validate_choices(choices, object) do
+ choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
+ {options, max_count} = get_options_and_max_count(object)
+ count = Enum.count(options)
+
+ with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
+ {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
+ {:ok, options, choices}
+ else
+ {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
+ {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
+ end
end
- def get_visibility(_, _, %Participation{}) do
- {"direct", "direct"}
+ 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(%{"visibility" => visibility}, in_reply_to, _)
when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
@@ -197,13 +200,13 @@ defmodule Pleroma.Web.CommonAPI do
def get_replied_to_visibility(activity) do
with %Object{} = object <- Object.normalize(activity) do
- Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
+ Visibility.get_visibility(object)
end
end
- defp check_expiry_date({:ok, nil} = res), do: res
+ def check_expiry_date({:ok, nil} = res), do: res
- defp check_expiry_date({:ok, in_seconds}) do
+ def check_expiry_date({:ok, in_seconds}) do
expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
if ActivityExpiration.expires_late_enough?(expiry) do
@@ -213,107 +216,57 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- defp check_expiry_date(expiry_str) do
+ def check_expiry_date(expiry_str) do
Ecto.Type.cast(:integer, expiry_str)
|> check_expiry_date()
end
- def post(user, %{"status" => status} = data) do
- limit = Pleroma.Config.get([:instance, :limit])
-
- with status <- String.trim(status),
- attachments <- attachments_from_ids(data),
- in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
- in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]),
- {visibility, in_reply_to_visibility} <-
- get_visibility(data, in_reply_to, in_reply_to_conversation),
- {_, false} <-
- {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
- {content_html, mentions, tags} <-
- make_content_html(
- status,
- attachments,
- data,
- visibility
- ),
- mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
- addressed_users <- get_addressed_users(mentioned_users, data["to"]),
- {poll, poll_emoji} <- make_poll_data(data),
- {to, cc} <-
- get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
- context <- make_context(in_reply_to, in_reply_to_conversation),
- cw <- data["spoiler_text"] || "",
- sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
- {:ok, expires_at} <- check_expiry_date(data["expires_in"]),
- full_payload <- String.trim(status <> cw),
- :ok <- validate_character_limit(full_payload, attachments, limit),
- object <-
- make_note_data(
- user.ap_id,
- to,
- context,
- content_html,
- attachments,
- in_reply_to,
- tags,
- cw,
- cc,
- sensitive,
- poll
- ),
- object <- put_emoji(object, full_payload, poll_emoji) do
- preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
- direct? = visibility == "direct"
-
- result =
- %{
- to: to,
- actor: user,
- context: context,
- object: object,
- additional: %{"cc" => cc, "directMessage" => direct?}
- }
- |> maybe_add_list_data(user, visibility)
- |> ActivityPub.create(preview?)
-
- if expires_at do
- with {:ok, activity} <- result do
- {:ok, _} = ActivityExpiration.create(activity, expires_at)
- end
- end
-
- result
- else
- {:private_to_public, true} ->
- {:error, dgettext("errors", "The message visibility must be direct")}
-
- {:error, _} = e ->
- e
+ def listen(user, %{"title" => _} = data) do
+ with visibility <- data["visibility"] || "public",
+ {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
+ listen_data <-
+ Map.take(data, ["album", "artist", "title", "length"])
+ |> Map.put("type", "Audio")
+ |> Map.put("to", to)
+ |> Map.put("cc", cc)
+ |> Map.put("actor", user.ap_id),
+ {:ok, activity} <-
+ ActivityPub.listen(%{
+ actor: user,
+ to: to,
+ object: listen_data,
+ context: Utils.generate_context_id(),
+ additional: %{"cc" => cc}
+ }) do
+ {:ok, activity}
+ end
+ end
- e ->
- {:error, e}
+ def post(user, %{"status" => _} = data) do
+ with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
+ draft.changes
+ |> ActivityPub.create(draft.preview?)
+ |> maybe_create_activity_expiration(draft.expires_at)
end
end
- # parse and put emoji to object data
- defp put_emoji(map, text, emojis) do
- Map.put(
- map,
- "emoji",
- Map.merge(Emoji.Formatter.get_emoji_map(text), emojis)
- )
+ defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
+ with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
+ {:ok, activity}
+ end
end
+ defp maybe_create_activity_expiration(result, _), do: result
+
# Updates the emojis for a user based on their profile
def update(user) do
emoji = emoji_from_profile(user)
- source_data = user.info |> Map.get(:source_data, {}) |> Map.put("tag", emoji)
+ source_data = user.info |> Map.get(:source_data, %{}) |> Map.put("tag", emoji)
user =
- with {:ok, user} <- User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
- user
- else
- _e -> user
+ case User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
+ {:ok, user} -> user
+ _ -> user
end
ActivityPub.update(%{
@@ -328,14 +281,8 @@ defmodule Pleroma.Web.CommonAPI do
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
- data: %{
- "type" => "Create"
- },
- object: %Object{
- data: %{
- "type" => "Note"
- }
- }
+ data: %{"type" => "Create"},
+ object: %Object{data: %{"type" => "Note"}}
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- Visibility.is_public?(activity),
{:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
@@ -372,51 +319,46 @@ defmodule Pleroma.Web.CommonAPI do
def thread_muted?(%{id: nil} = _user, _activity), do: false
def thread_muted?(user, activity) do
- with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
- false
- else
- _ -> true
- end
+ ThreadMute.check_muted(user.id, activity.data["context"]) != []
end
- def report(user, data) do
- with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
- {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
+ def report(user, %{"account_id" => account_id} = data) do
+ with {:ok, account} <- get_reported_account(account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
- {:ok, statuses} <- get_report_statuses(account, data),
- {:ok, activity} <-
- ActivityPub.flag(%{
- context: Utils.generate_context_id(),
- actor: user,
- account: account,
- statuses: statuses,
- content: content_html,
- forward: data["forward"] || false
- }) do
- {:ok, activity}
- else
- {:error, err} -> {:error, err}
- {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
- {:account, nil} -> {:error, dgettext("errors", "Account not found")}
+ {:ok, statuses} <- get_report_statuses(account, data) do
+ ActivityPub.flag(%{
+ context: Utils.generate_context_id(),
+ actor: user,
+ account: account,
+ statuses: statuses,
+ content: content_html,
+ forward: data["forward"] || false
+ })
+ end
+ end
+
+ def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
+
+ defp get_reported_account(account_id) do
+ case User.get_cached_by_id(account_id) do
+ %User{} = account -> {:ok, account}
+ _ -> {:error, dgettext("errors", "Account not found")}
end
end
def update_report_state(activity_id, state) do
- with %Activity{} = activity <- Activity.get_by_id(activity_id),
- {:ok, activity} <- Utils.update_report_state(activity, state) do
- {:ok, activity}
+ with %Activity{} = activity <- Activity.get_by_id(activity_id) do
+ Utils.update_report_state(activity, state)
else
nil -> {:error, :not_found}
- {:error, reason} -> {:error, reason}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
- {:ok, activity} <- toggle_sensitive(activity, opts),
- {:ok, activity} <- set_visibility(activity, opts) do
- {:ok, activity}
+ {:ok, activity} <- toggle_sensitive(activity, opts) do
+ set_visibility(activity, opts)
else
nil -> {:error, :not_found}
{:error, reason} -> {:error, reason}
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 52fbc162b..88a5f434a 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.CommonAPI.Utils do
import Pleroma.Web.Gettext
+ import Pleroma.Web.ControllerHelper, only: [truthy_param?: 1]
alias Calendar.Strftime
alias Pleroma.Activity
@@ -41,14 +42,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
- def get_replied_to_activity(""), do: nil
-
- def get_replied_to_activity(id) when not is_nil(id) do
- Activity.get_by_id(id)
- end
-
- def get_replied_to_activity(_), do: nil
-
def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
attachments_from_ids_descs(ids, desc)
end
@@ -159,70 +152,74 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_add_list_data(activity_params, _, _), do: activity_params
+ def make_poll_data(%{"poll" => %{"expires_in" => expires_in}} = data)
+ when is_binary(expires_in) do
+ # In some cases mastofe sends out strings instead of integers
+ data
+ |> put_in(["poll", "expires_in"], String.to_integer(expires_in))
+ |> make_poll_data()
+ end
+
def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
when is_list(options) do
- %{max_expiration: max_expiration, min_expiration: min_expiration} =
- limits = Pleroma.Config.get([:instance, :poll_limits])
-
- # XXX: There is probably a cleaner way of doing this
- try do
- # In some cases mastofe sends out strings instead of integers
- expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in
-
- if Enum.count(options) > limits.max_options do
- raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options"
- end
+ limits = Pleroma.Config.get([:instance, :poll_limits])
- {poll, emoji} =
+ with :ok <- validate_poll_expiration(expires_in, limits),
+ :ok <- validate_poll_options_amount(options, limits),
+ :ok <- validate_poll_options_length(options, limits) do
+ {option_notes, emoji} =
Enum.map_reduce(options, %{}, fn option, emoji ->
- if String.length(option) > limits.max_option_chars do
- raise ArgumentError,
- message:
- "Poll options cannot be longer than #{limits.max_option_chars} characters each"
- end
-
- {%{
- "name" => option,
- "type" => "Note",
- "replies" => %{"type" => "Collection", "totalItems" => 0}
- }, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
- end)
-
- case expires_in do
- expires_in when expires_in > max_expiration ->
- raise ArgumentError, message: "Expiration date is too far in the future"
-
- expires_in when expires_in < min_expiration ->
- raise ArgumentError, message: "Expiration date is too soon"
+ note = %{
+ "name" => option,
+ "type" => "Note",
+ "replies" => %{"type" => "Collection", "totalItems" => 0}
+ }
- _ ->
- :noop
- end
+ {note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
+ end)
end_time =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(expires_in)
|> NaiveDateTime.to_iso8601()
- poll =
- if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do
- %{"type" => "Question", "anyOf" => poll, "closed" => end_time}
- else
- %{"type" => "Question", "oneOf" => poll, "closed" => end_time}
- end
+ key = if truthy_param?(data["poll"]["multiple"]), do: "anyOf", else: "oneOf"
+ poll = %{"type" => "Question", key => option_notes, "closed" => end_time}
- {poll, emoji}
- rescue
- e in ArgumentError -> e.message
+ {:ok, {poll, emoji}}
end
end
def make_poll_data(%{"poll" => poll}) when is_map(poll) do
- "Invalid poll"
+ {:error, "Invalid poll"}
end
def make_poll_data(_data) do
- {%{}, %{}}
+ {:ok, {%{}, %{}}}
+ end
+
+ defp validate_poll_options_amount(options, %{max_options: max_options}) do
+ if Enum.count(options) > max_options do
+ {:error, "Poll can't contain more than #{max_options} options"}
+ else
+ :ok
+ end
+ end
+
+ defp validate_poll_options_length(options, %{max_option_chars: max_option_chars}) do
+ if Enum.any?(options, &(String.length(&1) > max_option_chars)) do
+ {:error, "Poll options cannot be longer than #{max_option_chars} characters each"}
+ else
+ :ok
+ end
+ end
+
+ defp validate_poll_expiration(expires_in, %{min_expiration: min, max_expiration: max}) do
+ cond do
+ expires_in > max -> {:error, "Expiration date is too far in the future"}
+ expires_in < min -> {:error, "Expiration date is too soon"}
+ true -> :ok
+ end
end
def make_content_html(
@@ -234,7 +231,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
no_attachment_links =
data
|> Map.get("no_attachment_links", Config.get([:instance, :no_attachment_links]))
- |> Kernel.in([true, "true"])
+ |> truthy_param?()
content_type = get_content_type(data["content_type"])
@@ -347,25 +344,25 @@ defmodule Pleroma.Web.CommonAPI.Utils do
attachments,
in_reply_to,
tags,
- cw \\ nil,
+ summary \\ nil,
cc \\ [],
sensitive \\ false,
- merge \\ %{}
+ extra_params \\ %{}
) do
%{
"type" => "Note",
"to" => to,
"cc" => cc,
"content" => content_html,
- "summary" => cw,
- "sensitive" => !Enum.member?(["false", "False", "0", false], sensitive),
+ "summary" => summary,
+ "sensitive" => truthy_param?(sensitive),
"context" => context,
"attachment" => attachments,
"actor" => actor,
"tag" => Keyword.values(tags) |> Enum.uniq()
}
|> add_in_reply_to(in_reply_to)
- |> Map.merge(merge)
+ |> Map.merge(extra_params)
end
defp add_in_reply_to(object, nil), do: object
@@ -434,12 +431,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
- def emoji_from_profile(%{info: _info} = user) do
- (Emoji.Formatter.get_emoji(user.bio) ++ Emoji.Formatter.get_emoji(user.name))
- |> Enum.map(fn {shortcode, %Emoji{file: url}} ->
+ def emoji_from_profile(%User{bio: bio, name: name}) do
+ [bio, name]
+ |> Enum.map(&Emoji.Formatter.get_emoji/1)
+ |> Enum.concat()
+ |> Enum.map(fn {shortcode, %Emoji{file: path}} ->
%{
"type" => "Emoji",
- "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
+ "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{path}"},
"name" => ":#{shortcode}:"
}
end)
@@ -571,15 +570,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
}
end
- def validate_character_limit(full_payload, attachments, limit) do
+ def validate_character_limit("" = _full_payload, [] = _attachments) do
+ {:error, dgettext("errors", "Cannot post an empty status without attachments")}
+ end
+
+ def validate_character_limit(full_payload, _attachments) do
+ limit = Pleroma.Config.get([:instance, :limit])
length = String.length(full_payload)
if length < limit do
- if length > 0 or Enum.count(attachments) > 0 do
- :ok
- else
- {:error, dgettext("errors", "Cannot post an empty status without attachments")}
- end
+ :ok
else
{:error, dgettext("errors", "The status is over the character limit")}
end
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index b53a01955..9a4e322c9 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller
# As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
- @falsy_param_values [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"]
+ @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"]
def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil
def truthy_param?(value), do: value not in @falsy_param_values
@@ -68,4 +68,23 @@ defmodule Pleroma.Web.ControllerHelper do
conn
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
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index eb805e853..2212e93f4 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -97,10 +97,7 @@ defmodule Pleroma.Web.Endpoint do
extra: extra
)
- # Note: the plug and its configuration is compile-time this can't be upstreamed yet
- if proxies = Pleroma.Config.get([__MODULE__, :reverse_proxies]) do
- plug(RemoteIp, proxies: proxies)
- end
+ plug(Pleroma.Plugs.RemoteIp)
defmodule Instrumenter do
use Prometheus.PhoenixInstrumenter
diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex
new file mode 100644
index 000000000..ac9af7502
--- /dev/null
+++ b/lib/pleroma/web/masto_fe_controller.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastoFEController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User
+
+ @doc "GET /web/*path"
+ def index(%{assigns: %{user: user}} = conn, _params) do
+ token = get_session(conn, :oauth_token)
+
+ if user && token do
+ conn
+ |> put_layout(false)
+ |> render("index.html", token: token, user: user, custom_emojis: Pleroma.Emoji.get_all())
+ else
+ conn
+ |> put_session(:return_to, conn.request_path)
+ |> redirect(to: "/web/login")
+ end
+ end
+
+ @doc "PUT /api/web/settings"
+ 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
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
new file mode 100644
index 000000000..df14ad66f
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
new file mode 100644
index 000000000..abbe16a88
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
new file mode 100644
index 000000000..bfd5120ba
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
new file mode 100644
index 000000000..ea1e36a12
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -0,0 +1,32 @@
+# 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.ConversationController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Repo
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ @doc "GET /api/v1/conversations"
+ def index(%{assigns: %{user: user}} = conn, params) do
+ participations = Participation.for_user_with_last_activity_id(user, params)
+
+ conn
+ |> add_link_headers(participations)
+ |> render("participations.json", participations: participations, for: user)
+ end
+
+ @doc "POST /api/v1/conversations/:id/read"
+ def read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
+ with %Participation{} = participation <-
+ Repo.get_by(Participation, id: participation_id, user_id: user.id),
+ {:ok, participation} <- Participation.mark_as_read(participation) do
+ render(conn, "participation.json", participation: participation, for: user)
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
new file mode 100644
index 000000000..391c0648b
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
new file mode 100644
index 000000000..03db6c9b8
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -0,0 +1,26 @@
+# 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.DomainBlockController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User
+
+ @doc "GET /api/v1/domain_blocks"
+ def index(%{assigns: %{user: %{info: info}}} = conn, _) do
+ json(conn, Map.get(info, :domain_blocks, []))
+ end
+
+ @doc "POST /api/v1/domain_blocks"
+ def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
+ User.block_domain(blocker, domain)
+ json(conn, %{})
+ end
+
+ @doc "DELETE /api/v1/domain_blocks"
+ def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
+ User.unblock_domain(blocker, domain)
+ json(conn, %{})
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
new file mode 100644
index 000000000..19041304e
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -0,0 +1,72 @@
+# 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.FilterController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Filter
+
+ @doc "GET /api/v1/filters"
+ def index(%{assigns: %{user: user}} = conn, _) do
+ filters = Filter.get_filters(user)
+
+ render(conn, "filters.json", filters: filters)
+ end
+
+ @doc "POST /api/v1/filters"
+ def create(
+ %{assigns: %{user: user}} = conn,
+ %{"phrase" => phrase, "context" => context} = params
+ ) do
+ query = %Filter{
+ user_id: user.id,
+ phrase: phrase,
+ context: context,
+ hide: Map.get(params, "irreversible", false),
+ whole_word: Map.get(params, "boolean", true)
+ # expires_at
+ }
+
+ {:ok, response} = Filter.create(query)
+
+ render(conn, "filter.json", filter: response)
+ end
+
+ @doc "GET /api/v1/filters/:id"
+ def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ filter = Filter.get(filter_id, user)
+
+ render(conn, "filter.json", filter: filter)
+ end
+
+ @doc "PUT /api/v1/filters/:id"
+ def update(
+ %{assigns: %{user: user}} = conn,
+ %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
+ ) do
+ query = %Filter{
+ user_id: user.id,
+ filter_id: filter_id,
+ phrase: phrase,
+ context: context,
+ hide: Map.get(params, "irreversible", nil),
+ whole_word: Map.get(params, "boolean", true)
+ # expires_at
+ }
+
+ {:ok, response} = Filter.update(query)
+ render(conn, "filter.json", filter: response)
+ end
+
+ @doc "DELETE /api/v1/filters/:id"
+ def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ query = %Filter{
+ user_id: user.id,
+ filter_id: filter_id
+ }
+
+ {:ok, _} = Filter.delete(query)
+ json(conn, %{})
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
new file mode 100644
index 000000000..ce7b625ee
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -0,0 +1,49 @@
+# 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.FollowRequestController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
+ plug(:assign_follower when action != :index)
+
+ action_fallback(:errors)
+
+ @doc "GET /api/v1/follow_requests"
+ def index(%{assigns: %{user: followed}} = conn, _params) do
+ follow_requests = User.get_follow_requests(followed)
+
+ render(conn, "index.json", for: followed, users: follow_requests, as: :user)
+ end
+
+ @doc "POST /api/v1/follow_requests/:id/authorize"
+ def authorize(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
+ with {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
+ render(conn, "relationship.json", user: followed, target: follower)
+ end
+ end
+
+ @doc "POST /api/v1/follow_requests/:id/reject"
+ def reject(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
+ with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
+ render(conn, "relationship.json", user: followed, target: follower)
+ end
+ end
+
+ defp assign_follower(%{params: %{"id" => id}} = conn, _) do
+ case User.get_cached_by_id(id) do
+ %User{} = follower -> assign(conn, :follower, follower)
+ nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
+ end
+ end
+
+ defp errors(conn, {:error, message}) do
+ conn
+ |> put_status(:forbidden)
+ |> json(%{error: message})
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
new file mode 100644
index 000000000..a55f60fec
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
index 2873deda8..50f42bee5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -49,7 +49,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
with {:ok, users} <- Pleroma.List.get_following(list) do
conn
|> put_view(AccountView)
- |> render("accounts.json", for: user, users: users, as: :user)
+ |> render("index.json", for: user, users: users, as: :user)
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
index 1e88ff7fe..e92f5d089 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -5,1010 +5,31 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper,
- only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
- alias Ecto.Changeset
- alias Pleroma.Activity
alias Pleroma.Bookmark
- alias Pleroma.Config
- alias Pleroma.Conversation.Participation
- alias Pleroma.Emoji
- alias Pleroma.Filter
- alias Pleroma.HTTP
- alias Pleroma.Object
alias Pleroma.Pagination
- alias Pleroma.Plugs.RateLimiter
- alias Pleroma.Repo
- alias Pleroma.ScheduledActivity
- 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.ConversationView
- alias Pleroma.Web.MastodonAPI.FilterView
- alias Pleroma.Web.MastodonAPI.ListView
- alias Pleroma.Web.MastodonAPI.MastodonAPI
- alias Pleroma.Web.MastodonAPI.MastodonView
- alias Pleroma.Web.MastodonAPI.ReportView
- alias Pleroma.Web.MastodonAPI.ScheduledActivityView
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
-
- alias Pleroma.Web.ControllerHelper
- import Ecto.Query
require Logger
- require Pleroma.Constants
-
- @rate_limited_relations_actions ~w(follow unfollow)a
-
- @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
- post_status delete_status)a
-
- plug(
- RateLimiter,
- {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
- when action in ~w(reblog_status unreblog_status)a
- )
-
- plug(
- RateLimiter,
- {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
- when action in ~w(fav_status unfav_status)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, :statuses_actions when action in @rate_limited_status_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)
- 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, ControllerHelper.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
- 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 home_timeline(%{assigns: %{user: user}} = conn, params) do
- params =
- params
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
-
- activities =
- [user.ap_id | user.following]
- |> 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 public_timeline(%{assigns: %{user: user}} = conn, params) do
- local_only = params["local"] in [true, "True", "true", "1"]
-
- activities =
- params
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("local_only", local_only)
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> ActivityPub.fetch_public_activities()
- |> Enum.reverse()
-
- conn
- |> add_link_headers(activities, %{"local" => local_only})
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- end
-
- def user_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 =
- 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 dm_timeline(%{assigns: %{user: user}} = conn, params) do
- params =
- params
- |> Map.put("type", "Create")
- |> Map.put("blocking_user", user)
- |> Map.put("user", user)
- |> Map.put(:visibility, "direct")
-
- activities =
- [user.ap_id]
- |> ActivityPub.fetch_activities_query(params)
- |> Pagination.fetch_paginated(params)
-
- conn
- |> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- end
-
- def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
- limit = 100
-
- activities =
- ids
- |> Enum.take(limit)
- |> Activity.all_by_ids_with_object()
- |> Enum.filter(&Visibility.visible_for_user?(&1, user))
-
- conn
- |> put_view(StatusView)
- |> render("index.json", activities: activities, for: user, as: :activity)
- end
-
- def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- true <- Visibility.visible_for_user?(activity, user) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user})
- end
- end
-
- def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id(id),
- activities <-
- ActivityPub.fetch_activities_for_context(activity.data["context"], %{
- "blocking_user" => user,
- "user" => user,
- "exclude_id" => activity.id
- }),
- grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
- result = %{
- ancestors:
- StatusView.render(
- "index.json",
- for: user,
- activities: grouped_activities[true] || [],
- as: :activity
- )
- |> Enum.reverse(),
- # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
- descendants:
- StatusView.render(
- "index.json",
- for: user,
- activities: grouped_activities[false] || [],
- as: :activity
- )
- |> Enum.reverse()
- # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
- }
-
- json(conn, result)
- 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 scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
- with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
- conn
- |> add_link_headers(scheduled_activities)
- |> put_view(ScheduledActivityView)
- |> render("index.json", %{scheduled_activities: scheduled_activities})
- end
- end
-
- def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
- with %ScheduledActivity{} = scheduled_activity <-
- ScheduledActivity.get(user, scheduled_activity_id) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- else
- _ -> {:error, :not_found}
- end
- end
-
- def update_scheduled_status(
- %{assigns: %{user: user}} = conn,
- %{"id" => scheduled_activity_id} = params
- ) do
- with %ScheduledActivity{} = scheduled_activity <-
- ScheduledActivity.get(user, scheduled_activity_id),
- {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- else
- nil -> {:error, :not_found}
- error -> error
- end
- end
-
- def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
- with %ScheduledActivity{} = scheduled_activity <-
- ScheduledActivity.get(user, scheduled_activity_id),
- {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- else
- nil -> {:error, :not_found}
- error -> error
- end
- end
-
- def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
- params =
- params
- |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
-
- scheduled_at = params["scheduled_at"]
-
- if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
- with {:ok, scheduled_activity} <-
- ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- end
- else
- params = Map.drop(params, ["scheduled_at"])
-
- case CommonAPI.post(user, params) do
- {:error, message} ->
- conn
- |> put_status(:unprocessable_entity)
- |> json(%{error: message})
-
- {:ok, activity} ->
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{
- activity: activity,
- for: user,
- as: :activity,
- with_direct_conversation_id: true
- })
- end
- end
- end
-
- def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
- json(conn, %{})
- else
- _e -> render_error(conn, :forbidden, "Can't delete this post")
- end
- end
-
- def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
- %Activity{} = announce <- Activity.normalize(announce.data) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: announce, for: user, as: :activity})
- end
- end
-
- def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- %User{} = user <- User.get_cached_by_nickname(user.nickname),
- true <- Visibility.visible_for_user?(activity, user),
- {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- %User{} = user <- User.get_cached_by_nickname(user.nickname),
- true <- Visibility.visible_for_user?(activity, user),
- {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- activity = Activity.get_by_id(id)
-
- with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- activity = Activity.get_by_id(id)
-
- with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- id = List.wrap(id)
- q = from(u in User, where: u.id in ^id)
- targets = Repo.all(q)
-
- 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, data) do
- with %Object{} = object <- Repo.get(Object, data["id"]),
- true <- Object.authorize_mutation(object, user),
- true <- is_binary(data["description"]),
- description <- data["description"] do
- new_data = %{object.data | "name" => description}
-
- {:ok, _} =
- object
- |> Object.change(%{data: new_data})
- |> Repo.update()
-
- attachment_data = Map.put(new_data, "id", object.id)
-
- conn
- |> put_view(StatusView)
- |> render("attachment.json", %{attachment: attachment_data})
- end
- end
-
- 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)
-
- conn
- |> json(mascot)
- end
-
- def favourited_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: %{"likes" => likes}} <- Object.normalize(activity) do
- q = from(u in User, where: u.ap_id in ^likes)
-
- users =
- Repo.all(q)
- |> Enum.filter(&(not User.blocks?(user, &1)))
-
- conn
- |> put_view(AccountView)
- |> render("accounts.json", %{for: user, users: users, as: :user})
- else
- {:visible, false} -> {:error, :not_found}
- _ -> json(conn, [])
- end
- end
-
- 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
- q = from(u in User, where: u.ap_id in ^announces)
-
- users =
- Repo.all(q)
- |> Enum.filter(&(not User.blocks?(user, &1)))
-
- conn
- |> put_view(AccountView)
- |> render("accounts.json", %{for: user, users: users, as: :user})
- else
- {:visible, false} -> {:error, :not_found}
- _ -> json(conn, [])
- end
- end
-
- def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
- local_only = params["local"] in [true, "True", "true", "1"]
-
- tags =
- [params["tag"], params["any"]]
- |> List.flatten()
- |> Enum.uniq()
- |> Enum.filter(& &1)
- |> Enum.map(&String.downcase(&1))
-
- tag_all =
- params["all"] ||
- []
- |> Enum.map(&String.downcase(&1))
-
- tag_reject =
- params["none"] ||
- []
- |> Enum.map(&String.downcase(&1))
-
- activities =
- params
- |> Map.put("type", "Create")
- |> Map.put("local_only", local_only)
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
- |> Map.put("tag", tags)
- |> Map.put("tag_all", tag_all)
- |> Map.put("tag_reject", tag_reject)
- |> ActivityPub.fetch_public_activities()
- |> Enum.reverse()
-
- conn
- |> add_link_headers(activities, %{"local" => local_only})
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- 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_requests(%{assigns: %{user: followed}} = conn, _params) do
- follow_requests = User.get_follow_requests(followed)
-
- conn
- |> put_view(AccountView)
- |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
- end
-
- def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
- with %User{} = follower <- User.get_cached_by_id(id),
- {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: followed, target: follower})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
- with %User{} = follower <- User.get_cached_by_id(id),
- {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: followed, target: follower})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- 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
+ 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("account.json", %{user: followed, for: follower})
- else
- {:followed, _} ->
- {:error, :not_found}
-
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- 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})
+ |> render("show.json", %{user: followed, for: follower})
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)
@@ -1018,90 +39,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def mutes(%{assigns: %{user: user}} = conn, _) 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)
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
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)
end
end
- def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
- json(conn, info.domain_blocks || [])
- end
-
- def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
- User.block_domain(blocker, domain)
- json(conn, %{})
- end
-
- def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
- User.unblock_domain(blocker, domain)
- json(conn, %{})
- 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
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- 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
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
@@ -1119,37 +68,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> render("index.json", %{activities: activities, for: user, as: :activity})
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
user = User.get_cached_by_id(user.id)
@@ -1167,239 +85,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
- def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
- lists = Pleroma.List.get_lists_account_belongs(user, account_id)
- res = ListView.render("lists.json", lists: lists)
- json(conn, res)
- end
-
- def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
- with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
- params =
- params
- |> Map.put("type", "Create")
- |> Map.put("blocking_user", user)
- |> Map.put("user", user)
- |> Map.put("muting_user", user)
-
- # we must filter the following list for the user to avoid leaking statuses the user
- # does not actually have permission to see (for more info, peruse security issue #270).
- activities =
- following
- |> Enum.filter(fn x -> x in user.following end)
- |> ActivityPub.fetch_activities_bounded(following, params)
- |> Enum.reverse()
-
- conn
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- else
- _e -> render_error(conn, :forbidden, "Error.")
- end
- 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("account.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(),
- %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
- {: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
-
- defp get_or_make_app do
- find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
- scopes = ["read", "write", "follow", "push"]
-
- with %App{} = app <- Repo.get_by(App, find_attrs) do
- {:ok, app} =
- if app.scopes == scopes do
- {:ok, app}
- else
- app
- |> Changeset.change(%{scopes: scopes})
- |> Repo.update()
- end
-
- {:ok, app}
- else
- _e ->
- cs =
- App.register_changeset(
- %App{},
- Map.put(find_attrs, :scopes, scopes)
- )
-
- Repo.insert(cs)
- end
- end
-
- def logout(conn, _) do
- conn
- |> clear_session
- |> redirect(to: "/")
- end
-
- def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- Logger.debug("Unimplemented, returning unmodified relationship")
-
- with %User{} = target <- User.get_cached_by_id(id) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: user, target: target})
- end
- end
-
+ # Stubs for unimplemented mastodon api
+ #
def empty_array(conn, _) do
Logger.debug("Unimplemented, returning an empty array")
json(conn, [])
@@ -1409,250 +96,4 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
Logger.debug("Unimplemented, returning an empty object")
json(conn, %{})
end
-
- def get_filters(%{assigns: %{user: user}} = conn, _) do
- filters = Filter.get_filters(user)
- res = FilterView.render("filters.json", filters: filters)
- json(conn, res)
- end
-
- def create_filter(
- %{assigns: %{user: user}} = conn,
- %{"phrase" => phrase, "context" => context} = params
- ) do
- query = %Filter{
- user_id: user.id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", false),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
- }
-
- {:ok, response} = Filter.create(query)
- res = FilterView.render("filter.json", filter: response)
- json(conn, res)
- end
-
- def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
- filter = Filter.get(filter_id, user)
- res = FilterView.render("filter.json", filter: filter)
- json(conn, res)
- end
-
- def update_filter(
- %{assigns: %{user: user}} = conn,
- %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
- ) do
- query = %Filter{
- user_id: user.id,
- filter_id: filter_id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", nil),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
- }
-
- {:ok, response} = Filter.update(query)
- res = FilterView.render("filter.json", filter: response)
- json(conn, res)
- end
-
- def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
- query = %Filter{
- user_id: user.id,
- filter_id: filter_id
- }
-
- {:ok, _} = Filter.delete(query)
- 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 status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
- with %Activity{} = activity <- Activity.get_by_id(status_id),
- true <- Visibility.visible_for_user?(activity, user) do
- data =
- StatusView.render(
- "card.json",
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- )
-
- json(conn, data)
- else
- _e ->
- %{}
- end
- end
-
- def reports(%{assigns: %{user: user}} = conn, params) do
- case CommonAPI.report(user, params) do
- {:ok, activity} ->
- conn
- |> put_view(ReportView)
- |> try_render("report.json", %{activity: activity})
-
- {:error, err} ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: err})
- 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, _params) do
- render_error(conn, :bad_request, "Missing parameters")
- end
-
- def account_register(conn, _) do
- render_error(conn, :forbidden, "Invalid credentials")
- end
-
- def conversations(%{assigns: %{user: user}} = conn, params) do
- participations = Participation.for_user_with_last_activity_id(user, params)
-
- conversations =
- Enum.map(participations, fn participation ->
- ConversationView.render("participation.json", %{participation: participation, for: user})
- end)
-
- conn
- |> add_link_headers(participations)
- |> json(conversations)
- end
-
- def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
- with %Participation{} = participation <-
- Repo.get_by(Participation, id: participation_id, user_id: user.id),
- {:ok, participation} <- Participation.mark_as_read(participation) do
- participation_view =
- ConversationView.render("participation.json", %{participation: participation, for: user})
-
- conn
- |> json(participation_view)
- 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 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?(false), do: false
- defp present?(_), do: true
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
new file mode 100644
index 000000000..57a5b60fb
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
new file mode 100644
index 000000000..fbf7f8673
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
new file mode 100644
index 000000000..1c084b740
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -0,0 +1,16 @@
+# 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.ReportController do
+ use Pleroma.Web, :controller
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ @doc "POST /api/v1/reports"
+ def create(%{assigns: %{user: user}} = conn, params) do
+ with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
+ render(conn, "show.json", activity: activity)
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
new file mode 100644
index 000000000..0a56b10b6
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -0,0 +1,51 @@
+# 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.ScheduledActivityController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.Web.MastodonAPI.MastodonAPI
+
+ plug(:assign_scheduled_activity when action != :index)
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ @doc "GET /api/v1/scheduled_statuses"
+ def index(%{assigns: %{user: user}} = conn, params) do
+ with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
+ conn
+ |> add_link_headers(scheduled_activities)
+ |> render("index.json", scheduled_activities: scheduled_activities)
+ end
+ end
+
+ @doc "GET /api/v1/scheduled_statuses/:id"
+ def show(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
+ render(conn, "show.json", scheduled_activity: scheduled_activity)
+ end
+
+ @doc "PUT /api/v1/scheduled_statuses/:id"
+ def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
+ with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
+ render(conn, "show.json", scheduled_activity: scheduled_activity)
+ end
+ end
+
+ @doc "DELETE /api/v1/scheduled_statuses/:id"
+ def delete(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
+ with {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
+ render(conn, "show.json", scheduled_activity: scheduled_activity)
+ end
+ end
+
+ defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+ case ScheduledActivity.get(user, id) do
+ %ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
+ nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index c91713773..3fc89d645 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
conn
|> put_view(AccountView)
- |> render("accounts.json", users: accounts, for: user, as: :user)
+ |> render("index.json", users: accounts, for: user, as: :user)
end
def search2(conn, params), do: do_search(:v2, conn, params)
@@ -72,7 +72,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defp resource_search(_, "accounts", query, options) do
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
defp resource_search(_, "statuses", query, options) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
new file mode 100644
index 000000000..79cced163
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -0,0 +1,286 @@
+# 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.StatusController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [try_render: 3]
+
+ require Ecto.Query
+
+ alias Pleroma.Activity
+ alias Pleroma.Bookmark
+ alias Pleroma.Object
+ alias Pleroma.Plugs.RateLimiter
+ alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+
+ @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
+
+ plug(
+ RateLimiter,
+ {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
+ when action in ~w(reblog unreblog)a
+ )
+
+ plug(
+ RateLimiter,
+ {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+ when action in ~w(favourite unfavourite)a
+ )
+
+ plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ @doc """
+ GET `/api/v1/statuses?ids[]=1&ids[]=2`
+
+ `ids` query param is required
+ """
+ def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
+ limit = 100
+
+ activities =
+ ids
+ |> Enum.take(limit)
+ |> Activity.all_by_ids_with_object()
+ |> Enum.filter(&Visibility.visible_for_user?(&1, user))
+
+ render(conn, "index.json", activities: activities, for: user, as: :activity)
+ end
+
+ @doc """
+ POST /api/v1/statuses
+
+ Creates a scheduled status when `scheduled_at` param is present and it's far enough
+ """
+ def create(
+ %{assigns: %{user: user}} = conn,
+ %{"status" => _, "scheduled_at" => scheduled_at} = params
+ ) do
+ params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+
+ if ScheduledActivity.far_enough?(scheduled_at) do
+ with {:ok, scheduled_activity} <-
+ ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", scheduled_activity: scheduled_activity)
+ end
+ else
+ create(conn, Map.drop(params, ["scheduled_at"]))
+ end
+ end
+
+ @doc """
+ POST /api/v1/statuses
+
+ Creates a regular status
+ """
+ def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
+ params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+
+ with {:ok, activity} <- CommonAPI.post(user, params) do
+ try_render(conn, "show.json",
+ activity: activity,
+ for: user,
+ as: :activity,
+ with_direct_conversation_id: true
+ )
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: message})
+ end
+ end
+
+ def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
+ create(conn, Map.put(params, "status", ""))
+ end
+
+ @doc "GET /api/v1/statuses/:id"
+ def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ true <- Visibility.visible_for_user?(activity, user) do
+ try_render(conn, "show.json", activity: activity, for: user)
+ end
+ end
+
+ @doc "DELETE /api/v1/statuses/:id"
+ def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
+ json(conn, %{})
+ else
+ _e -> render_error(conn, :forbidden, "Can't delete this post")
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/reblog"
+ 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
+ end
+
+ @doc "POST /api/v1/statuses/:id/unreblog"
+ def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
+ try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/favourite"
+ def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/unfavourite"
+ def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/pin"
+ def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/unpin"
+ def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/bookmark"
+ def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ %User{} = user <- User.get_cached_by_nickname(user.nickname),
+ true <- Visibility.visible_for_user?(activity, user),
+ {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/unbookmark"
+ def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ %User{} = user <- User.get_cached_by_nickname(user.nickname),
+ true <- Visibility.visible_for_user?(activity, user),
+ {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/mute"
+ def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id),
+ {:ok, activity} <- CommonAPI.add_mute(user, activity) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/unmute"
+ def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id),
+ {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "GET /api/v1/statuses/:id/card"
+ @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
+ def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
+ with %Activity{} = activity <- Activity.get_by_id(status_id),
+ true <- Visibility.visible_for_user?(activity, user) do
+ data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ render(conn, "card.json", data)
+ else
+ _ -> render_error(conn, :not_found, "Record not found")
+ end
+ end
+
+ @doc "GET /api/v1/statuses/:id/favourited_by"
+ def favourited_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: %{"likes" => likes}} <- Object.normalize(activity) do
+ users =
+ User
+ |> Ecto.Query.where([u], u.ap_id in ^likes)
+ |> Repo.all()
+ |> Enum.filter(&(not User.blocks?(user, &1)))
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", for: user, users: users, as: :user)
+ else
+ {:visible, false} -> {:error, :not_found}
+ _ -> json(conn, [])
+ end
+ end
+
+ @doc "GET /api/v1/statuses/:id/reblogged_by"
+ 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, "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)
+ |> Repo.all()
+ |> Enum.filter(&(not User.blocks?(user, &1)))
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", for: user, users: users, as: :user)
+ else
+ {:visible, false} -> {:error, :not_found}
+ _ -> json(conn, [])
+ end
+ end
+
+ @doc "GET /api/v1/statuses/:id/context"
+ def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id) do
+ activities =
+ ActivityPub.fetch_activities_for_context(activity.data["context"], %{
+ "blocking_user" => user,
+ "user" => user,
+ "exclude_id" => activity.id
+ })
+
+ render(conn, "context.json", activity: activity, activities: activities, user: user)
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
new file mode 100644
index 000000000..9076bb849
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
new file mode 100644
index 000000000..bb8b0eb32
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -0,0 +1,136 @@
+# 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.TimelineController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper,
+ only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1]
+
+ alias Pleroma.Pagination
+ alias Pleroma.Web.ActivityPub.ActivityPub
+
+ plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
+
+ # GET /api/v1/timelines/home
+ def home(%{assigns: %{user: user}} = conn, params) do
+ params =
+ params
+ |> Map.put("type", ["Create", "Announce"])
+ |> Map.put("blocking_user", user)
+ |> Map.put("muting_user", user)
+ |> Map.put("user", user)
+
+ recipients = [user.ap_id | user.following]
+
+ activities =
+ recipients
+ |> ActivityPub.fetch_activities(params)
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities)
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ end
+
+ # GET /api/v1/timelines/direct
+ def direct(%{assigns: %{user: user}} = conn, params) do
+ params =
+ params
+ |> Map.put("type", "Create")
+ |> Map.put("blocking_user", user)
+ |> Map.put("user", user)
+ |> Map.put(:visibility, "direct")
+
+ activities =
+ [user.ap_id]
+ |> ActivityPub.fetch_activities_query(params)
+ |> Pagination.fetch_paginated(params)
+
+ conn
+ |> add_link_headers(activities)
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ end
+
+ # GET /api/v1/timelines/public
+ def public(%{assigns: %{user: user}} = conn, params) do
+ local_only = truthy_param?(params["local"])
+
+ activities =
+ params
+ |> Map.put("type", ["Create", "Announce"])
+ |> Map.put("local_only", local_only)
+ |> Map.put("blocking_user", user)
+ |> Map.put("muting_user", user)
+ |> ActivityPub.fetch_public_activities()
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities, %{"local" => local_only})
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ end
+
+ # GET /api/v1/timelines/tag/:tag
+ def hashtag(%{assigns: %{user: user}} = conn, params) do
+ local_only = truthy_param?(params["local"])
+
+ tags =
+ [params["tag"], params["any"]]
+ |> List.flatten()
+ |> Enum.uniq()
+ |> Enum.filter(& &1)
+ |> Enum.map(&String.downcase(&1))
+
+ tag_all =
+ params
+ |> Map.get("all", [])
+ |> Enum.map(&String.downcase(&1))
+
+ tag_reject =
+ params
+ |> Map.get("none", [])
+ |> Enum.map(&String.downcase(&1))
+
+ activities =
+ params
+ |> Map.put("type", "Create")
+ |> Map.put("local_only", local_only)
+ |> Map.put("blocking_user", user)
+ |> Map.put("muting_user", user)
+ |> Map.put("user", user)
+ |> Map.put("tag", tags)
+ |> Map.put("tag_all", tag_all)
+ |> Map.put("tag_reject", tag_reject)
+ |> ActivityPub.fetch_public_activities()
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities, %{"local" => local_only})
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ end
+
+ # GET /api/v1/timelines/list/:list_id
+ def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
+ with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
+ params =
+ params
+ |> Map.put("type", "Create")
+ |> Map.put("blocking_user", user)
+ |> Map.put("user", user)
+ |> Map.put("muting_user", user)
+
+ # we must filter the following list for the user to avoid leaking statuses the user
+ # does not actually have permission to see (for more info, peruse security issue #270).
+ activities =
+ following
+ |> Enum.filter(fn x -> x in user.following end)
+ |> ActivityPub.fetch_activities_bounded(following, params)
+ |> Enum.reverse()
+
+ render(conn, "index.json", activities: activities, for: user, as: :activity)
+ else
+ _e -> render_error(conn, :forbidden, "Error.")
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index a23aeea9b..99169ef95 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -11,15 +11,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MediaProxy
- def render("accounts.json", %{users: users} = opts) do
+ def render("index.json", %{users: users} = opts) do
users
- |> render_many(AccountView, "account.json", opts)
+ |> render_many(AccountView, "show.json", opts)
|> Enum.filter(&Enum.any?/1)
end
- def render("account.json", %{user: user} = opts) do
+ def render("show.json", %{user: user} = opts) do
if User.visible_for?(user, opts[:for]),
- do: do_render("account.json", opts),
+ do: do_render("show.json", opts),
else: %{}
end
@@ -66,7 +66,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
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)
image = User.avatar_url(user) |> MediaProxy.url()
@@ -166,6 +166,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_settings_store(user, opts[:for], opts)
|> maybe_put_chat_token(user, opts[:for], opts)
|> maybe_put_activation_status(user, opts[:for])
+ |> maybe_put_follow_requests_count(user, opts[:for])
end
defp username_from_nickname(string) when is_binary(string) do
@@ -174,6 +175,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp username_from_nickname(_), do: nil
+ defp maybe_put_follow_requests_count(
+ data,
+ %User{id: user_id} = user,
+ %User{id: user_id}
+ ) do
+ count =
+ User.get_follow_requests(user)
+ |> length()
+
+ data
+ |> Kernel.put_in([:follow_requests_count], count)
+ end
+
+ defp maybe_put_follow_requests_count(data, _, _), do: data
+
defp maybe_put_settings(
data,
%User{id: user_id} = user,
diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index 40acc07b3..e9d2735b3 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -11,6 +11,10 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
+ def render("participations.json", %{participations: participations, for: user}) do
+ render_many(participations, __MODULE__, "participation.json", as: :participation, for: user)
+ end
+
def render("participation.json", %{participation: participation, for: user}) do
participation = Repo.preload(participation, conversation: [], recipients: [])
@@ -23,25 +27,14 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
end
activity = Activity.get_by_id_with_object(last_activity_id)
-
- last_status = StatusView.render("status.json", %{activity: activity, for: user})
-
# Conversations return all users except the current user.
- users =
- participation.recipients
- |> Enum.reject(&(&1.id == user.id))
-
- accounts =
- AccountView.render("accounts.json", %{
- users: users,
- as: :user
- })
+ users = Enum.reject(participation.recipients, &(&1.id == user.id))
%{
id: participation.id |> to_string(),
- accounts: accounts,
+ accounts: render(AccountView, "index.json", users: users, as: :user),
unread: !participation.read,
- last_status: last_status
+ last_status: render(StatusView, "show.json", activity: activity, for: user)
}
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex
new file mode 100644
index 000000000..cb8688941
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
new file mode 100644
index 000000000..c4866e510
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/views/mastodon_view.ex b/lib/pleroma/web/mastodon_api/views/mastodon_view.ex
deleted file mode 100644
index 33b9a74be..000000000
--- a/lib/pleroma/web/mastodon_api/views/mastodon_view.ex
+++ /dev/null
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index ec8eadcaa..5e3dbe728 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -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("account.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("status.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("status.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("status.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
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
new file mode 100644
index 000000000..753039da3
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -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
diff --git a/lib/pleroma/web/mastodon_api/views/report_view.ex b/lib/pleroma/web/mastodon_api/views/report_view.ex
index a16e7ff10..9da2dd740 100644
--- a/lib/pleroma/web/mastodon_api/views/report_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/report_view.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.ReportView do
use Pleroma.Web, :view
- def render("report.json", %{activity: activity}) do
+ def render("show.json", %{activity: activity}) do
%{
id: to_string(activity.id),
action_taken: false
diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
index 0aae15ab9..fc042a276 100644
--- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
@@ -7,11 +7,10 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
alias Pleroma.ScheduledActivity
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{scheduled_activities: scheduled_activities}) do
- render_many(scheduled_activities, ScheduledActivityView, "show.json")
+ render_many(scheduled_activities, __MODULE__, "show.json")
end
def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do
@@ -24,12 +23,8 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
end
defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do
- try do
- attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
- Map.put(data, :media_attachments, attachments)
- rescue
- _ -> data
- end
+ attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
+ Map.put(data, :media_attachments, attachments)
end
defp with_media_attachments(data, _), do: data
@@ -45,13 +40,9 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
in_reply_to_id: params["in_reply_to_id"]
}
- data =
- if media_ids = params["media_ids"] do
- Map.put(data, :media_ids, media_ids)
- else
- data
- end
-
- data
+ case params["media_ids"] do
+ nil -> data
+ media_ids -> Map.put(data, :media_ids, media_ids)
+ end
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index ef796cddd..9b8dd3086 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -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
@@ -73,17 +74,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities)
+ opts = Map.put(opts, :replied_to_activities, replied_to_activities)
- opts.activities
- |> safe_render_many(
- StatusView,
- "status.json",
- Map.put(opts, :replied_to_activities, replied_to_activities)
- )
+ safe_render_many(opts.activities, StatusView, "show.json", opts)
end
def render(
- "status.json",
+ "show.json",
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
user = get_user(activity.data["actor"])
@@ -96,7 +93,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one()
- reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
+ reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
@@ -112,7 +109,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
id: to_string(activity.id),
uri: 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_account_id: nil,
reblog: reblogged,
@@ -128,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] || [],
@@ -144,7 +141,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
- def render("status.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
+ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity)
user = get_user(activity.data["actor"])
@@ -262,7 +259,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
id: to_string(activity.id),
uri: object.data["id"],
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_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
@@ -281,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: %{
@@ -303,7 +300,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
- def render("status.json", _) do
+ def render("show.json", _) do
nil
end
@@ -343,9 +340,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
- def render("card.json", _) do
- nil
- end
+ def render("card.json", _), do: nil
def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
@@ -374,73 +369,39 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
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
+ def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
+ object = Object.normalize(activity)
- 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
+ user = get_user(activity.data["actor"])
+ created_at = Utils.to_masto_date(activity.data["published"])
+
+ %{
+ id: activity.id,
+ account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+ created_at: created_at,
+ title: object.data["title"] |> HTML.strip_tags(),
+ artist: object.data["artist"] |> HTML.strip_tags(),
+ album: object.data["album"] |> HTML.strip_tags(),
+ length: object.data["length"]
+ }
+ end
+
+ def render("listens.json", opts) do
+ safe_render_many(opts.activities, StatusView, "listen.json", opts)
+ end
+
+ def render("context.json", %{activity: activity, activities: activities, user: user}) do
+ %{ancestors: ancestors, descendants: descendants} =
+ activities
+ |> Enum.reverse()
+ |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
+ |> Map.put_new(:ancestors, [])
+ |> Map.put_new(:descendants, [])
+
+ %{
+ ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
+ descendants: render("index.json", for: user, activities: descendants, as: :activity)
+ }
end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex
index ddcdb1871..cc3fb1ce5 100644
--- a/lib/pleroma/web/oauth/app.ex
+++ b/lib/pleroma/web/oauth/app.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.OAuth.App do
use Ecto.Schema
import Ecto.Changeset
+ alias Pleroma.Repo
@type t :: %__MODULE__{}
@@ -39,4 +40,29 @@ defmodule Pleroma.Web.OAuth.App do
changeset
end
end
+
+ @doc """
+ Gets app by attrs or create new with attrs.
+ And updates the scopes if need.
+ """
+ @spec get_or_make(map(), list(String.t())) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
+ def get_or_make(attrs, scopes) do
+ with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do
+ update_scopes(app, scopes)
+ else
+ _e ->
+ %__MODULE__{}
+ |> register_changeset(Map.put(attrs, :scopes, scopes))
+ |> Repo.insert()
+ end
+ end
+
+ defp update_scopes(%__MODULE__{} = app, []), do: {:ok, app}
+ defp update_scopes(%__MODULE__{scopes: scopes} = app, scopes), do: {:ok, app}
+
+ defp update_scopes(%__MODULE__{} = app, scopes) do
+ app
+ |> change(%{scopes: scopes})
+ |> Repo.update()
+ end
end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index a57670e02..1cd7294e7 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -212,13 +212,31 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:auth_active, false} ->
# Per https://github.com/tootsuite/mastodon/blob/
# 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} ->
- 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} ->
- render_error(conn, :forbidden, "Password reset is required")
+ render_error(
+ conn,
+ :forbidden,
+ "Password reset is required",
+ %{},
+ "password_reset_required"
+ )
_error ->
render_invalid_credentials_error(conn)
@@ -442,7 +460,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
# Special case: Local MastodonFE
- defp redirect_uri(%Plug.Conn{} = conn, "."), do: mastodon_api_url(conn, :login)
+ defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
new file mode 100644
index 000000000..63c44086c
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -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
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
new file mode 100644
index 000000000..7f6a76c0e
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -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
diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
new file mode 100644
index 000000000..0fb978c5d
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -0,0 +1,52 @@
+# 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.ScrobbleController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2]
+
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
+ params =
+ if !params["length"] do
+ params
+ else
+ params
+ |> Map.put("length", fetch_integer_param(params, "length"))
+ end
+
+ with {:ok, activity} <- CommonAPI.listen(user, params) do
+ conn
+ |> put_view(StatusView)
+ |> render("listen.json", %{activity: activity, for: user})
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => message})
+ end
+ end
+
+ def user_scrobbles(%{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, "type", ["Listen"])
+
+ activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params)
+
+ conn
+ |> add_link_headers(activities)
+ |> put_view(StatusView)
+ |> render("listens.json", %{
+ activities: activities,
+ for: reading_user,
+ as: :activity
+ })
+ end
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 316c895ee..f91af8137 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -287,19 +287,50 @@ defmodule Pleroma.Web.Router do
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
- pipe_through(:authenticated_api)
-
scope [] do
+ pipe_through(:authenticated_api)
pipe_through(:oauth_read)
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
end
scope [] do
+ pipe_through(:authenticated_api)
pipe_through(:oauth_write)
patch("/conversations/:id", PleromaAPIController, :update_conversation)
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
+
+ scope [] do
+ pipe_through(:api)
+ pipe_through(:oauth_read_or_public)
+ get("/accounts/:id/favourites", AccountController, :favourites)
+ 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
+
+ scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
+ pipe_through([:api, :oauth_read_or_public])
+
+ get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
@@ -308,19 +339,19 @@ defmodule Pleroma.Web.Router do
scope [] do
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("/follow_requests", MastodonAPIController, :follow_requests)
+ get("/follow_requests", FollowRequestController, :index)
get("/blocks", MastodonAPIController, :blocks)
get("/mutes", MastodonAPIController, :mutes)
- get("/timelines/home", MastodonAPIController, :home_timeline)
- get("/timelines/direct", MastodonAPIController, :dm_timeline)
+ get("/timelines/home", TimelineController, :home)
+ get("/timelines/direct", TimelineController, :direct)
get("/favourites", MastodonAPIController, :favourites)
get("/bookmarks", MastodonAPIController, :bookmarks)
@@ -331,21 +362,21 @@ defmodule Pleroma.Web.Router do
post("/notifications/dismiss", NotificationController, :dismiss)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
- get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
- get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
+ get("/scheduled_statuses", ScheduledActivityController, :index)
+ get("/scheduled_statuses/:id", ScheduledActivityController, :show)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
- get("/domain_blocks", MastodonAPIController, :domain_blocks)
+ get("/domain_blocks", DomainBlockController, :index)
- get("/filters", MastodonAPIController, :get_filters)
+ get("/filters", FilterController, :index)
- get("/suggestions", MastodonAPIController, :suggestions)
+ get("/suggestions", SuggestionController, :index)
- get("/conversations", MastodonAPIController, :conversations)
- post("/conversations/:id/read", MastodonAPIController, :conversation_read)
+ get("/conversations", ConversationController, :index)
+ post("/conversations/:id/read", ConversationController, :read)
get("/endorsements", MastodonAPIController, :empty_array)
end
@@ -353,29 +384,29 @@ defmodule Pleroma.Web.Router do
scope [] do
pipe_through(:oauth_write)
- patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
+ patch("/accounts/update_credentials", AccountController, :update_credentials)
- post("/statuses", MastodonAPIController, :post_status)
- delete("/statuses/:id", MastodonAPIController, :delete_status)
+ post("/statuses", StatusController, :create)
+ delete("/statuses/:id", StatusController, :delete)
- post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
- post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
- post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
- post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
- post("/statuses/:id/pin", MastodonAPIController, :pin_status)
- post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
- post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
- post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
- post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
- post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
+ post("/statuses/:id/reblog", StatusController, :reblog)
+ post("/statuses/:id/unreblog", StatusController, :unreblog)
+ post("/statuses/:id/favourite", StatusController, :favourite)
+ post("/statuses/:id/unfavourite", StatusController, :unfavourite)
+ post("/statuses/:id/pin", StatusController, :pin)
+ post("/statuses/:id/unpin", StatusController, :unpin)
+ post("/statuses/:id/bookmark", StatusController, :bookmark)
+ post("/statuses/:id/unbookmark", StatusController, :unbookmark)
+ post("/statuses/:id/mute", StatusController, :mute_conversation)
+ post("/statuses/:id/unmute", StatusController, :unmute_conversation)
- put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
- delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
+ put("/scheduled_statuses/:id", ScheduledActivityController, :update)
+ delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
- post("/polls/:id/votes", MastodonAPIController, :poll_vote)
+ post("/polls/:id/votes", PollController, :vote)
- post("/media", MastodonAPIController, :upload)
- put("/media/:id", MastodonAPIController, :update_media)
+ post("/media", MediaController, :create)
+ put("/media/:id", MediaController, :update)
delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create)
@@ -384,41 +415,30 @@ defmodule Pleroma.Web.Router do
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
- post("/filters", MastodonAPIController, :create_filter)
- get("/filters/:id", MastodonAPIController, :get_filter)
- put("/filters/:id", MastodonAPIController, :update_filter)
- delete("/filters/:id", MastodonAPIController, :delete_filter)
+ post("/filters", FilterController, :create)
+ get("/filters/:id", FilterController, :show)
+ put("/filters/:id", FilterController, :update)
+ 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", MastodonAPIController, :reports)
+ post("/reports", ReportController, :create)
end
scope [] do
pipe_through(:oauth_follow)
- post("/follows", MastodonAPIController, :follow)
- post("/accounts/:id/follow", MastodonAPIController, :follow)
-
- post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
- post("/accounts/:id/block", MastodonAPIController, :block)
- post("/accounts/:id/unblock", MastodonAPIController, :unblock)
- post("/accounts/:id/mute", MastodonAPIController, :mute)
- post("/accounts/:id/unmute", MastodonAPIController, :unmute)
-
- post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
- post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
+ post("/follows", MastodonAPIController, :follows)
+ post("/accounts/:id/follow", AccountController, :follow)
+ post("/accounts/:id/unfollow", AccountController, :unfollow)
+ post("/accounts/:id/block", AccountController, :block)
+ post("/accounts/:id/unblock", AccountController, :unblock)
+ post("/accounts/:id/mute", AccountController, :mute)
+ post("/accounts/:id/unmute", AccountController, :unmute)
- post("/domain_blocks", MastodonAPIController, :block_domain)
- delete("/domain_blocks", MastodonAPIController, :unblock_domain)
+ post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
+ post("/follow_requests/:id/reject", FollowRequestController, :reject)
- post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
- post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
+ post("/domain_blocks", DomainBlockController, :create)
+ delete("/domain_blocks", DomainBlockController, :delete)
end
scope [] do
@@ -431,59 +451,51 @@ defmodule Pleroma.Web.Router do
end
end
- scope "/api/web", Pleroma.Web.MastodonAPI do
+ scope "/api/web", Pleroma.Web do
pipe_through([:authenticated_api, :oauth_write])
- put("/settings", MastodonAPIController, :put_settings)
+ put("/settings", MastoFEController, :put_settings)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
- post("/accounts", MastodonAPIController, :account_register)
-
- 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)
+ post("/accounts", AccountController, :create)
+ get("/accounts/search", SearchController, :account_search)
- get("/statuses/:id/card", MastodonAPIController, :status_card)
+ get("/instance", InstanceController, :show)
+ get("/instance/peers", InstanceController, :peers)
- get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
- get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
+ post("/apps", AppController, :create)
+ get("/apps/verify_credentials", AppController, :verify_credentials)
- get("/trends", MastodonAPIController, :empty_array)
+ get("/statuses/:id/card", StatusController, :card)
+ get("/statuses/:id/favourited_by", StatusController, :favourited_by)
+ get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
- get("/accounts/search", SearchController, :account_search)
+ get("/custom_emojis", CustomEmojiController, :index)
- post(
- "/pleroma/accounts/confirmation_resend",
- MastodonAPIController,
- :account_confirmation_resend
- )
+ get("/trends", MastodonAPIController, :empty_array)
scope [] do
pipe_through(:oauth_read_or_public)
- get("/timelines/public", MastodonAPIController, :public_timeline)
- get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
- get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
+ get("/timelines/public", TimelineController, :public)
+ get("/timelines/tag/:tag", TimelineController, :hashtag)
+ get("/timelines/list/:list_id", TimelineController, :list)
- get("/statuses", MastodonAPIController, :get_statuses)
- get("/statuses/:id", MastodonAPIController, :get_status)
- get("/statuses/:id/context", MastodonAPIController, :get_context)
+ get("/statuses", StatusController, :index)
+ get("/statuses/:id", StatusController, :show)
+ 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/followers", MastodonAPIController, :followers)
- get("/accounts/:id/following", MastodonAPIController, :following)
- get("/accounts/:id", MastodonAPIController, :user)
+ get("/accounts/:id/statuses", AccountController, :statuses)
+ get("/accounts/:id/followers", AccountController, :followers)
+ get("/accounts/:id/following", AccountController, :following)
+ get("/accounts/:id", AccountController, :show)
get("/search", SearchController, :search)
-
- get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
end
end
@@ -597,6 +609,7 @@ defmodule Pleroma.Web.Router do
scope [] do
pipe_through(:oauth_write)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
+ post("/api/ap/upload_media", ActivityPubController, :upload_media)
end
scope [] do
@@ -645,17 +658,17 @@ defmodule Pleroma.Web.Router do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
- scope "/", Pleroma.Web.MastodonAPI do
+ scope "/", Pleroma.Web do
pipe_through(:mastodon_html)
- get("/web/login", MastodonAPIController, :login)
- delete("/auth/sign_out", MastodonAPIController, :logout)
+ get("/web/login", MastodonAPI.AuthController, :login)
+ delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
- post("/auth/password", MastodonAPIController, :password_reset)
+ post("/auth/password", MastodonAPI.AuthController, :password_reset)
scope [] do
pipe_through(:oauth_read)
- get("/web/*path", MastodonAPIController, :index)
+ get("/web/*path", MastoFEController, :index)
end
end
diff --git a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex b/lib/pleroma/web/templates/masto_fe/index.html.eex
index 3325beca1..feff36fae 100644
--- a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex
+++ b/lib/pleroma/web/templates/masto_fe/index.html.eex
@@ -14,7 +14,7 @@
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/compose.js'>
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/home_timeline.js'>
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/notifications.js'>
-<script id='initial-state' type='application/json'><%= raw @initial_state %></script>
+<script id='initial-state' type='application/json'><%= initial_state(@token, @user, @custom_emojis) %></script>
<script src="/packs/core/common.js"></script>
<link rel="stylesheet" media="all" href="/packs/core/common.css" />
diff --git a/lib/pleroma/web/translation_helpers.ex b/lib/pleroma/web/translation_helpers.ex
index 8f5a43bf6..a104ea6b8 100644
--- a/lib/pleroma/web/translation_helpers.ex
+++ b/lib/pleroma/web/translation_helpers.ex
@@ -3,15 +3,27 @@
# SPDX-License-Identifier: AGPL-3.0-only
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
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)
|> Plug.Conn.put_status(unquote(status))
- |> Phoenix.Controller.json(%{
- error: Pleroma.Web.Gettext.dgettext("errors", unquote(msgid), unquote(bindings))
- })
+ |> Phoenix.Controller.json(error_map)
end
end
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 8eda762c7..bfd838902 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])
# true if captcha is disabled or enabled and valid, false otherwise
captcha_ok =
- if !captcha_enabled do
+ if not captcha_enabled do
:ok
else
Pleroma.Captcha.validate(
diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex
new file mode 100644
index 000000000..21b086d4c
--- /dev/null
+++ b/lib/pleroma/web/views/masto_fe_view.ex
@@ -0,0 +1,102 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastoFEView do
+ use Pleroma.Web, :view
+ alias Pleroma.Config
+ alias Pleroma.User
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.CustomEmojiView
+
+ @default_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
+ }
+ }
+ }
+
+ def initial_state(token, user, custom_emojis) do
+ limit = Config.get([:instance, :limit])
+
+ %{
+ 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 || @default_settings,
+ push_subscription: nil,
+ accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)},
+ custom_emojis: render(CustomEmojiView, "index.json", custom_emojis: custom_emojis),
+ char_limit: limit
+ }
+ |> Jason.encode!()
+ |> Phoenix.HTML.raw()
+ end
+
+ defp present?(nil), do: false
+ defp present?(false), do: false
+ defp present?(_), do: true
+end
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index b13030fa0..a9f14d09a 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.StreamerView do
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
- "status.json",
+ "show.json",
activity: activity,
for: user
)
@@ -43,7 +43,7 @@ defmodule Pleroma.Web.StreamerView do
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
- "status.json",
+ "show.json",
activity: activity
)
|> Jason.encode!()