aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/pleroma/app.ex49
-rw-r--r--lib/pleroma/activity.ex38
-rw-r--r--lib/pleroma/config/loader.ex2
-rw-r--r--lib/pleroma/config/transfer_task.ex29
-rw-r--r--lib/pleroma/emoji/formatter.ex14
-rw-r--r--lib/pleroma/emoji/pack.ex507
-rw-r--r--lib/pleroma/following_relationship.ex67
-rw-r--r--lib/pleroma/formatter.ex7
-rw-r--r--lib/pleroma/notification.ex72
-rw-r--r--lib/pleroma/object.ex5
-rw-r--r--lib/pleroma/plugs/authentication_plug.ex6
-rw-r--r--lib/pleroma/plugs/ensure_authenticated_plug.ex8
-rw-r--r--lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex6
-rw-r--r--lib/pleroma/plugs/expect_authenticated_check_plug.ex20
-rw-r--r--lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex21
-rw-r--r--lib/pleroma/plugs/http_security_plug.ex2
-rw-r--r--lib/pleroma/plugs/legacy_authentication_plug.ex3
-rw-r--r--lib/pleroma/plugs/oauth_scopes_plug.ex28
-rw-r--r--lib/pleroma/plugs/plug_helper.ex40
-rw-r--r--lib/pleroma/stats.ex18
-rw-r--r--lib/pleroma/tests/auth_test_controller.ex93
-rw-r--r--lib/pleroma/user.ex238
-rw-r--r--lib/pleroma/user/query.ex4
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex135
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex7
-rw-r--r--lib/pleroma/web/activity_pub/mrf/object_age_policy.ex10
-rw-r--r--lib/pleroma/web/activity_pub/mrf/simple_policy.ex15
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/note_validator.ex1
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/object_id.ex12
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/uri.ex20
-rw-r--r--lib/pleroma/web/activity_pub/publisher.ex13
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex15
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex16
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex7
-rw-r--r--lib/pleroma/web/admin_api/admin_api_controller.ex150
-rw-r--r--lib/pleroma/web/admin_api/views/status_view.ex18
-rw-r--r--lib/pleroma/web/api_spec.ex8
-rw-r--r--lib/pleroma/web/api_spec/helpers.ex22
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex702
-rw-r--r--lib/pleroma/web/api_spec/operations/app_operation.ex66
-rw-r--r--lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex88
-rw-r--r--lib/pleroma/web/api_spec/operations/domain_block_operation.ex31
-rw-r--r--lib/pleroma/web/api_spec/render_error.ex231
-rw-r--r--lib/pleroma/web/api_spec/schemas/account.ex167
-rw-r--r--lib/pleroma/web/api_spec/schemas/account_field.ex26
-rw-r--r--lib/pleroma/web/api_spec/schemas/account_relationship.ex44
-rw-r--r--lib/pleroma/web/api_spec/schemas/actor_type.ex13
-rw-r--r--lib/pleroma/web/api_spec/schemas/api_error.ex (renamed from lib/pleroma/web/api_spec/schemas/domain_block_request.ex)13
-rw-r--r--lib/pleroma/web/api_spec/schemas/app_create_request.ex33
-rw-r--r--lib/pleroma/web/api_spec/schemas/app_create_response.ex33
-rw-r--r--lib/pleroma/web/api_spec/schemas/boolean_like.ex36
-rw-r--r--lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex16
-rw-r--r--lib/pleroma/web/api_spec/schemas/emoji.ex29
-rw-r--r--lib/pleroma/web/api_spec/schemas/flake_id.ex14
-rw-r--r--lib/pleroma/web/api_spec/schemas/poll.ex36
-rw-r--r--lib/pleroma/web/api_spec/schemas/status.ex226
-rw-r--r--lib/pleroma/web/api_spec/schemas/visibility_scope.ex14
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex16
-rw-r--r--lib/pleroma/web/common_api/common_api.ex45
-rw-r--r--lib/pleroma/web/common_api/utils.ex35
-rw-r--r--lib/pleroma/web/controller_helper.ex5
-rw-r--r--lib/pleroma/web/fallback_redirect_controller.ex2
-rw-r--r--lib/pleroma/web/federator/federator.ex13
-rw-r--r--lib/pleroma/web/feed/feed_view.ex9
-rw-r--r--lib/pleroma/web/masto_fe_controller.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex152
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/app_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/auth_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/instance_controller.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/marker_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex20
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/media_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/poll_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/report_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex9
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex34
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex9
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex19
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/views/app_view.ex15
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/views/poll_view.ex7
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex22
-rw-r--r--lib/pleroma/web/mastodon_api/views/subscription_view.ex (renamed from lib/pleroma/web/mastodon_api/views/push_subscription_view.ex)4
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy_controller.ex1
-rw-r--r--lib/pleroma/web/mongooseim/mongoose_im_controller.ex4
-rw-r--r--lib/pleroma/web/oauth/app.ex82
-rw-r--r--lib/pleroma/web/oauth/oauth_controller.ex3
-rw-r--r--lib/pleroma/web/oauth/scopes.ex6
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/account_controller.ex29
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex695
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex22
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex8
-rw-r--r--lib/pleroma/web/push/impl.ex80
-rw-r--r--lib/pleroma/web/push/subscription.ex8
-rw-r--r--lib/pleroma/web/router.ex339
-rw-r--r--lib/pleroma/web/static_fe/static_fe_view.ex9
-rw-r--r--lib/pleroma/web/streamer/worker.ex18
-rw-r--r--lib/pleroma/web/templates/feed/feed/_activity.atom.eex8
-rw-r--r--lib/pleroma/web/templates/feed/feed/_activity.rss.eex8
-rw-r--r--lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex16
-rw-r--r--lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex15
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex6
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex10
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex4
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex2
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex33
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex107
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api_controller.ex24
-rw-r--r--lib/pleroma/web/web.ex124
-rw-r--r--lib/pleroma/workers/background_worker.ex4
121 files changed, 4155 insertions, 1560 deletions
diff --git a/lib/mix/tasks/pleroma/app.ex b/lib/mix/tasks/pleroma/app.ex
new file mode 100644
index 000000000..463e2449f
--- /dev/null
+++ b/lib/mix/tasks/pleroma/app.ex
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.App do
+ @moduledoc File.read!("docs/administration/CLI_tasks/oauth_app.md")
+ use Mix.Task
+
+ import Mix.Pleroma
+
+ @shortdoc "Creates trusted OAuth App"
+
+ def run(["create" | options]) do
+ start_pleroma()
+
+ {opts, _} =
+ OptionParser.parse!(options,
+ strict: [name: :string, redirect_uri: :string, scopes: :string],
+ aliases: [n: :name, r: :redirect_uri, s: :scopes]
+ )
+
+ scopes =
+ if opts[:scopes] do
+ String.split(opts[:scopes], ",")
+ else
+ ["read", "write", "follow", "push"]
+ end
+
+ params = %{
+ client_name: opts[:name],
+ redirect_uris: opts[:redirect_uri],
+ trusted: true,
+ scopes: scopes
+ }
+
+ with {:ok, app} <- Pleroma.Web.OAuth.App.create(params) do
+ shell_info("#{app.client_name} successfully created:")
+ shell_info("App client_id: " <> app.client_id)
+ shell_info("App client_secret: " <> app.client_secret)
+ else
+ {:error, changeset} ->
+ shell_error("Creating failed:")
+
+ Enum.each(Pleroma.Web.OAuth.App.errors(changeset), fn {key, error} ->
+ shell_error("#{key}: #{error}")
+ end)
+ end
+ end
+end
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 5a8329e69..6213d0eb7 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -27,17 +27,13 @@ defmodule Pleroma.Activity do
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@mastodon_notification_types %{
"Create" => "mention",
- "Follow" => "follow",
+ "Follow" => ["follow", "follow_request"],
"Announce" => "reblog",
"Like" => "favourite",
"Move" => "move",
"EmojiReact" => "pleroma:emoji_reaction"
}
- @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
- into: %{},
- do: {v, k}
-
schema "activities" do
field(:data, :map)
field(:local, :boolean, default: true)
@@ -291,15 +287,43 @@ defmodule Pleroma.Activity do
defp purge_web_resp_cache(nil), do: nil
- for {ap_type, type} <- @mastodon_notification_types do
+ def follow_accepted?(
+ %Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
+ ) do
+ with %User{} = follower <- Activity.user_actor(activity),
+ %User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
+ Pleroma.FollowingRelationship.following?(follower, followed)
+ else
+ _ -> false
+ end
+ end
+
+ def follow_accepted?(_), do: false
+
+ @spec mastodon_notification_type(Activity.t()) :: String.t() | nil
+
+ for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
do: unquote(type)
end
+ def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
+ if follow_accepted?(activity) do
+ "follow"
+ else
+ "follow_request"
+ end
+ end
+
def mastodon_notification_type(%Activity{}), do: nil
+ @spec from_mastodon_notification_type(String.t()) :: String.t() | nil
+ @doc "Converts Mastodon notification type to AR activity type"
def from_mastodon_notification_type(type) do
- Map.get(@mastodon_to_ap_notification_types, type)
+ with {k, _v} <-
+ Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
+ k
+ end
end
def all_by_actor_and_id(actor, status_ids \\ [])
diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex
index 6ca6550bd..0f3ecf1ed 100644
--- a/lib/pleroma/config/loader.ex
+++ b/lib/pleroma/config/loader.ex
@@ -47,7 +47,7 @@ defmodule Pleroma.Config.Loader do
@spec filter_group(atom(), keyword()) :: keyword()
def filter_group(group, configs) do
Enum.reject(configs[group], fn {key, _v} ->
- key in @reject_keys or (group == :phoenix and key == :serve_endpoints)
+ key in @reject_keys or (group == :phoenix and key == :serve_endpoints) or group == :postgrex
end)
end
end
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
index 3871e1cbb..c02b70e96 100644
--- a/lib/pleroma/config/transfer_task.ex
+++ b/lib/pleroma/config/transfer_task.ex
@@ -46,14 +46,6 @@ defmodule Pleroma.Config.TransferTask do
with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do
# We need to restart applications for loaded settings take effect
- # TODO: some problem with prometheus after restart!
- reject_restart =
- if restart_pleroma? do
- [nil, :prometheus]
- else
- [:pleroma, nil, :prometheus]
- end
-
{logger, other} =
(Repo.all(ConfigDB) ++ deleted_settings)
|> Enum.map(&transform_and_merge/1)
@@ -65,10 +57,20 @@ defmodule Pleroma.Config.TransferTask do
started_applications = Application.started_applications()
+ # TODO: some problem with prometheus after restart!
+ reject = [nil, :prometheus, :postgrex]
+
+ reject =
+ if restart_pleroma? do
+ reject
+ else
+ [:pleroma | reject]
+ end
+
other
|> Enum.map(&update/1)
|> Enum.uniq()
- |> Enum.reject(&(&1 in reject_restart))
+ |> Enum.reject(&(&1 in reject))
|> maybe_set_pleroma_last()
|> Enum.each(&restart(started_applications, &1, Config.get(:env)))
@@ -122,7 +124,7 @@ defmodule Pleroma.Config.TransferTask do
:ok = update_env(:logger, :backends, merged)
end
- defp configure({group, key, _, merged}) do
+ defp configure({_, key, _, merged}) when key in [:console, :ex_syslogger] do
merged =
if key == :console do
put_in(merged[:format], merged[:format] <> "\n")
@@ -136,7 +138,12 @@ defmodule Pleroma.Config.TransferTask do
else: key
Logger.configure_backend(backend, merged)
- :ok = update_env(:logger, group, merged)
+ :ok = update_env(:logger, key, merged)
+ end
+
+ defp configure({_, key, _, merged}) do
+ Logger.configure([{key, merged}])
+ :ok = update_env(:logger, key, merged)
end
defp update({group, key, value, merged}) do
diff --git a/lib/pleroma/emoji/formatter.ex b/lib/pleroma/emoji/formatter.ex
index 59ff2cac3..dc45b8a38 100644
--- a/lib/pleroma/emoji/formatter.ex
+++ b/lib/pleroma/emoji/formatter.ex
@@ -38,22 +38,14 @@ defmodule Pleroma.Emoji.Formatter do
def demojify(text, nil), do: text
- @doc "Outputs a list of the emoji-shortcodes in a text"
- def get_emoji(text) when is_binary(text) do
- Enum.filter(Emoji.get_all(), fn {emoji, %Emoji{}} ->
- String.contains?(text, ":#{emoji}:")
- end)
- end
-
- def get_emoji(_), do: []
-
@doc "Outputs a list of the emoji-Maps in a text"
def get_emoji_map(text) when is_binary(text) do
- get_emoji(text)
+ Emoji.get_all()
+ |> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end)
|> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end
- def get_emoji_map(_), do: []
+ def get_emoji_map(_), do: %{}
end
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
new file mode 100644
index 000000000..242344374
--- /dev/null
+++ b/lib/pleroma/emoji/pack.ex
@@ -0,0 +1,507 @@
+defmodule Pleroma.Emoji.Pack do
+ @derive {Jason.Encoder, only: [:files, :pack]}
+ defstruct files: %{},
+ pack_file: nil,
+ path: nil,
+ pack: %{},
+ name: nil
+
+ @type t() :: %__MODULE__{
+ files: %{String.t() => Path.t()},
+ pack_file: Path.t(),
+ path: Path.t(),
+ pack: map(),
+ name: String.t()
+ }
+
+ alias Pleroma.Emoji
+
+ @spec emoji_path() :: Path.t()
+ def emoji_path do
+ static = Pleroma.Config.get!([:instance, :static_dir])
+ Path.join(static, "emoji")
+ end
+
+ @spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values}
+ def create(name) when byte_size(name) > 0 do
+ dir = Path.join(emoji_path(), name)
+
+ with :ok <- File.mkdir(dir) do
+ %__MODULE__{
+ pack_file: Path.join(dir, "pack.json")
+ }
+ |> save_pack()
+ end
+ end
+
+ def create(_), do: {:error, :empty_values}
+
+ @spec show(String.t()) :: {:ok, t()} | {:loaded, nil} | {:error, :empty_values}
+ def show(name) when byte_size(name) > 0 do
+ with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
+ {_, pack} <- validate_pack(pack) do
+ {:ok, pack}
+ end
+ end
+
+ def show(_), do: {:error, :empty_values}
+
+ @spec delete(String.t()) ::
+ {:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
+ def delete(name) when byte_size(name) > 0 do
+ emoji_path()
+ |> Path.join(name)
+ |> File.rm_rf()
+ end
+
+ def delete(_), do: {:error, :empty_values}
+
+ @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) ::
+ {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
+ def add_file(name, shortcode, filename, file)
+ when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(filename) > 0 do
+ with {_, nil} <- {:exists, Emoji.get(shortcode)},
+ {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)} do
+ file_path = Path.join(pack.path, filename)
+
+ create_subdirs(file_path)
+
+ case file do
+ %Plug.Upload{path: upload_path} ->
+ # Copy the uploaded file from the temporary directory
+ File.copy!(upload_path, file_path)
+
+ url when is_binary(url) ->
+ # Download and write the file
+ file_contents = Tesla.get!(url).body
+ File.write!(file_path, file_contents)
+ end
+
+ files = Map.put(pack.files, shortcode, filename)
+
+ updated_pack = %{pack | files: files}
+
+ case save_pack(updated_pack) do
+ :ok ->
+ Emoji.reload()
+ {:ok, updated_pack}
+
+ e ->
+ e
+ end
+ end
+ end
+
+ def add_file(_, _, _, _), do: {:error, :empty_values}
+
+ defp create_subdirs(file_path) do
+ if String.contains?(file_path, "/") do
+ file_path
+ |> Path.dirname()
+ |> File.mkdir_p!()
+ end
+ end
+
+ @spec delete_file(String.t(), String.t()) ::
+ {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
+ def delete_file(name, shortcode) when byte_size(name) > 0 and byte_size(shortcode) > 0 do
+ with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
+ {_, {filename, files}} when not is_nil(filename) <-
+ {:exists, Map.pop(pack.files, shortcode)},
+ emoji <- Path.join(pack.path, filename),
+ {_, true} <- {:exists, File.exists?(emoji)} do
+ emoji_dir = Path.dirname(emoji)
+
+ File.rm!(emoji)
+
+ if String.contains?(filename, "/") and File.ls!(emoji_dir) == [] do
+ File.rmdir!(emoji_dir)
+ end
+
+ updated_pack = %{pack | files: files}
+
+ case save_pack(updated_pack) do
+ :ok ->
+ Emoji.reload()
+ {:ok, updated_pack}
+
+ e ->
+ e
+ end
+ end
+ end
+
+ def delete_file(_, _), do: {:error, :empty_values}
+
+ @spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) ::
+ {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
+ def update_file(name, shortcode, new_shortcode, new_filename, force)
+ when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(new_shortcode) > 0 and
+ byte_size(new_filename) > 0 do
+ with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
+ {_, {filename, files}} when not is_nil(filename) <-
+ {:exists, Map.pop(pack.files, shortcode)},
+ {_, true} <- {:not_used, force or is_nil(Emoji.get(new_shortcode))} do
+ old_path = Path.join(pack.path, filename)
+ old_dir = Path.dirname(old_path)
+ new_path = Path.join(pack.path, new_filename)
+
+ create_subdirs(new_path)
+
+ :ok = File.rename(old_path, new_path)
+
+ if String.contains?(filename, "/") and File.ls!(old_dir) == [] do
+ File.rmdir!(old_dir)
+ end
+
+ files = Map.put(files, new_shortcode, new_filename)
+
+ updated_pack = %{pack | files: files}
+
+ case save_pack(updated_pack) do
+ :ok ->
+ Emoji.reload()
+ {:ok, updated_pack}
+
+ e ->
+ e
+ end
+ end
+ end
+
+ def update_file(_, _, _, _, _), do: {:error, :empty_values}
+
+ @spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, atom()}
+ def import_from_filesystem do
+ emoji_path = emoji_path()
+
+ with {:ok, %{access: :read_write}} <- File.stat(emoji_path),
+ {:ok, results} <- File.ls(emoji_path) do
+ names =
+ results
+ |> Enum.map(&Path.join(emoji_path, &1))
+ |> Enum.reject(fn path ->
+ File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
+ end)
+ |> Enum.map(&write_pack_contents/1)
+ |> Enum.filter(& &1)
+
+ {:ok, names}
+ else
+ {:ok, %{access: _}} -> {:error, :no_read_write}
+ e -> e
+ end
+ end
+
+ defp write_pack_contents(path) do
+ pack = %__MODULE__{
+ files: files_from_path(path),
+ path: path,
+ pack_file: Path.join(path, "pack.json")
+ }
+
+ case save_pack(pack) do
+ :ok -> Path.basename(path)
+ _ -> nil
+ end
+ end
+
+ defp files_from_path(path) do
+ txt_path = Path.join(path, "emoji.txt")
+
+ if File.exists?(txt_path) do
+ # There's an emoji.txt file, it's likely from a pack installed by the pack manager.
+ # Make a pack.json file from the contents of that emoji.txt file
+
+ # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
+
+ # Create a map of shortcodes to filenames from emoji.txt
+ File.read!(txt_path)
+ |> String.split("\n")
+ |> Enum.map(&String.trim/1)
+ |> Enum.map(fn line ->
+ case String.split(line, ~r/,\s*/) do
+ # This matches both strings with and without tags
+ # and we don't care about tags here
+ [name, file | _] ->
+ file_dir_name = Path.dirname(file)
+
+ file =
+ if String.ends_with?(path, file_dir_name) do
+ Path.basename(file)
+ else
+ file
+ end
+
+ {name, file}
+
+ _ ->
+ nil
+ end
+ end)
+ |> Enum.filter(& &1)
+ |> Enum.into(%{})
+ else
+ # If there's no emoji.txt, assume all files
+ # that are of certain extensions from the config are emojis and import them all
+ pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
+ Emoji.Loader.make_shortcode_to_file_map(path, pack_extensions)
+ end
+ end
+
+ @spec list_remote(String.t()) :: {:ok, map()}
+ def list_remote(url) do
+ uri =
+ url
+ |> String.trim()
+ |> URI.parse()
+
+ with {_, true} <- {:shareable, shareable_packs_available?(uri)} do
+ packs =
+ uri
+ |> URI.merge("/api/pleroma/emoji/packs")
+ |> to_string()
+ |> Tesla.get!()
+ |> Map.get(:body)
+ |> Jason.decode!()
+
+ {:ok, packs}
+ end
+ end
+
+ @spec list_local() :: {:ok, map()}
+ def list_local do
+ emoji_path = emoji_path()
+
+ # Create the directory first if it does not exist. This is probably the first request made
+ # with the API so it should be sufficient
+ with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
+ {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
+ packs =
+ results
+ |> Enum.map(&load_pack/1)
+ |> Enum.filter(& &1)
+ |> Enum.map(&validate_pack/1)
+ |> Map.new()
+
+ {:ok, packs}
+ end
+ end
+
+ defp validate_pack(pack) do
+ if downloadable?(pack) do
+ archive = fetch_archive(pack)
+ archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
+
+ info =
+ pack.pack
+ |> Map.put("can-download", true)
+ |> Map.put("download-sha256", archive_sha)
+
+ {pack.name, Map.put(pack, :pack, info)}
+ else
+ info = Map.put(pack.pack, "can-download", false)
+ {pack.name, Map.put(pack, :pack, info)}
+ end
+ end
+
+ defp downloadable?(pack) do
+ # If the pack is set as shared, check if it can be downloaded
+ # That means that when asked, the pack can be packed and sent to the remote
+ # Otherwise, they'd have to download it from external-src
+ pack.pack["share-files"] &&
+ Enum.all?(pack.files, fn {_, file} ->
+ File.exists?(Path.join(pack.path, file))
+ end)
+ end
+
+ @spec get_archive(String.t()) :: {:ok, binary()}
+ def get_archive(name) do
+ with {_, %__MODULE__{} = pack} <- {:exists?, load_pack(name)},
+ {_, true} <- {:can_download?, downloadable?(pack)} do
+ {:ok, fetch_archive(pack)}
+ end
+ end
+
+ defp fetch_archive(pack) do
+ hash = :crypto.hash(:md5, File.read!(pack.pack_file))
+
+ case Cachex.get!(:emoji_packs_cache, pack.name) do
+ %{hash: ^hash, pack_data: archive} ->
+ archive
+
+ _ ->
+ create_archive_and_cache(pack, hash)
+ end
+ end
+
+ defp create_archive_and_cache(pack, hash) do
+ files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
+
+ {:ok, {_, result}} =
+ :zip.zip('#{pack.name}.zip', files, [:memory, cwd: to_charlist(pack.path)])
+
+ ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
+ overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files))
+
+ Cachex.put!(
+ :emoji_packs_cache,
+ pack.name,
+ # if pack.json MD5 changes, the cache is not valid anymore
+ %{hash: hash, pack_data: result},
+ # Add a minute to cache time for every file in the pack
+ ttl: overall_ttl
+ )
+
+ result
+ end
+
+ @spec download(String.t(), String.t(), String.t()) :: :ok
+ def download(name, url, as) do
+ uri =
+ url
+ |> String.trim()
+ |> URI.parse()
+
+ with {_, true} <- {:shareable, shareable_packs_available?(uri)} do
+ remote_pack =
+ uri
+ |> URI.merge("/api/pleroma/emoji/packs/#{name}")
+ |> to_string()
+ |> Tesla.get!()
+ |> Map.get(:body)
+ |> Jason.decode!()
+
+ result =
+ case remote_pack["pack"] do
+ %{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
+ {:ok,
+ %{
+ sha: sha,
+ url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string()
+ }}
+
+ %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
+ {:ok,
+ %{
+ sha: sha,
+ url: src,
+ fallback: true
+ }}
+
+ _ ->
+ {:error,
+ "The pack was not set as shared and there is no fallback src to download from"}
+ end
+
+ with {:ok, %{sha: sha, url: url} = pinfo} <- result,
+ %{body: archive} <- Tesla.get!(url),
+ {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, archive)} do
+ local_name = as || name
+
+ path = Path.join(emoji_path(), local_name)
+
+ pack = %__MODULE__{
+ name: local_name,
+ path: path,
+ files: remote_pack["files"],
+ pack_file: Path.join(path, "pack.json")
+ }
+
+ File.mkdir_p!(pack.path)
+
+ files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
+ # Fallback cannot contain a pack.json file
+ files = if pinfo[:fallback], do: files, else: ['pack.json' | files]
+
+ {:ok, _} = :zip.unzip(archive, cwd: to_charlist(pack.path), file_list: files)
+
+ # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
+ # in it to depend on itself
+ if pinfo[:fallback] do
+ save_pack(pack)
+ end
+
+ :ok
+ end
+ end
+ end
+
+ defp save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true))
+
+ @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
+ def save_metadata(metadata, %__MODULE__{} = pack) do
+ pack = Map.put(pack, :pack, metadata)
+
+ with :ok <- save_pack(pack) do
+ {:ok, pack}
+ end
+ end
+
+ @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
+ def update_metadata(name, data) do
+ pack = load_pack(name)
+
+ fb_sha_changed? =
+ not is_nil(data["fallback-src"]) and data["fallback-src"] != pack.pack["fallback-src"]
+
+ with {_, true} <- {:update?, fb_sha_changed?},
+ {:ok, %{body: zip}} <- Tesla.get(data["fallback-src"]),
+ {:ok, f_list} <- :zip.unzip(zip, [:memory]),
+ {_, true} <- {:has_all_files?, has_all_files?(pack.files, f_list)} do
+ fallback_sha = :crypto.hash(:sha256, zip) |> Base.encode16()
+
+ data
+ |> Map.put("fallback-src-sha256", fallback_sha)
+ |> save_metadata(pack)
+ else
+ {:update?, _} -> save_metadata(data, pack)
+ e -> e
+ end
+ end
+
+ # Check if all files from the pack.json are in the archive
+ defp has_all_files?(files, f_list) do
+ Enum.all?(files, fn {_, from_manifest} ->
+ List.keyfind(f_list, to_charlist(from_manifest), 0)
+ end)
+ end
+
+ @spec load_pack(String.t()) :: t() | nil
+ def load_pack(name) do
+ pack_file = Path.join([emoji_path(), name, "pack.json"])
+
+ if File.exists?(pack_file) do
+ pack_file
+ |> File.read!()
+ |> from_json()
+ |> Map.put(:pack_file, pack_file)
+ |> Map.put(:path, Path.dirname(pack_file))
+ |> Map.put(:name, name)
+ end
+ end
+
+ defp from_json(json) do
+ map = Jason.decode!(json)
+
+ struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
+ end
+
+ defp shareable_packs_available?(uri) do
+ uri
+ |> URI.merge("/.well-known/nodeinfo")
+ |> to_string()
+ |> Tesla.get!()
+ |> Map.get(:body)
+ |> Jason.decode!()
+ |> Map.get("links")
+ |> List.last()
+ |> Map.get("href")
+ # Get the actual nodeinfo address and fetch it
+ |> Tesla.get!()
+ |> Map.get(:body)
+ |> Jason.decode!()
+ |> get_in(["metadata", "features"])
+ |> Enum.member?("shareable_emoji_packs")
+ end
+end
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index 9ccf40495..3a3082e72 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -10,11 +10,12 @@ defmodule Pleroma.FollowingRelationship do
alias Ecto.Changeset
alias FlakeId.Ecto.CompatType
+ alias Pleroma.FollowingRelationship.State
alias Pleroma.Repo
alias Pleroma.User
schema "following_relationships" do
- field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending)
+ field(:state, State, default: :follow_pending)
belongs_to(:follower, User, type: CompatType)
belongs_to(:following, User, type: CompatType)
@@ -22,6 +23,11 @@ defmodule Pleroma.FollowingRelationship do
timestamps()
end
+ @doc "Returns underlying integer code for state atom"
+ def state_int_code(state_atom), do: State.__enum_map__() |> Keyword.fetch!(state_atom)
+
+ def accept_state_code, do: state_int_code(:follow_accept)
+
def changeset(%__MODULE__{} = following_relationship, attrs) do
following_relationship
|> cast(attrs, [:state])
@@ -82,6 +88,29 @@ defmodule Pleroma.FollowingRelationship do
|> Repo.aggregate(:count, :id)
end
+ def followers_query(%User{} = user) do
+ __MODULE__
+ |> join(:inner, [r], u in User, on: r.follower_id == u.id)
+ |> where([r], r.following_id == ^user.id)
+ |> where([r], r.state == ^:follow_accept)
+ end
+
+ def followers_ap_ids(%User{} = user, from_ap_ids \\ nil) do
+ query =
+ user
+ |> followers_query()
+ |> select([r, u], u.ap_id)
+
+ query =
+ if from_ap_ids do
+ where(query, [r, u], u.ap_id in ^from_ap_ids)
+ else
+ query
+ end
+
+ Repo.all(query)
+ end
+
def following_count(%User{id: nil}), do: 0
def following_count(%User{} = user) do
@@ -105,12 +134,16 @@ defmodule Pleroma.FollowingRelationship do
|> Repo.exists?()
end
+ def following_query(%User{} = user) do
+ __MODULE__
+ |> join(:inner, [r], u in User, on: r.following_id == u.id)
+ |> where([r], r.follower_id == ^user.id)
+ |> where([r], r.state == ^:follow_accept)
+ end
+
def following(%User{} = user) do
following =
- __MODULE__
- |> join(:inner, [r], u in User, on: r.following_id == u.id)
- |> where([r], r.follower_id == ^user.id)
- |> where([r], r.state == ^:follow_accept)
+ following_query(user)
|> select([r, u], u.follower_address)
|> Repo.all()
@@ -171,6 +204,30 @@ defmodule Pleroma.FollowingRelationship do
end)
end
+ @doc """
+ For a query with joined activity,
+ keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
+ """
+ def keep_following_or_not_domain_blocked(query, user) do
+ where(
+ query,
+ [_, activity],
+ fragment(
+ # "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
+ """
+ NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR
+ ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
+ ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
+ """,
+ activity.actor,
+ ^user.domain_blocks,
+ activity.actor,
+ ^User.binary_id(user.id),
+ ^accept_state_code()
+ )
+ )
+ end
+
defp validate_not_self_relationship(%Changeset{} = changeset) do
changeset
|> validate_follower_id_following_id_inequality()
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index c44e7fc8b..02a93a8dc 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -31,7 +31,7 @@ defmodule Pleroma.Formatter do
def mention_handler("@" <> nickname, buffer, opts, acc) do
case User.get_cached_by_nickname(nickname) do
%User{id: id} = user ->
- ap_id = get_ap_id(user)
+ user_url = user.uri || user.ap_id
nickname_text = get_nickname_text(nickname, opts)
link =
@@ -42,7 +42,7 @@ defmodule Pleroma.Formatter do
["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)],
"data-user": id,
class: "u-url mention",
- href: ap_id,
+ href: user_url,
rel: "ugc"
),
class: "h-card"
@@ -146,9 +146,6 @@ defmodule Pleroma.Formatter do
end
end
- defp get_ap_id(%User{source_data: %{"url" => url}}) when is_binary(url), do: url
- defp get_ap_id(%User{ap_id: ap_id}), do: ap_id
-
defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)
end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 3084bac3b..75c555021 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Notification do
alias Ecto.Multi
alias Pleroma.Activity
+ alias Pleroma.FollowingRelationship
alias Pleroma.Marker
alias Pleroma.Notification
alias Pleroma.Object
@@ -94,15 +95,13 @@ defmodule Pleroma.Notification do
|> exclude_visibility(opts)
end
+ # Excludes blocked users and non-followed domain-blocked users
defp exclude_blocked(query, user, opts) do
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
query
|> where([n, a], a.actor not in ^blocked_ap_ids)
- |> where(
- [n, a],
- fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
- )
+ |> FollowingRelationship.keep_following_or_not_domain_blocked(user)
end
defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
@@ -280,6 +279,16 @@ defmodule Pleroma.Notification do
|> Repo.delete_all()
end
+ def dismiss(%Pleroma.Activity{} = activity) do
+ Notification
+ |> where([n], n.activity_id == ^activity.id)
+ |> Repo.delete_all()
+ |> case do
+ {_, notifications} -> {:ok, notifications}
+ _ -> {:error, "Cannot dismiss notification"}
+ end
+ end
+
def dismiss(%{id: user_id} = _user, id) do
notification = Repo.get(Notification, id)
@@ -302,8 +311,17 @@ defmodule Pleroma.Notification do
end
end
+ def create_notifications(%Activity{data: %{"type" => "Follow"}} = activity) do
+ if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
+ Activity.follow_accepted?(activity) do
+ do_create_notifications(activity)
+ else
+ {:ok, []}
+ end
+ end
+
def create_notifications(%Activity{data: %{"type" => type}} = activity)
- when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do
+ when type in ["Like", "Announce", "Move", "EmojiReact"] do
do_create_notifications(activity)
end
@@ -342,10 +360,11 @@ defmodule Pleroma.Notification do
@doc """
Returns a tuple with 2 elements:
- {enabled notification receivers, currently disabled receivers (blocking / [thread] muting)}
+ {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
"""
+ @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
@@ -358,17 +377,14 @@ defmodule Pleroma.Notification do
|> Utils.maybe_notify_followers(activity)
|> Enum.uniq()
- # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs
+ potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only)
+
notification_enabled_ap_ids =
potential_receiver_ap_ids
+ |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
|> exclude_relationship_restricted_ap_ids(activity)
|> exclude_thread_muter_ap_ids(activity)
- potential_receivers =
- potential_receiver_ap_ids
- |> Enum.uniq()
- |> User.get_users_from_set(local_only)
-
notification_enabled_users =
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
@@ -377,6 +393,38 @@ defmodule Pleroma.Notification do
def get_notified_from_activity(_, _local_only), do: {[], []}
+ @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
+ def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
+
+ def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
+
+ def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
+ activity_actor_domain = activity.actor && URI.parse(activity.actor).host
+
+ users =
+ ap_ids
+ |> Enum.map(fn ap_id ->
+ Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
+ User.get_cached_by_ap_id(ap_id)
+ end)
+ |> Enum.filter(& &1)
+
+ domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
+
+ domain_blocker_follower_ap_ids =
+ if Enum.any?(domain_blocker_ap_ids) do
+ activity
+ |> Activity.user_actor()
+ |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
+ else
+ []
+ end
+
+ ap_ids
+ |> Kernel.--(domain_blocker_ap_ids)
+ |> Kernel.++(domain_blocker_follower_ap_ids)
+ end
+
@doc "Filters out AP IDs of users basing on their relationships with activity actor user"
def exclude_relationship_restricted_ap_ids([], _activity), do: []
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 9574432f0..e678fd415 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -261,7 +261,7 @@ defmodule Pleroma.Object do
end
end
- def increase_vote_count(ap_id, name) do
+ def increase_vote_count(ap_id, name, actor) do
with %Object{} = object <- Object.normalize(ap_id),
"Question" <- object.data["type"] do
multiple = Map.has_key?(object.data, "anyOf")
@@ -276,12 +276,15 @@ defmodule Pleroma.Object do
option
end)
+ voters = [actor | object.data["voters"] || []] |> Enum.uniq()
+
data =
if multiple do
Map.put(object.data, "anyOf", options)
else
Map.put(object.data, "oneOf", options)
end
+ |> Map.put("voters", voters)
object
|> Object.change(%{data: data})
diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex
index 089028d77..0061c69dc 100644
--- a/lib/pleroma/plugs/authentication_plug.ex
+++ b/lib/pleroma/plugs/authentication_plug.ex
@@ -4,8 +4,11 @@
defmodule Pleroma.Plugs.AuthenticationPlug do
alias Comeonin.Pbkdf2
- import Plug.Conn
+ alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
+
+ import Plug.Conn
+
require Logger
def init(options), do: options
@@ -37,6 +40,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
if Pbkdf2.checkpw(password, password_hash) do
conn
|> assign(:user, auth_user)
+ |> OAuthScopesPlug.skip_plug()
else
conn
end
diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex
index 054d2297f..9c8f5597f 100644
--- a/lib/pleroma/plugs/ensure_authenticated_plug.ex
+++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex
@@ -5,17 +5,21 @@
defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
import Plug.Conn
import Pleroma.Web.TranslationHelpers
+
alias Pleroma.User
+ use Pleroma.Web, :plug
+
def init(options) do
options
end
- def call(%{assigns: %{user: %User{}}} = conn, _) do
+ @impl true
+ def perform(%{assigns: %{user: %User{}}} = conn, _) do
conn
end
- def call(conn, options) do
+ def perform(conn, options) do
perform =
cond do
options[:if_func] -> options[:if_func].()
diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
index d980ff13d..7265bb87a 100644
--- a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
+++ b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
@@ -5,14 +5,18 @@
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
import Pleroma.Web.TranslationHelpers
import Plug.Conn
+
alias Pleroma.Config
alias Pleroma.User
+ use Pleroma.Web, :plug
+
def init(options) do
options
end
- def call(conn, _) do
+ @impl true
+ def perform(conn, _) do
public? = Config.get!([:instance, :public])
case {public?, conn} do
diff --git a/lib/pleroma/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_authenticated_check_plug.ex
new file mode 100644
index 000000000..66b8d5de5
--- /dev/null
+++ b/lib/pleroma/plugs/expect_authenticated_check_plug.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do
+ @moduledoc """
+ Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain.
+
+ No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
+ """
+
+ use Pleroma.Web, :plug
+
+ def init(options), do: options
+
+ @impl true
+ def perform(conn, _) do
+ conn
+ end
+end
diff --git a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex
new file mode 100644
index 000000000..ba0ef76bd
--- /dev/null
+++ b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do
+ @moduledoc """
+ Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug
+ chain.
+
+ No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
+ """
+
+ use Pleroma.Web, :plug
+
+ def init(options), do: options
+
+ @impl true
+ def perform(conn, _) do
+ conn
+ end
+end
diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex
index 81e6b4f2a..6462797b6 100644
--- a/lib/pleroma/plugs/http_security_plug.ex
+++ b/lib/pleroma/plugs/http_security_plug.ex
@@ -75,7 +75,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
"default-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
- "img-src 'self' data: https:",
+ "img-src 'self' data: blob: https:",
"media-src 'self' https:",
"style-src 'self' 'unsafe-inline'",
"font-src 'self'",
diff --git a/lib/pleroma/plugs/legacy_authentication_plug.ex b/lib/pleroma/plugs/legacy_authentication_plug.ex
index 5c5c36c56..d346e01a6 100644
--- a/lib/pleroma/plugs/legacy_authentication_plug.ex
+++ b/lib/pleroma/plugs/legacy_authentication_plug.ex
@@ -4,6 +4,8 @@
defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
import Plug.Conn
+
+ alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
def init(options) do
@@ -27,6 +29,7 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
conn
|> assign(:auth_user, user)
|> assign(:user, user)
+ |> OAuthScopesPlug.skip_plug()
else
_ ->
conn
diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex
index 38df074ad..efc25b79f 100644
--- a/lib/pleroma/plugs/oauth_scopes_plug.ex
+++ b/lib/pleroma/plugs/oauth_scopes_plug.ex
@@ -7,13 +7,13 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
import Pleroma.Web.Gettext
alias Pleroma.Config
- alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- @behaviour Plug
+ use Pleroma.Web, :plug
def init(%{scopes: _} = options), do: options
- def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
+ @impl true
+ def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
op = options[:op] || :|
token = assigns[:token]
@@ -28,10 +28,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
conn
options[:fallback] == :proceed_unauthenticated ->
- conn
- |> assign(:user, nil)
- |> assign(:token, nil)
- |> maybe_perform_instance_privacy_check(options)
+ drop_auth_info(conn)
true ->
missing_scopes = scopes -- matched_scopes
@@ -47,6 +44,15 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
end
end
+ @doc "Drops authentication info from connection"
+ def drop_auth_info(conn) do
+ # To simplify debugging, setting a private variable on `conn` if auth info is dropped
+ conn
+ |> put_private(:authentication_ignored, true)
+ |> assign(:user, nil)
+ |> assign(:token, nil)
+ end
+
@doc "Filters descendants of supported scopes"
def filter_descendants(scopes, supported_scopes) do
Enum.filter(
@@ -68,12 +74,4 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
scopes
end
end
-
- defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do
- if options[:skip_instance_privacy_check] do
- conn
- else
- EnsurePublicOrAuthenticatedPlug.call(conn, [])
- end
- end
end
diff --git a/lib/pleroma/plugs/plug_helper.ex b/lib/pleroma/plugs/plug_helper.ex
new file mode 100644
index 000000000..9c67be8ef
--- /dev/null
+++ b/lib/pleroma/plugs/plug_helper.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.PlugHelper do
+ @moduledoc "Pleroma Plug helper"
+
+ @called_plugs_list_id :called_plugs
+ def called_plugs_list_id, do: @called_plugs_list_id
+
+ @skipped_plugs_list_id :skipped_plugs
+ def skipped_plugs_list_id, do: @skipped_plugs_list_id
+
+ @doc "Returns `true` if specified plug was called."
+ def plug_called?(conn, plug_module) do
+ contained_in_private_list?(conn, @called_plugs_list_id, plug_module)
+ end
+
+ @doc "Returns `true` if specified plug was explicitly marked as skipped."
+ def plug_skipped?(conn, plug_module) do
+ contained_in_private_list?(conn, @skipped_plugs_list_id, plug_module)
+ end
+
+ @doc "Returns `true` if specified plug was either called or explicitly marked as skipped."
+ def plug_called_or_skipped?(conn, plug_module) do
+ plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module)
+ end
+
+ # Appends plug to known list (skipped, called). Intended to be used from within plug code only.
+ def append_to_private_list(conn, list_id, value) do
+ list = conn.private[list_id] || []
+ modified_list = Enum.uniq(list ++ [value])
+ Plug.Conn.put_private(conn, list_id, modified_list)
+ end
+
+ defp contained_in_private_list?(conn, private_variable, value) do
+ list = conn.private[private_variable] || []
+ value in list
+ end
+end
diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index 4446562ac..8d2809bbb 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -45,11 +45,11 @@ defmodule Pleroma.Stats do
end
def init(_args) do
- {:ok, get_stat_data()}
+ {:ok, calculate_stat_data()}
end
def handle_call(:force_update, _from, _state) do
- new_stats = get_stat_data()
+ new_stats = calculate_stat_data()
{:reply, new_stats, new_stats}
end
@@ -58,12 +58,12 @@ defmodule Pleroma.Stats do
end
def handle_cast(:run_update, _state) do
- new_stats = get_stat_data()
+ new_stats = calculate_stat_data()
{:noreply, new_stats}
end
- defp get_stat_data do
+ def calculate_stat_data do
peers =
from(
u in User,
@@ -77,7 +77,15 @@ defmodule Pleroma.Stats do
status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count)
- user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id)
+ users_query =
+ from(u in User,
+ where: u.deactivated != true,
+ where: u.local == true,
+ where: not is_nil(u.nickname),
+ where: not u.invisible
+ )
+
+ user_count = Repo.aggregate(users_query, :count, :id)
%{
peers: peers,
diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex
new file mode 100644
index 000000000..fb04411d9
--- /dev/null
+++ b/lib/pleroma/tests/auth_test_controller.ex
@@ -0,0 +1,93 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+# A test controller reachable only in :test env.
+defmodule Pleroma.Tests.AuthTestController do
+ @moduledoc false
+
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.User
+
+ # Serves only with proper OAuth token (:api and :authenticated_api)
+ # Skipping EnsurePublicOrAuthenticatedPlug has no effect in this case
+ #
+ # Suggested use case: all :authenticated_api endpoints (makes no sense for :api endpoints)
+ plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :do_oauth_check)
+
+ # Via :api, keeps :user if token has requested scopes (if :user is dropped, serves if public)
+ # Via :authenticated_api, serves if token is present and has requested scopes
+ #
+ # Suggested use case: vast majority of :api endpoints (no sense for :authenticated_api ones)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read"], fallback: :proceed_unauthenticated}
+ when action == :fallback_oauth_check
+ )
+
+ # Keeps :user if present, executes regardless of token / token scopes
+ # Fails with no :user for :authenticated_api / no user for :api on private instance
+ # Note: EnsurePublicOrAuthenticatedPlug is not skipped (private instance fails on no :user)
+ # Note: Basic Auth processing results in :skip_plug call for OAuthScopesPlug
+ #
+ # Suggested use: suppressing OAuth checks for other auth mechanisms (like Basic Auth)
+ # For controller-level use, see :skip_oauth_skip_publicity_check instead
+ plug(
+ :skip_plug,
+ OAuthScopesPlug when action == :skip_oauth_check
+ )
+
+ # (Shouldn't be executed since the plug is skipped)
+ plug(OAuthScopesPlug, %{scopes: ["admin"]} when action == :skip_oauth_check)
+
+ # Via :api, keeps :user if token has requested scopes, and continues with nil :user otherwise
+ # Via :authenticated_api, serves if token is present and has requested scopes
+ #
+ # Suggested use: as :fallback_oauth_check but open with nil :user for :api on private instances
+ plug(
+ :skip_plug,
+ EnsurePublicOrAuthenticatedPlug when action == :fallback_oauth_skip_publicity_check
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read"], fallback: :proceed_unauthenticated}
+ when action == :fallback_oauth_skip_publicity_check
+ )
+
+ # Via :api, keeps :user if present, serves regardless of token presence / scopes / :user presence
+ # Via :authenticated_api, serves if :user is set (regardless of token presence and its scopes)
+ #
+ # Suggested use: making an :api endpoint always accessible (e.g. email confirmation endpoint)
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
+ when action == :skip_oauth_skip_publicity_check
+ )
+
+ # Via :authenticated_api, always fails with 403 (endpoint is insecure)
+ # Via :api, drops :user if present and serves if public (private instance rejects on no user)
+ #
+ # Suggested use: none; please define OAuth rules for all :api / :authenticated_api endpoints
+ plug(:skip_plug, [] when action == :missing_oauth_check_definition)
+
+ def do_oauth_check(conn, _params), do: conn_state(conn)
+
+ def fallback_oauth_check(conn, _params), do: conn_state(conn)
+
+ def skip_oauth_check(conn, _params), do: conn_state(conn)
+
+ def fallback_oauth_skip_publicity_check(conn, _params), do: conn_state(conn)
+
+ def skip_oauth_skip_publicity_check(conn, _params), do: conn_state(conn)
+
+ def missing_oauth_check_definition(conn, _params), do: conn_state(conn)
+
+ defp conn_state(%{assigns: %{user: %User{} = user}} = conn),
+ do: json(conn, %{user_id: user.id})
+
+ defp conn_state(conn), do: json(conn, %{user_id: nil})
+end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 670ce397b..b451202b2 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -15,6 +15,7 @@ defmodule Pleroma.User do
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
+ alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.HTML
@@ -28,6 +29,7 @@ defmodule Pleroma.User do
alias Pleroma.UserRelationship
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
@@ -82,6 +84,7 @@ defmodule Pleroma.User do
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:keys, :string)
+ field(:public_key, :string)
field(:ap_id, :string)
field(:avatar, :map)
field(:local, :boolean, default: true)
@@ -94,7 +97,6 @@ defmodule Pleroma.User do
field(:last_digest_emailed_at, :naive_datetime)
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
- field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:following_count, :integer, default: 0)
@@ -112,7 +114,7 @@ defmodule Pleroma.User do
field(:show_role, :boolean, default: true)
field(:settings, :map, default: nil)
field(:magic_key, :string, default: nil)
- field(:uri, :string, default: nil)
+ field(:uri, Types.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
field(:hide_followers, :boolean, default: false)
@@ -122,7 +124,7 @@ defmodule Pleroma.User do
field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
- field(:emoji, {:array, :map}, default: [])
+ field(:emoji, :map, default: %{})
field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: [])
field(:raw_fields, {:array, :map}, default: [])
@@ -132,6 +134,8 @@ defmodule Pleroma.User do
field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
field(:also_known_as, {:array, :string}, default: [])
+ field(:inbox, :string)
+ field(:shared_inbox, :string)
embeds_one(
:notification_settings,
@@ -306,6 +310,7 @@ defmodule Pleroma.User do
end
end
+ # Should probably be renamed or removed
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
@@ -339,62 +344,72 @@ defmodule Pleroma.User do
end
end
- def remote_user_creation(params) do
+ defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
+
+ defp fix_follower_address(%{nickname: nickname} = params),
+ do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
+
+ defp fix_follower_address(params), do: params
+
+ def remote_user_changeset(struct \\ %User{local: false}, params) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
+ name =
+ case params[:name] do
+ name when is_binary(name) and byte_size(name) > 0 -> name
+ _ -> params[:nickname]
+ end
+
params =
params
+ |> Map.put(:name, name)
+ |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
|> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit)
|> truncate_fields_param()
+ |> fix_follower_address()
- changeset =
- %User{local: false}
- |> cast(
- params,
- [
- :bio,
- :name,
- :ap_id,
- :nickname,
- :avatar,
- :ap_enabled,
- :source_data,
- :banner,
- :locked,
- :magic_key,
- :uri,
- :hide_followers,
- :hide_follows,
- :hide_followers_count,
- :hide_follows_count,
- :follower_count,
- :fields,
- :following_count,
- :discoverable,
- :invisible,
- :actor_type,
- :also_known_as
- ]
- )
- |> validate_required([:name, :ap_id])
- |> unique_constraint(:nickname)
- |> validate_format(:nickname, @email_regex)
- |> validate_length(:bio, max: bio_limit)
- |> validate_length(:name, max: name_limit)
- |> validate_fields(true)
-
- case params[:source_data] do
- %{"followers" => followers, "following" => following} ->
- changeset
- |> put_change(:follower_address, followers)
- |> put_change(:following_address, following)
-
- _ ->
- followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
- put_change(changeset, :follower_address, followers)
- end
+ struct
+ |> cast(
+ params,
+ [
+ :bio,
+ :name,
+ :emoji,
+ :ap_id,
+ :inbox,
+ :shared_inbox,
+ :nickname,
+ :public_key,
+ :avatar,
+ :ap_enabled,
+ :banner,
+ :locked,
+ :last_refreshed_at,
+ :magic_key,
+ :uri,
+ :follower_address,
+ :following_address,
+ :hide_followers,
+ :hide_follows,
+ :hide_followers_count,
+ :hide_follows_count,
+ :follower_count,
+ :fields,
+ :following_count,
+ :discoverable,
+ :invisible,
+ :actor_type,
+ :also_known_as
+ ]
+ )
+ |> validate_required([:name, :ap_id])
+ |> unique_constraint(:nickname)
+ |> validate_format(:nickname, @email_regex)
+ |> validate_length(:bio, max: bio_limit)
+ |> validate_length(:name, max: name_limit)
+ |> validate_fields(true)
end
def update_changeset(struct, params \\ %{}) do
@@ -407,7 +422,11 @@ defmodule Pleroma.User do
[
:bio,
:name,
+ :emoji,
:avatar,
+ :public_key,
+ :inbox,
+ :shared_inbox,
:locked,
:no_rich_text,
:default_scope,
@@ -434,6 +453,7 @@ defmodule Pleroma.User do
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> put_fields()
+ |> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|> put_change_if_present(:avatar, &put_upload(&1, :avatar))
|> put_change_if_present(:banner, &put_upload(&1, :banner))
@@ -469,6 +489,18 @@ defmodule Pleroma.User do
|> elem(0)
end
+ defp put_emoji(changeset) do
+ bio = get_change(changeset, :bio)
+ name = get_change(changeset, :name)
+
+ if bio || name do
+ emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))
+ put_change(changeset, :emoji, emoji)
+ else
+ changeset
+ end
+ end
+
defp put_change_if_present(changeset, map_field, value_function) do
if value = get_change(changeset, map_field) do
with {:ok, new_value} <- value_function.(value) do
@@ -488,49 +520,6 @@ defmodule Pleroma.User do
end
end
- def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
- bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
- name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
-
- params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
-
- params = if remote?, do: truncate_fields_param(params), else: params
-
- struct
- |> cast(
- params,
- [
- :bio,
- :name,
- :follower_address,
- :following_address,
- :avatar,
- :last_refreshed_at,
- :ap_enabled,
- :source_data,
- :banner,
- :locked,
- :magic_key,
- :follower_count,
- :following_count,
- :hide_follows,
- :fields,
- :hide_followers,
- :allow_following_move,
- :discoverable,
- :hide_followers_count,
- :hide_follows_count,
- :actor_type,
- :also_known_as
- ]
- )
- |> unique_constraint(:nickname)
- |> validate_format(:nickname, local_nickname_regex())
- |> validate_length(:bio, max: bio_limit)
- |> validate_length(:name, max: name_limit)
- |> validate_fields(remote?)
- end
-
def update_as_admin_changeset(struct, params) do
struct
|> update_changeset(params)
@@ -606,7 +595,7 @@ defmodule Pleroma.User do
struct
|> confirmation_changeset(need_confirmation: need_confirmation?)
- |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
+ |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
@@ -699,6 +688,8 @@ defmodule Pleroma.User do
def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
+
+ # "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
follow(follower, followed, :follow_pending)
end
@@ -841,6 +832,7 @@ defmodule Pleroma.User do
def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
+ Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
{:ok, user}
end
@@ -856,9 +848,22 @@ defmodule Pleroma.User do
end
end
+ def get_user_friends_ap_ids(user) do
+ from(u in User.get_friends_query(user), select: u.ap_id)
+ |> Repo.all()
+ end
+
+ @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
+ def get_cached_user_friends_ap_ids(user) do
+ Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
+ get_user_friends_ap_ids(user)
+ end)
+ end
+
def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
+ Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil
@@ -1189,7 +1194,9 @@ defmodule Pleroma.User do
end
@spec get_recipients_from_activity(Activity.t()) :: [User.t()]
- def get_recipients_from_activity(%Activity{recipients: to}) do
+ def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
+ to = [actor | to]
+
User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
|> Repo.all()
end
@@ -1621,8 +1628,7 @@ defmodule Pleroma.User do
|> set_cache()
end
- # AP style
- def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
+ def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
key =
public_key_pem
|> :public_key.pem_decode()
@@ -1632,7 +1638,7 @@ defmodule Pleroma.User do
{:ok, key}
end
- def public_key(_), do: {:error, "not found key"}
+ def public_key(_), do: {:error, "key not found"}
def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
@@ -1643,17 +1649,6 @@ defmodule Pleroma.User do
end
end
- defp blank?(""), do: nil
- defp blank?(n), do: n
-
- def insert_or_update_user(data) do
- data
- |> Map.put(:name, blank?(data[:name]) || data[:nickname])
- |> remote_user_creation()
- |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
- |> set_cache()
- end
-
def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
def ap_enabled?(_), do: false
@@ -1962,12 +1957,6 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
- def update_source_data(user, source_data) do
- user
- |> cast(%{source_data: source_data}, [:source_data])
- |> update_and_set_cache()
- end
-
def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
%{
admin: is_admin,
@@ -1975,21 +1964,6 @@ defmodule Pleroma.User do
}
end
- # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
- # For example: [{"name": "Pronoun", "value": "she/her"}, …]
- def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
- limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
-
- attachment
- |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
- |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
- |> Enum.take(limit)
- end
-
- def fields(%{fields: nil}), do: []
-
- def fields(%{fields: fields}), do: fields
-
def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Pleroma.Config.get([:instance, limit_name], 0)
@@ -2177,9 +2151,7 @@ defmodule Pleroma.User do
# - display name
def sanitize_html(%User{} = user, filter) do
fields =
- user
- |> User.fields()
- |> Enum.map(fn %{"name" => name, "value" => value} ->
+ Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index ec88088cf..ac77aab71 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -54,13 +54,13 @@ defmodule Pleroma.User.Query do
select: term(),
limit: pos_integer()
}
- | %{}
+ | map()
@ilike_criteria [:nickname, :name, :query]
@equal_criteria [:email]
@contains_criteria [:ap_id, :nickname]
- @spec build(criteria()) :: Query.t()
+ @spec build(Query.t(), criteria()) :: Query.t()
def build(query \\ base_query(), criteria) do
prepare_query(query, criteria)
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 86b105b7f..1f4a09370 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -118,9 +118,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def increase_poll_votes_if_vote(%{
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
- "type" => "Create"
+ "type" => "Create",
+ "actor" => actor
}) do
- Object.increase_vote_count(reply_ap_id, name)
+ Object.increase_vote_count(reply_ap_id, name, actor)
end
def increase_poll_votes_if_vote(_create_data), do: :noop
@@ -397,36 +398,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
- @spec like(User.t(), Object.t(), String.t() | nil, boolean()) ::
- {:ok, Activity.t(), Object.t()} | {:error, any()}
- def like(user, object, activity_id \\ nil, local \\ true) do
- with {:ok, result} <- Repo.transaction(fn -> do_like(user, object, activity_id, local) end) do
- result
- end
- end
-
- defp do_like(
- %User{ap_id: ap_id} = user,
- %Object{data: %{"id" => _}} = object,
- activity_id,
- local
- ) do
- with nil <- get_existing_like(ap_id, object),
- like_data <- make_like_data(user, object, activity_id),
- {:ok, activity} <- insert(like_data, local),
- {:ok, object} <- add_like_to_object(activity, object),
- :ok <- maybe_federate(activity) do
- {:ok, activity, object}
- else
- %Activity{} = activity ->
- {:ok, activity, object}
-
- {:error, error} ->
- Repo.rollback(error)
- end
- end
-
@spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
@@ -467,6 +438,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp do_announce(user, object, activity_id, local, public) do
with true <- is_announceable?(object, user, public),
+ object <- Object.get_by_id(object.id),
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
@@ -853,7 +825,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
- when visibility not in @valid_visibilities do
+ when visibility not in [nil | @valid_visibilities] do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
@@ -1060,7 +1032,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
raise "Can't use the child object without preloading!"
end
- defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
+ defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do
from(
[_activity, object] in query,
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
@@ -1069,16 +1041,51 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_media(query, _), do: query
- defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or val == "1" do
+ defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do
from(
[_activity, object] in query,
where: fragment("?->>'inReplyTo' is null", object.data)
)
end
+ defp restrict_replies(query, %{
+ "reply_filtering_user" => user,
+ "reply_visibility" => "self"
+ }) do
+ from(
+ [activity, object] in query,
+ where:
+ fragment(
+ "?->>'inReplyTo' is null OR ? = ANY(?)",
+ object.data,
+ ^user.ap_id,
+ activity.recipients
+ )
+ )
+ end
+
+ defp restrict_replies(query, %{
+ "reply_filtering_user" => user,
+ "reply_visibility" => "following"
+ }) do
+ from(
+ [activity, object] in query,
+ where:
+ fragment(
+ "?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?",
+ object.data,
+ ^[user.ap_id | User.get_cached_user_friends_ap_ids(user)],
+ activity.recipients,
+ activity.actor,
+ activity.actor,
+ ^user.ap_id
+ )
+ )
+ end
+
defp restrict_replies(query, _), do: query
- defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
+ defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end
@@ -1157,7 +1164,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end
- defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
+ # TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only,
+ # the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2'
+ # and `restrict_muted/2`
+
+ defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids})
+ when pinned in [true, "true", "1"] do
from(activity in query, where: activity.id in ^ids)
end
@@ -1290,6 +1302,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"])
+ |> restrict_replies(opts)
|> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> restrict_tag_all(opts)
@@ -1304,7 +1317,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config)
- |> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
@@ -1427,19 +1439,44 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
+ emojis =
+ data
+ |> Map.get("tag", [])
+ |> Enum.filter(fn
+ %{"type" => "Emoji"} -> true
+ _ -> false
+ end)
+ |> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc ->
+ Map.put(acc, String.trim(name, ":"), url)
+ end)
+
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false
invisible = data["invisible"] || false
actor_type = data["type"] || "Person"
+ public_key =
+ if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
+ data["publicKey"]["publicKeyPem"]
+ else
+ nil
+ end
+
+ shared_inbox =
+ if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
+ data["endpoints"]["sharedInbox"]
+ else
+ nil
+ end
+
user_data = %{
ap_id: data["id"],
uri: get_actor_url(data["url"]),
ap_enabled: true,
- source_data: data,
banner: banner,
fields: fields,
+ emoji: emojis,
locked: locked,
discoverable: discoverable,
invisible: invisible,
@@ -1449,7 +1486,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
following_address: data["following"],
bio: data["summary"],
actor_type: actor_type,
- also_known_as: Map.get(data, "alsoKnownAs", [])
+ also_known_as: Map.get(data, "alsoKnownAs", []),
+ public_key: public_key,
+ inbox: data["inbox"],
+ shared_inbox: shared_inbox
}
# nickname can be nil because of virtual actors
@@ -1551,11 +1591,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def make_user_from_ap_id(ap_id) do
- if _user = User.get_cached_by_ap_id(ap_id) do
+ user = User.get_cached_by_ap_id(ap_id)
+
+ if user && !User.ap_enabled?(user) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
- User.insert_or_update_user(data)
+ if user do
+ user
+ |> User.remote_user_changeset(data)
+ |> User.update_and_set_cache()
+ else
+ data
+ |> User.remote_user_changeset()
+ |> Repo.insert()
+ |> User.set_cache()
+ end
else
e -> {:error, e}
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 8b9eb4a2c..d625530ec 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -12,8 +12,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Plugs.EnsureAuthenticatedPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectView
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
@@ -421,7 +423,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
- {:ok, activity, _object} <- ActivityPub.like(user, object) do
+ {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
+ {_, {:ok, %Activity{} = activity, _meta}} <-
+ {:common_pipeline,
+ Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
{:ok, activity}
else
_ -> {:error, dgettext("errors", "Can't like object")}
diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
index 4a8bc91ae..b0ccb63c8 100644
--- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
@moduledoc "Filter activities depending on their age"
@behaviour Pleroma.Web.ActivityPub.MRF
- defp check_date(%{"published" => published} = message) do
+ defp check_date(%{"object" => %{"published" => published}} = message) do
with %DateTime{} = now <- DateTime.utc_now(),
{:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
max_ttl <- Config.get([:mrf_object_age, :threshold]),
@@ -96,5 +96,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
def filter(message), do: {:ok, message}
@impl true
- def describe, do: {:ok, %{}}
+ def describe do
+ mrf_object_age =
+ Pleroma.Config.get(:mrf_object_age)
+ |> Enum.into(%{})
+
+ {:ok, %{mrf_object_age: mrf_object_age}}
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 4edc007fd..b7dcb1b86 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -149,6 +149,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_banner_removal(_actor_info, object), do: {:ok, object}
@impl true
+ def filter(%{"type" => "Delete", "actor" => actor} = object) do
+ %{host: actor_host} = URI.parse(actor)
+
+ reject_deletes =
+ Pleroma.Config.get([:mrf_simple, :reject_deletes])
+ |> MRF.subdomains_regex()
+
+ if MRF.subdomain_match?(reject_deletes, actor_host) do
+ {:reject, nil}
+ else
+ {:ok, object}
+ end
+ end
+
+ @impl true
def filter(%{"actor" => actor} = object) do
actor_info = URI.parse(actor)
diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
index c95b622e4..462a5620a 100644
--- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
@@ -35,6 +35,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:inRepyTo, :string)
+ field(:uri, Types.Uri)
field(:likes, {:array, :string}, default: [])
field(:announcements, {:array, :string}, default: [])
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex
index f6e749b33..f71f76370 100644
--- a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex
@@ -15,15 +15,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do
def cast(%{"id" => object}), do: cast(object)
- def cast(_) do
- :error
- end
+ def cast(_), do: :error
- def dump(data) do
- {:ok, data}
- end
+ def dump(data), do: {:ok, data}
- def load(data) do
- {:ok, data}
- end
+ def load(data), do: {:ok, data}
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/uri.ex b/lib/pleroma/web/activity_pub/object_validators/types/uri.ex
new file mode 100644
index 000000000..24845bcc0
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/uri.ex
@@ -0,0 +1,20 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Uri do
+ use Ecto.Type
+
+ def type, do: :string
+
+ def cast(uri) when is_binary(uri) do
+ case URI.parse(uri) do
+ %URI{host: nil} -> :error
+ %URI{host: ""} -> :error
+ %URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, uri}
+ _ -> :error
+ end
+ end
+
+ def cast(_), do: :error
+
+ def dump(data), do: {:ok, data}
+
+ def load(data), do: {:ok, data}
+end
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index 6c558e7f0..b70cbd043 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -141,8 +141,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|> Enum.map(& &1.ap_id)
end
- defp maybe_use_sharedinbox(%User{source_data: data}),
- do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
+ defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
+ defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
@doc """
Determine a user inbox to use based on heuristics. These heuristics
@@ -157,7 +157,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
"""
def determine_inbox(
%Activity{data: activity_data},
- %User{source_data: data} = user
+ %User{inbox: inbox} = user
) do
to = activity_data["to"] || []
cc = activity_data["cc"] || []
@@ -174,7 +174,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
maybe_use_sharedinbox(user)
true ->
- data["inbox"]
+ inbox
end
end
@@ -192,14 +192,13 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
inboxes =
recipients
|> Enum.filter(&User.ap_enabled?/1)
- |> Enum.map(fn %{source_data: data} -> data["inbox"] end)
+ |> Enum.map(fn actor -> actor.inbox end)
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
Repo.checkout(fn ->
Enum.each(inboxes, fn {inbox, unreachable_since} ->
- %User{ap_id: ap_id} =
- Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end)
+ %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
# Get all the recipients on the same host and add them to cc. Otherwise, a remote
# instance would only accept a first message for the first recipient and ignore the rest.
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index 666a4e310..5981e7545 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -15,10 +15,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Add like to object
# - Set up notification
def handle(%{data: %{"type" => "Like"}} = object, meta) do
- liked_object = Object.get_by_ap_id(object.data["object"])
- Utils.add_like_to_object(object, liked_object)
- Notification.create_notifications(object)
- {:ok, object, meta}
+ {:ok, result} =
+ Pleroma.Repo.transaction(fn ->
+ liked_object = Object.get_by_ap_id(object.data["object"])
+ Utils.add_like_to_object(object, liked_object)
+
+ Notification.create_notifications(object)
+
+ {:ok, object, meta}
+ end)
+
+ result
end
# Nothing to do
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 39feae285..09119137b 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -711,7 +711,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
actor
- |> User.upgrade_changeset(new_user_data, true)
+ |> User.remote_user_changeset(new_user_data)
|> User.update_and_set_cache()
ActivityPub.update(%{
@@ -1160,7 +1160,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def take_emoji_tags(%User{emoji: emoji}) do
emoji
- |> Enum.flat_map(&Map.to_list/1)
+ |> Map.to_list()
|> Enum.map(&build_emoji_tag/1)
end
@@ -1254,12 +1254,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
- already_ap <- User.ap_enabled?(user),
- {:ok, user} <- upgrade_user(user, data) do
- if not already_ap do
- TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
- end
-
+ {:ok, user} <- update_user(user, data) do
+ TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
{:ok, user}
else
%User{} = user -> {:ok, user}
@@ -1267,9 +1263,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- defp upgrade_user(user, data) do
+ defp update_user(user, data) do
user
- |> User.upgrade_changeset(data, true)
+ |> User.remote_user_changeset(data)
|> User.update_and_set_cache()
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index bc21ac6c7..34590b16d 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -79,10 +79,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
emoji_tags = Transmogrifier.take_emoji_tags(user)
- fields =
- user
- |> User.fields()
- |> Enum.map(&Map.put(&1, "type", "PropertyValue"))
+ fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
%{
"id" => user.ap_id,
@@ -103,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
},
"endpoints" => endpoints,
"attachment" => fields,
- "tag" => (user.source_data["tag"] || []) ++ emoji_tags,
+ "tag" => emoji_tags,
"discoverable" => user.discoverable
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 831c3bd02..816c11e01 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -27,7 +27,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.OAuth.App
alias Pleroma.Web.Router
require Logger
@@ -46,6 +48,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
%{scopes: ["write:accounts"], admin: true}
when action in [
:get_password_reset,
+ :force_password_reset,
:user_delete,
:users_create,
:user_toggle_activation,
@@ -54,7 +57,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:tag_users,
:untag_users,
:right_add,
+ :right_add_multiple,
:right_delete,
+ :right_delete_multiple,
:update_user_credentials
]
)
@@ -82,13 +87,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["write:reports"], admin: true}
- when action in [:reports_update]
+ when action in [:reports_update, :report_notes_create, :report_notes_delete]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true}
- when action == :list_user_statuses
+ when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]
)
plug(
@@ -100,13 +105,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["read"], admin: true}
- when action in [:config_show, :list_log, :stats]
+ when action in [
+ :config_show,
+ :list_log,
+ :stats,
+ :relay_list,
+ :config_descriptions,
+ :need_reboot
+ ]
)
plug(
OAuthScopesPlug,
%{scopes: ["write"], admin: true}
- when action == :config_update
+ when action in [
+ :restart,
+ :config_update,
+ :resend_confirmation_email,
+ :confirm_email,
+ :oauth_app_create,
+ :oauth_app_list,
+ :oauth_app_update,
+ :oauth_app_delete,
+ :reload_emoji
+ ]
)
action_fallback(:errors)
@@ -914,16 +936,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end)
|> List.flatten()
- response = %{configs: merged}
-
- response =
- if Restarter.Pleroma.need_reboot?() do
- Map.put(response, :need_reboot, true)
- else
- response
- end
-
- json(conn, response)
+ json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
@@ -950,28 +963,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
Config.TransferTask.load_and_update_env(deleted, false)
- need_reboot? =
- Restarter.Pleroma.need_reboot?() ||
- Enum.any?(updated, fn config ->
+ if !Restarter.Pleroma.need_reboot?() do
+ changed_reboot_settings? =
+ (updated ++ deleted)
+ |> Enum.any?(fn config ->
group = ConfigDB.from_string(config.group)
key = ConfigDB.from_string(config.key)
value = ConfigDB.from_binary(config.value)
Config.TransferTask.pleroma_need_restart?(group, key, value)
end)
- response = %{configs: updated}
-
- response =
- if need_reboot? do
- Restarter.Pleroma.need_reboot()
- Map.put(response, :need_reboot, need_reboot?)
- else
- response
- end
+ if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
+ end
conn
|> put_view(ConfigView)
- |> render("index.json", response)
+ |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
@@ -983,6 +990,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
+ def need_reboot(conn, _params) do
+ json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
+ end
+
defp configurable_from_database(conn) do
if Config.get(:configurable_from_database) do
:ok
@@ -1028,6 +1039,83 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
conn |> json("")
end
+ def oauth_app_create(conn, params) do
+ params =
+ if params["name"] do
+ Map.put(params, "client_name", params["name"])
+ else
+ params
+ end
+
+ result =
+ case App.create(params) do
+ {:ok, app} ->
+ AppView.render("show.json", %{app: app, admin: true})
+
+ {:error, changeset} ->
+ App.errors(changeset)
+ end
+
+ json(conn, result)
+ end
+
+ def oauth_app_update(conn, params) do
+ params =
+ if params["name"] do
+ Map.put(params, "client_name", params["name"])
+ else
+ params
+ end
+
+ with {:ok, app} <- App.update(params) do
+ json(conn, AppView.render("show.json", %{app: app, admin: true}))
+ else
+ {:error, changeset} ->
+ json(conn, App.errors(changeset))
+
+ nil ->
+ json_response(conn, :bad_request, "")
+ end
+ end
+
+ def oauth_app_list(conn, params) do
+ {page, page_size} = page_params(params)
+
+ search_params = %{
+ client_name: params["name"],
+ client_id: params["client_id"],
+ page: page,
+ page_size: page_size
+ }
+
+ search_params =
+ if Map.has_key?(params, "trusted") do
+ Map.put(search_params, :trusted, params["trusted"])
+ else
+ search_params
+ end
+
+ with {:ok, apps, count} <- App.search(search_params) do
+ json(
+ conn,
+ AppView.render("index.json",
+ apps: apps,
+ count: count,
+ page_size: page_size,
+ admin: true
+ )
+ )
+ end
+ end
+
+ def oauth_app_delete(conn, params) do
+ with {:ok, _app} <- App.destroy(params["id"]) do
+ json_response(conn, :no_content, "")
+ else
+ _ -> json_response(conn, :bad_request, "")
+ end
+ end
+
def stats(conn, _) do
count = Stats.get_status_visibility_count()
@@ -1035,25 +1123,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> json(%{"status_visibility" => count})
end
- def errors(conn, {:error, :not_found}) do
+ defp errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
- def errors(conn, {:error, reason}) do
+ defp errors(conn, {:error, reason}) do
conn
|> put_status(:bad_request)
|> json(reason)
end
- def errors(conn, {:param_cast, _}) do
+ defp errors(conn, {:param_cast, _}) do
conn
|> put_status(:bad_request)
|> json(dgettext("errors", "Invalid parameters"))
end
- def errors(conn, _) do
+ defp errors(conn, _) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "Something went wrong"))
diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex
index 360ddc22c..3637dee24 100644
--- a/lib/pleroma/web/admin_api/views/status_view.ex
+++ b/lib/pleroma/web/admin_api/views/status_view.ex
@@ -8,15 +8,16 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
require Pleroma.Constants
alias Pleroma.User
+ alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", opts) do
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
- user = get_user(activity.data["actor"])
+ user = StatusView.get_user(activity.data["actor"])
- Pleroma.Web.MastodonAPI.StatusView.render("show.json", opts)
+ StatusView.render("show.json", opts)
|> Map.merge(%{account: merge_account_views(user)})
end
@@ -26,17 +27,4 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
end
defp merge_account_views(_), do: %{}
-
- defp get_user(ap_id) do
- cond do
- user = User.get_cached_by_ap_id(ap_id) ->
- user
-
- user = User.get_by_guessed_nickname(ap_id) ->
- user
-
- true ->
- User.error_user(ap_id)
- end
- end
end
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index 3890489e3..b3c1e3ea2 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.ApiSpec do
alias OpenApiSpex.OpenApi
+ alias OpenApiSpex.Operation
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
@@ -24,6 +25,13 @@ defmodule Pleroma.Web.ApiSpec do
# populate the paths from a phoenix router
paths: OpenApiSpex.Paths.from_router(Router),
components: %OpenApiSpex.Components{
+ parameters: %{
+ "accountIdOrNickname" =>
+ Operation.parameter(:id, :path, :string, "Account ID or nickname",
+ example: "123",
+ required: true
+ )
+ },
securitySchemes: %{
"oAuth" => %OpenApiSpex.SecurityScheme{
type: "oauth2",
diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex
index 7348dcbee..ce40fb9e8 100644
--- a/lib/pleroma/web/api_spec/helpers.ex
+++ b/lib/pleroma/web/api_spec/helpers.ex
@@ -3,6 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Helpers do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+
def request_body(description, schema_ref, opts \\ []) do
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
@@ -24,4 +27,23 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
required: opts[:required] || false
}
end
+
+ def pagination_params do
+ [
+ Operation.parameter(:max_id, :query, :string, "Return items older than this ID"),
+ Operation.parameter(:min_id, :query, :string, "Return the oldest items newer than this ID"),
+ Operation.parameter(
+ :since_id,
+ :query,
+ :string,
+ "Return the newest items newer than this ID"
+ ),
+ Operation.parameter(
+ :limit,
+ :query,
+ %Schema{type: :integer, default: 20, maximum: 40},
+ "Limit"
+ )
+ ]
+ end
end
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
new file mode 100644
index 000000000..d3e8bd484
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -0,0 +1,702 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.AccountOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Reference
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
+ alias Pleroma.Web.ApiSpec.Schemas.ActorType
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ @spec open_api_operation(atom) :: Operation.t()
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ @spec create_operation() :: Operation.t()
+ def create_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Register an account",
+ description:
+ "Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.",
+ operationId: "AccountController.create",
+ requestBody: request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Account", "application/json", create_response()),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 429 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def verify_credentials_operation do
+ %Operation{
+ tags: ["accounts"],
+ description: "Test to make sure that the user token works.",
+ summary: "Verify account credentials",
+ operationId: "AccountController.verify_credentials",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ responses: %{
+ 200 => Operation.response("Account", "application/json", Account)
+ }
+ }
+ end
+
+ def update_credentials_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Update account credentials",
+ description: "Update the user's display and preferences.",
+ operationId: "AccountController.update_credentials",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ requestBody: request_body("Parameters", update_creadentials_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Account", "application/json", Account),
+ 403 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def relationships_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Check relationships to other accounts",
+ operationId: "AccountController.relationships",
+ description: "Find out whether a given account is followed, blocked, muted, etc.",
+ security: [%{"oAuth" => ["read:follows"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :query,
+ %Schema{
+ oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}]
+ },
+ "Account IDs",
+ example: "123"
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Account", "application/json", array_of_relationships())
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Account",
+ operationId: "AccountController.show",
+ description: "View information about a profile.",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Account", "application/json", Account),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def statuses_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Statuses",
+ operationId: "AccountController.statuses",
+ description:
+ "Statuses posted to the given account. Public (for public statuses only), or user token + `read:statuses` (for private statuses the user is authorized to see)",
+ parameters:
+ [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+ Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"),
+ Operation.parameter(:tagged, :query, :string, "With tag"),
+ Operation.parameter(
+ :only_media,
+ :query,
+ BooleanLike,
+ "Include only statuses with media attached"
+ ),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike,
+ "Include statuses from muted acccounts."
+ ),
+ Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
+ Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
+ Operation.parameter(
+ :exclude_visibilities,
+ :query,
+ %Schema{type: :array, items: VisibilityScope},
+ "Exclude visibilities"
+ )
+ ] ++ pagination_params(),
+ responses: %{
+ 200 => Operation.response("Statuses", "application/json", array_of_statuses()),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def followers_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Followers",
+ operationId: "AccountController.followers",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ description:
+ "Accounts which follow the given account, if network is not hidden by the account owner.",
+ parameters:
+ [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
+ responses: %{
+ 200 => Operation.response("Accounts", "application/json", array_of_accounts())
+ }
+ }
+ end
+
+ def following_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Following",
+ operationId: "AccountController.following",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ description:
+ "Accounts which the given account is following, if network is not hidden by the account owner.",
+ parameters:
+ [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
+ responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
+ }
+ end
+
+ def lists_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Lists containing this account",
+ operationId: "AccountController.lists",
+ security: [%{"oAuth" => ["read:lists"]}],
+ description: "User lists that you have added this account to.",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{200 => Operation.response("Lists", "application/json", array_of_lists())}
+ }
+ end
+
+ def follow_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Follow",
+ operationId: "AccountController.follow",
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ description: "Follow the given account",
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+ Operation.parameter(
+ :reblogs,
+ :query,
+ BooleanLike,
+ "Receive this account's reblogs in home timeline? Defaults to true."
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unfollow_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Unfollow",
+ operationId: "AccountController.unfollow",
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ description: "Unfollow the given account",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def mute_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Mute",
+ operationId: "AccountController.mute",
+ security: [%{"oAuth" => ["follow", "write:mutes"]}],
+ requestBody: request_body("Parameters", mute_request()),
+ description:
+ "Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).",
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+ Operation.parameter(
+ :notifications,
+ :query,
+ %Schema{allOf: [BooleanLike], default: true},
+ "Mute notifications in addition to statuses? Defaults to `true`."
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ def unmute_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Unmute",
+ operationId: "AccountController.unmute",
+ security: [%{"oAuth" => ["follow", "write:mutes"]}],
+ description: "Unmute the given account.",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ def block_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Block",
+ operationId: "AccountController.block",
+ security: [%{"oAuth" => ["follow", "write:blocks"]}],
+ description:
+ "Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ def unblock_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Unblock",
+ operationId: "AccountController.unblock",
+ security: [%{"oAuth" => ["follow", "write:blocks"]}],
+ description: "Unblock the given account.",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ def follow_by_uri_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Follow by URI",
+ operationId: "AccountController.follows",
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ requestBody: request_body("Parameters", follow_by_uri_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Account", "application/json", AccountRelationship),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def mutes_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Muted accounts",
+ operationId: "AccountController.mutes",
+ description: "Accounts the user has muted.",
+ security: [%{"oAuth" => ["follow", "read:mutes"]}],
+ responses: %{
+ 200 => Operation.response("Accounts", "application/json", array_of_accounts())
+ }
+ }
+ end
+
+ def blocks_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Blocked users",
+ operationId: "AccountController.blocks",
+ description: "View your blocks. See also accounts/:id/{block,unblock}",
+ security: [%{"oAuth" => ["read:blocks"]}],
+ responses: %{
+ 200 => Operation.response("Accounts", "application/json", array_of_accounts())
+ }
+ }
+ end
+
+ def endorsements_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Endorsements",
+ operationId: "AccountController.endorsements",
+ description: "Not implemented",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ responses: %{
+ 200 => Operation.response("Empry array", "application/json", %Schema{type: :array})
+ }
+ }
+ end
+
+ def identity_proofs_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Identity proofs",
+ operationId: "AccountController.identity_proofs",
+ description: "Not implemented",
+ responses: %{
+ 200 => Operation.response("Empry array", "application/json", %Schema{type: :array})
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "AccountCreateRequest",
+ description: "POST body for creating an account",
+ type: :object,
+ properties: %{
+ reason: %Schema{
+ type: :string,
+ description:
+ "Text that will be reviewed by moderators if registrations require manual approval"
+ },
+ username: %Schema{type: :string, description: "The desired username for the account"},
+ email: %Schema{
+ type: :string,
+ description:
+ "The email address to be used for login. Required when `account_activation_required` is enabled.",
+ format: :email
+ },
+ password: %Schema{
+ type: :string,
+ description: "The password to be used for login",
+ format: :password
+ },
+ agreement: %Schema{
+ type: :boolean,
+ description:
+ "Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE."
+ },
+ locale: %Schema{
+ type: :string,
+ description: "The language of the confirmation email that will be sent"
+ },
+ # Pleroma-specific properties:
+ fullname: %Schema{type: :string, description: "Full name"},
+ bio: %Schema{type: :string, description: "Bio", default: ""},
+ captcha_solution: %Schema{
+ type: :string,
+ description: "Provider-specific captcha solution"
+ },
+ captcha_token: %Schema{type: :string, description: "Provider-specific captcha token"},
+ captcha_answer_data: %Schema{type: :string, description: "Provider-specific captcha data"},
+ token: %Schema{
+ type: :string,
+ description: "Invite token required when the registrations aren't public"
+ }
+ },
+ required: [:username, :password, :agreement],
+ example: %{
+ "username" => "cofe",
+ "email" => "cofe@example.com",
+ "password" => "secret",
+ "agreement" => "true",
+ "bio" => "☕️"
+ }
+ }
+ end
+
+ defp create_response do
+ %Schema{
+ title: "AccountCreateResponse",
+ description: "Response schema for an account",
+ type: :object,
+ properties: %{
+ token_type: %Schema{type: :string},
+ access_token: %Schema{type: :string},
+ scope: %Schema{type: :array, items: %Schema{type: :string}},
+ created_at: %Schema{type: :integer, format: :"date-time"}
+ },
+ example: %{
+ "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
+ "created_at" => 1_585_918_714,
+ "scope" => ["read", "write", "follow", "push"],
+ "token_type" => "Bearer"
+ }
+ }
+ end
+
+ defp update_creadentials_request do
+ %Schema{
+ title: "AccountUpdateCredentialsRequest",
+ description: "POST body for creating an account",
+ type: :object,
+ properties: %{
+ bot: %Schema{
+ type: :boolean,
+ description: "Whether the account has a bot flag."
+ },
+ display_name: %Schema{
+ type: :string,
+ description: "The display name to use for the profile."
+ },
+ note: %Schema{type: :string, description: "The account bio."},
+ avatar: %Schema{
+ type: :string,
+ description: "Avatar image encoded using multipart/form-data",
+ format: :binary
+ },
+ header: %Schema{
+ type: :string,
+ description: "Header image encoded using multipart/form-data",
+ format: :binary
+ },
+ locked: %Schema{
+ type: :boolean,
+ description: "Whether manual approval of follow requests is required."
+ },
+ fields_attributes: %Schema{
+ oneOf: [
+ %Schema{type: :array, items: attribute_field()},
+ %Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
+ ]
+ },
+ # NOTE: `source` field is not supported
+ #
+ # source: %Schema{
+ # type: :object,
+ # properties: %{
+ # privacy: %Schema{type: :string},
+ # sensitive: %Schema{type: :boolean},
+ # language: %Schema{type: :string}
+ # }
+ # },
+
+ # Pleroma-specific fields
+ no_rich_text: %Schema{
+ type: :boolean,
+ description: "html tags are stripped from all statuses requested from the API"
+ },
+ hide_followers: %Schema{type: :boolean, description: "user's followers will be hidden"},
+ hide_follows: %Schema{type: :boolean, description: "user's follows will be hidden"},
+ hide_followers_count: %Schema{
+ type: :boolean,
+ description: "user's follower count will be hidden"
+ },
+ hide_follows_count: %Schema{
+ type: :boolean,
+ description: "user's follow count will be hidden"
+ },
+ hide_favorites: %Schema{
+ type: :boolean,
+ description: "user's favorites timeline will be hidden"
+ },
+ show_role: %Schema{
+ type: :boolean,
+ description: "user's role (e.g admin, moderator) will be exposed to anyone in the
+ API"
+ },
+ default_scope: VisibilityScope,
+ pleroma_settings_store: %Schema{
+ type: :object,
+ description: "Opaque user settings to be saved on the backend."
+ },
+ skip_thread_containment: %Schema{
+ type: :boolean,
+ description: "Skip filtering out broken threads"
+ },
+ allow_following_move: %Schema{
+ type: :boolean,
+ description: "Allows automatically follow moved following accounts"
+ },
+ pleroma_background_image: %Schema{
+ type: :string,
+ description: "Sets the background image of the user.",
+ format: :binary
+ },
+ discoverable: %Schema{
+ type: :boolean,
+ description:
+ "Discovery of this account in search results and other services is allowed."
+ },
+ actor_type: ActorType
+ },
+ example: %{
+ bot: false,
+ display_name: "cofe",
+ note: "foobar",
+ fields_attributes: [%{name: "foo", value: "bar"}],
+ no_rich_text: false,
+ hide_followers: true,
+ hide_follows: false,
+ hide_followers_count: false,
+ hide_follows_count: false,
+ hide_favorites: false,
+ show_role: false,
+ default_scope: "private",
+ pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
+ skip_thread_containment: false,
+ allow_following_move: false,
+ discoverable: false,
+ actor_type: "Person"
+ }
+ }
+ end
+
+ defp array_of_accounts do
+ %Schema{
+ title: "ArrayOfAccounts",
+ type: :array,
+ items: Account
+ }
+ end
+
+ defp array_of_relationships do
+ %Schema{
+ title: "ArrayOfRelationships",
+ description: "Response schema for account relationships",
+ type: :array,
+ items: AccountRelationship,
+ example: [
+ %{
+ "id" => "1",
+ "following" => true,
+ "showing_reblogs" => true,
+ "followed_by" => true,
+ "blocking" => false,
+ "blocked_by" => true,
+ "muting" => false,
+ "muting_notifications" => false,
+ "requested" => false,
+ "domain_blocking" => false,
+ "subscribing" => false,
+ "endorsed" => true
+ },
+ %{
+ "id" => "2",
+ "following" => true,
+ "showing_reblogs" => true,
+ "followed_by" => true,
+ "blocking" => false,
+ "blocked_by" => true,
+ "muting" => true,
+ "muting_notifications" => false,
+ "requested" => true,
+ "domain_blocking" => false,
+ "subscribing" => false,
+ "endorsed" => false
+ },
+ %{
+ "id" => "3",
+ "following" => true,
+ "showing_reblogs" => true,
+ "followed_by" => true,
+ "blocking" => true,
+ "blocked_by" => false,
+ "muting" => true,
+ "muting_notifications" => false,
+ "requested" => false,
+ "domain_blocking" => true,
+ "subscribing" => true,
+ "endorsed" => false
+ }
+ ]
+ }
+ end
+
+ defp follow_by_uri_request do
+ %Schema{
+ title: "AccountFollowsRequest",
+ description: "POST body for muting an account",
+ type: :object,
+ properties: %{
+ uri: %Schema{type: :string, format: :uri}
+ },
+ required: [:uri]
+ }
+ end
+
+ defp mute_request do
+ %Schema{
+ title: "AccountMuteRequest",
+ description: "POST body for muting an account",
+ type: :object,
+ properties: %{
+ notifications: %Schema{
+ type: :boolean,
+ description: "Mute notifications in addition to statuses? Defaults to true.",
+ default: true
+ }
+ },
+ example: %{
+ "notifications" => true
+ }
+ }
+ end
+
+ defp list do
+ %Schema{
+ title: "List",
+ description: "Response schema for a list",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ title: %Schema{type: :string}
+ },
+ example: %{
+ "id" => "123",
+ "title" => "my list"
+ }
+ }
+ end
+
+ defp array_of_lists do
+ %Schema{
+ title: "ArrayOfLists",
+ description: "Response schema for lists",
+ type: :array,
+ items: list(),
+ example: [
+ %{"id" => "123", "title" => "my list"},
+ %{"id" => "1337", "title" => "anotehr list"}
+ ]
+ }
+ end
+
+ defp array_of_statuses do
+ %Schema{
+ title: "ArrayOfStatuses",
+ type: :array,
+ items: Status
+ }
+ end
+
+ defp attribute_field do
+ %Schema{
+ title: "AccountAttributeField",
+ description: "Request schema for account custom fields",
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string},
+ value: %Schema{type: :string}
+ },
+ required: [:name, :value],
+ example: %{
+ "name" => "Website",
+ "value" => "https://pleroma.com"
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex
index 26d8dbd42..f6ccd073f 100644
--- a/lib/pleroma/web/api_spec/operations/app_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/app_operation.ex
@@ -6,8 +6,6 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
- alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
- alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
@spec open_api_operation(atom) :: Operation.t()
def open_api_operation(action) do
@@ -22,9 +20,9 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
summary: "Create an application",
description: "Create a new application to obtain OAuth2 credentials",
operationId: "AppController.create",
- requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true),
+ requestBody: Helpers.request_body("Parameters", create_request(), required: true),
responses: %{
- 200 => Operation.response("App", "application/json", AppCreateResponse),
+ 200 => Operation.response("App", "application/json", create_response()),
422 =>
Operation.response(
"Unprocessable Entity",
@@ -51,11 +49,7 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
summary: "Verify your app works",
description: "Confirm that the app's OAuth2 credentials work.",
operationId: "AppController.verify_credentials",
- security: [
- %{
- "oAuth" => ["read"]
- }
- ],
+ security: [%{"oAuth" => ["read"]}],
responses: %{
200 =>
Operation.response("App", "application/json", %Schema{
@@ -93,4 +87,58 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
}
}
end
+
+ defp create_request do
+ %Schema{
+ title: "AppCreateRequest",
+ description: "POST body for creating an app",
+ type: :object,
+ properties: %{
+ client_name: %Schema{type: :string, description: "A name for your application."},
+ redirect_uris: %Schema{
+ type: :string,
+ description:
+ "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
+ },
+ scopes: %Schema{
+ type: :string,
+ description: "Space separated list of scopes",
+ default: "read"
+ },
+ website: %Schema{type: :string, description: "A URL to the homepage of your app"}
+ },
+ required: [:client_name, :redirect_uris],
+ example: %{
+ "client_name" => "My App",
+ "redirect_uris" => "https://myapp.com/auth/callback",
+ "website" => "https://myapp.com/"
+ }
+ }
+ end
+
+ defp create_response do
+ %Schema{
+ title: "AppCreateResponse",
+ description: "Response schema for an app",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ name: %Schema{type: :string},
+ client_id: %Schema{type: :string},
+ client_secret: %Schema{type: :string},
+ redirect_uri: %Schema{type: :string},
+ vapid_key: %Schema{type: :string},
+ website: %Schema{type: :string, nullable: true}
+ },
+ example: %{
+ "id" => "123",
+ "name" => "My App",
+ "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
+ "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
+ "vapid_key" =>
+ "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
+ "website" => "https://myapp.com/"
+ }
+ }
+ end
end
diff --git a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
new file mode 100644
index 000000000..2f812ac77
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
@@ -0,0 +1,88 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Emoji
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["custom_emojis"],
+ summary: "List custom custom emojis",
+ description: "Returns custom emojis that are available on the server.",
+ operationId: "CustomEmojiController.index",
+ responses: %{
+ 200 => Operation.response("Custom Emojis", "application/json", resposnse())
+ }
+ }
+ end
+
+ defp resposnse do
+ %Schema{
+ title: "CustomEmojisResponse",
+ description: "Response schema for custom emojis",
+ type: :array,
+ items: custom_emoji(),
+ example: [
+ %{
+ "category" => "Fun",
+ "shortcode" => "blank",
+ "static_url" => "https://lain.com/emoji/blank.png",
+ "tags" => ["Fun"],
+ "url" => "https://lain.com/emoji/blank.png",
+ "visible_in_picker" => false
+ },
+ %{
+ "category" => "Gif,Fun",
+ "shortcode" => "firefox",
+ "static_url" => "https://lain.com/emoji/Firefox.gif",
+ "tags" => ["Gif", "Fun"],
+ "url" => "https://lain.com/emoji/Firefox.gif",
+ "visible_in_picker" => true
+ },
+ %{
+ "category" => "pack:mixed",
+ "shortcode" => "sadcat",
+ "static_url" => "https://lain.com/emoji/mixed/sadcat.png",
+ "tags" => ["pack:mixed"],
+ "url" => "https://lain.com/emoji/mixed/sadcat.png",
+ "visible_in_picker" => true
+ }
+ ]
+ }
+ end
+
+ defp custom_emoji do
+ %Schema{
+ title: "CustomEmoji",
+ description: "Schema for a CustomEmoji",
+ allOf: [
+ Emoji,
+ %Schema{
+ type: :object,
+ properties: %{
+ category: %Schema{type: :string},
+ tags: %Schema{type: :array}
+ }
+ }
+ ],
+ example: %{
+ "category" => "Fun",
+ "shortcode" => "aaaa",
+ "url" =>
+ "https://files.mastodon.social/custom_emojis/images/000/007/118/original/aaaa.png",
+ "static_url" =>
+ "https://files.mastodon.social/custom_emojis/images/000/007/118/static/aaaa.png",
+ "visible_in_picker" => true,
+ "tags" => ["Gif", "Fun"]
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/domain_block_operation.ex b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex
index dd14837c3..3b7f51ceb 100644
--- a/lib/pleroma/web/api_spec/operations/domain_block_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex
@@ -6,8 +6,6 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
- alias Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest
- alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
@@ -22,7 +20,13 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
security: [%{"oAuth" => ["follow", "read:blocks"]}],
operationId: "DomainBlockController.index",
responses: %{
- 200 => Operation.response("Domain blocks", "application/json", DomainBlocksResponse)
+ 200 =>
+ Operation.response("Domain blocks", "application/json", %Schema{
+ description: "Response schema for domain blocks",
+ type: :array,
+ items: %Schema{type: :string},
+ example: ["google.com", "facebook.com"]
+ })
}
}
end
@@ -40,7 +44,7 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
- prevent following new users from it (but does not remove existing follows)
""",
operationId: "DomainBlockController.create",
- requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true),
+ requestBody: domain_block_request(),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
@@ -54,11 +58,28 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
summary: "Unblock a domain",
description: "Remove a domain block, if it exists in the user's array of blocked domains.",
operationId: "DomainBlockController.delete",
- requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true),
+ requestBody: domain_block_request(),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
}
}
end
+
+ defp domain_block_request do
+ Helpers.request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ properties: %{
+ domain: %Schema{type: :string}
+ },
+ required: [:domain]
+ },
+ required: true,
+ example: %{
+ "domain" => "facebook.com"
+ }
+ )
+ end
end
diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex
new file mode 100644
index 000000000..b5877ca9c
--- /dev/null
+++ b/lib/pleroma/web/api_spec/render_error.ex
@@ -0,0 +1,231 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.RenderError do
+ @behaviour Plug
+
+ import Plug.Conn, only: [put_status: 2]
+ import Phoenix.Controller, only: [json: 2]
+ import Pleroma.Web.Gettext
+
+ @impl Plug
+ def init(opts), do: opts
+
+ @impl Plug
+
+ def call(conn, errors) do
+ errors =
+ Enum.map(errors, fn
+ %{name: nil} = err ->
+ %OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
+
+ err ->
+ err
+ end)
+
+ conn
+ |> put_status(:bad_request)
+ |> json(%{
+ error: errors |> Enum.map(&message/1) |> Enum.join(" "),
+ errors: errors |> Enum.map(&render_error/1)
+ })
+ end
+
+ defp render_error(error) do
+ pointer = OpenApiSpex.path_to_string(error)
+
+ %{
+ title: "Invalid value",
+ source: %{
+ pointer: pointer
+ },
+ message: OpenApiSpex.Cast.Error.message(error)
+ }
+ end
+
+ defp message(%{reason: :invalid_schema_type, type: type, name: name}) do
+ gettext("%{name} - Invalid schema.type. Got: %{type}.",
+ name: name,
+ type: inspect(type)
+ )
+ end
+
+ defp message(%{reason: :null_value, name: name} = error) do
+ case error.type do
+ nil ->
+ gettext("%{name} - null value.", name: name)
+
+ type ->
+ gettext("%{name} - null value where %{type} expected.",
+ name: name,
+ type: type
+ )
+ end
+ end
+
+ defp message(%{reason: :all_of, meta: %{invalid_schema: invalid_schema}}) do
+ gettext(
+ "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed.",
+ invalid_schema: invalid_schema
+ )
+ end
+
+ defp message(%{reason: :any_of, meta: %{failed_schemas: failed_schemas}}) do
+ gettext("Failed to cast value using any of: %{failed_schemas}.",
+ failed_schemas: failed_schemas
+ )
+ end
+
+ defp message(%{reason: :one_of, meta: %{failed_schemas: failed_schemas}}) do
+ gettext("Failed to cast value to one of: %{failed_schemas}.", failed_schemas: failed_schemas)
+ end
+
+ defp message(%{reason: :min_length, length: length, name: name}) do
+ gettext("%{name} - String length is smaller than minLength: %{length}.",
+ name: name,
+ length: length
+ )
+ end
+
+ defp message(%{reason: :max_length, length: length, name: name}) do
+ gettext("%{name} - String length is larger than maxLength: %{length}.",
+ name: name,
+ length: length
+ )
+ end
+
+ defp message(%{reason: :unique_items, name: name}) do
+ gettext("%{name} - Array items must be unique.", name: name)
+ end
+
+ defp message(%{reason: :min_items, length: min, value: array, name: name}) do
+ gettext("%{name} - Array length %{length} is smaller than minItems: %{min}.",
+ name: name,
+ length: length(array),
+ min: min
+ )
+ end
+
+ defp message(%{reason: :max_items, length: max, value: array, name: name}) do
+ gettext("%{name} - Array length %{length} is larger than maxItems: %{}.",
+ name: name,
+ length: length(array),
+ max: max
+ )
+ end
+
+ defp message(%{reason: :multiple_of, length: multiple, value: count, name: name}) do
+ gettext("%{name} - %{count} is not a multiple of %{multiple}.",
+ name: name,
+ count: count,
+ multiple: multiple
+ )
+ end
+
+ defp message(%{reason: :exclusive_max, length: max, value: value, name: name})
+ when value >= max do
+ gettext("%{name} - %{value} is larger than exclusive maximum %{max}.",
+ name: name,
+ value: value,
+ max: max
+ )
+ end
+
+ defp message(%{reason: :maximum, length: max, value: value, name: name})
+ when value > max do
+ gettext("%{name} - %{value} is larger than inclusive maximum %{max}.",
+ name: name,
+ value: value,
+ max: max
+ )
+ end
+
+ defp message(%{reason: :exclusive_multiple, length: min, value: value, name: name})
+ when value <= min do
+ gettext("%{name} - %{value} is smaller than exclusive minimum %{min}.",
+ name: name,
+ value: value,
+ min: min
+ )
+ end
+
+ defp message(%{reason: :minimum, length: min, value: value, name: name})
+ when value < min do
+ gettext("%{name} - %{value} is smaller than inclusive minimum %{min}.",
+ name: name,
+ value: value,
+ min: min
+ )
+ end
+
+ defp message(%{reason: :invalid_type, type: type, value: value, name: name}) do
+ gettext("%{name} - Invalid %{type}. Got: %{value}.",
+ name: name,
+ value: OpenApiSpex.TermType.type(value),
+ type: type
+ )
+ end
+
+ defp message(%{reason: :invalid_format, format: format, name: name}) do
+ gettext("%{name} - Invalid format. Expected %{format}.", name: name, format: inspect(format))
+ end
+
+ defp message(%{reason: :invalid_enum, name: name}) do
+ gettext("%{name} - Invalid value for enum.", name: name)
+ end
+
+ defp message(%{reason: :polymorphic_failed, type: polymorphic_type}) do
+ gettext("Failed to cast to any schema in %{polymorphic_type}",
+ polymorphic_type: polymorphic_type
+ )
+ end
+
+ defp message(%{reason: :unexpected_field, name: name}) do
+ gettext("Unexpected field: %{name}.", name: safe_string(name))
+ end
+
+ defp message(%{reason: :no_value_for_discriminator, name: field}) do
+ gettext("Value used as discriminator for `%{field}` matches no schemas.", name: field)
+ end
+
+ defp message(%{reason: :invalid_discriminator_value, name: field}) do
+ gettext("No value provided for required discriminator `%{field}`.", name: field)
+ end
+
+ defp message(%{reason: :unknown_schema, name: name}) do
+ gettext("Unknown schema: %{name}.", name: name)
+ end
+
+ defp message(%{reason: :missing_field, name: name}) do
+ gettext("Missing field: %{name}.", name: name)
+ end
+
+ defp message(%{reason: :missing_header, name: name}) do
+ gettext("Missing header: %{name}.", name: name)
+ end
+
+ defp message(%{reason: :invalid_header, name: name}) do
+ gettext("Invalid value for header: %{name}.", name: name)
+ end
+
+ defp message(%{reason: :max_properties, meta: meta}) do
+ gettext(
+ "Object property count %{property_count} is greater than maxProperties: %{max_properties}.",
+ property_count: meta.property_count,
+ max_properties: meta.max_properties
+ )
+ end
+
+ defp message(%{reason: :min_properties, meta: meta}) do
+ gettext(
+ "Object property count %{property_count} is less than minProperties: %{min_properties}",
+ property_count: meta.property_count,
+ min_properties: meta.min_properties
+ )
+ end
+
+ defp safe_string(string) do
+ to_string(string) |> String.slice(0..39)
+ end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
new file mode 100644
index 000000000..d54e2158d
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -0,0 +1,167 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Account do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.AccountField
+ alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
+ alias Pleroma.Web.ApiSpec.Schemas.ActorType
+ alias Pleroma.Web.ApiSpec.Schemas.Emoji
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Account",
+ description: "Response schema for an account",
+ type: :object,
+ properties: %{
+ acct: %Schema{type: :string},
+ avatar_static: %Schema{type: :string, format: :uri},
+ avatar: %Schema{type: :string, format: :uri},
+ bot: %Schema{type: :boolean},
+ created_at: %Schema{type: :string, format: "date-time"},
+ display_name: %Schema{type: :string},
+ emojis: %Schema{type: :array, items: Emoji},
+ fields: %Schema{type: :array, items: AccountField},
+ follow_requests_count: %Schema{type: :integer},
+ followers_count: %Schema{type: :integer},
+ following_count: %Schema{type: :integer},
+ header_static: %Schema{type: :string, format: :uri},
+ header: %Schema{type: :string, format: :uri},
+ id: FlakeID,
+ locked: %Schema{type: :boolean},
+ note: %Schema{type: :string, format: :html},
+ statuses_count: %Schema{type: :integer},
+ url: %Schema{type: :string, format: :uri},
+ username: %Schema{type: :string},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ allow_following_move: %Schema{type: :boolean},
+ background_image: %Schema{type: :string, nullable: true},
+ chat_token: %Schema{type: :string},
+ confirmation_pending: %Schema{type: :boolean},
+ hide_favorites: %Schema{type: :boolean},
+ hide_followers_count: %Schema{type: :boolean},
+ hide_followers: %Schema{type: :boolean},
+ hide_follows_count: %Schema{type: :boolean},
+ hide_follows: %Schema{type: :boolean},
+ is_admin: %Schema{type: :boolean},
+ is_moderator: %Schema{type: :boolean},
+ skip_thread_containment: %Schema{type: :boolean},
+ tags: %Schema{type: :array, items: %Schema{type: :string}},
+ unread_conversation_count: %Schema{type: :integer},
+ notification_settings: %Schema{
+ type: :object,
+ properties: %{
+ followers: %Schema{type: :boolean},
+ follows: %Schema{type: :boolean},
+ non_followers: %Schema{type: :boolean},
+ non_follows: %Schema{type: :boolean},
+ privacy_option: %Schema{type: :boolean}
+ }
+ },
+ relationship: AccountRelationship,
+ settings_store: %Schema{
+ type: :object
+ }
+ }
+ },
+ source: %Schema{
+ type: :object,
+ properties: %{
+ fields: %Schema{type: :array, items: AccountField},
+ note: %Schema{type: :string},
+ privacy: VisibilityScope,
+ sensitive: %Schema{type: :boolean},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ actor_type: ActorType,
+ discoverable: %Schema{type: :boolean},
+ no_rich_text: %Schema{type: :boolean},
+ show_role: %Schema{type: :boolean}
+ }
+ }
+ }
+ }
+ },
+ example: %{
+ "acct" => "foobar",
+ "avatar" => "https://mypleroma.com/images/avi.png",
+ "avatar_static" => "https://mypleroma.com/images/avi.png",
+ "bot" => false,
+ "created_at" => "2020-03-24T13:05:58.000Z",
+ "display_name" => "foobar",
+ "emojis" => [],
+ "fields" => [],
+ "follow_requests_count" => 0,
+ "followers_count" => 0,
+ "following_count" => 1,
+ "header" => "https://mypleroma.com/images/banner.png",
+ "header_static" => "https://mypleroma.com/images/banner.png",
+ "id" => "9tKi3esbG7OQgZ2920",
+ "locked" => false,
+ "note" => "cofe",
+ "pleroma" => %{
+ "allow_following_move" => true,
+ "background_image" => nil,
+ "confirmation_pending" => true,
+ "hide_favorites" => true,
+ "hide_followers" => false,
+ "hide_followers_count" => false,
+ "hide_follows" => false,
+ "hide_follows_count" => false,
+ "is_admin" => false,
+ "is_moderator" => false,
+ "skip_thread_containment" => false,
+ "chat_token" =>
+ "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc",
+ "unread_conversation_count" => 0,
+ "tags" => [],
+ "notification_settings" => %{
+ "followers" => true,
+ "follows" => true,
+ "non_followers" => true,
+ "non_follows" => true,
+ "privacy_option" => false
+ },
+ "relationship" => %{
+ "blocked_by" => false,
+ "blocking" => false,
+ "domain_blocking" => false,
+ "endorsed" => false,
+ "followed_by" => false,
+ "following" => false,
+ "id" => "9tKi3esbG7OQgZ2920",
+ "muting" => false,
+ "muting_notifications" => false,
+ "requested" => false,
+ "showing_reblogs" => true,
+ "subscribing" => false
+ },
+ "settings_store" => %{
+ "pleroma-fe" => %{}
+ }
+ },
+ "source" => %{
+ "fields" => [],
+ "note" => "foobar",
+ "pleroma" => %{
+ "actor_type" => "Person",
+ "discoverable" => false,
+ "no_rich_text" => false,
+ "show_role" => true
+ },
+ "privacy" => "public",
+ "sensitive" => false
+ },
+ "statuses_count" => 0,
+ "url" => "https://mypleroma.com/users/foobar",
+ "username" => "foobar"
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/account_field.ex b/lib/pleroma/web/api_spec/schemas/account_field.ex
new file mode 100644
index 000000000..fa97073a0
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/account_field.ex
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.AccountField do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "AccountField",
+ description: "Response schema for account custom fields",
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string},
+ value: %Schema{type: :string, format: :html},
+ verified_at: %Schema{type: :string, format: :"date-time", nullable: true}
+ },
+ example: %{
+ "name" => "Website",
+ "value" =>
+ "<a href=\"https://pleroma.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">pleroma.com</span><span class=\"invisible\"></span></a>",
+ "verified_at" => "2019-08-29T04:14:55.571+00:00"
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/account_relationship.ex b/lib/pleroma/web/api_spec/schemas/account_relationship.ex
new file mode 100644
index 000000000..8b982669e
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/account_relationship.ex
@@ -0,0 +1,44 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "AccountRelationship",
+ description: "Response schema for relationship",
+ type: :object,
+ properties: %{
+ blocked_by: %Schema{type: :boolean},
+ blocking: %Schema{type: :boolean},
+ domain_blocking: %Schema{type: :boolean},
+ endorsed: %Schema{type: :boolean},
+ followed_by: %Schema{type: :boolean},
+ following: %Schema{type: :boolean},
+ id: FlakeID,
+ muting: %Schema{type: :boolean},
+ muting_notifications: %Schema{type: :boolean},
+ requested: %Schema{type: :boolean},
+ showing_reblogs: %Schema{type: :boolean},
+ subscribing: %Schema{type: :boolean}
+ },
+ example: %{
+ "blocked_by" => false,
+ "blocking" => false,
+ "domain_blocking" => false,
+ "endorsed" => false,
+ "followed_by" => false,
+ "following" => false,
+ "id" => "9tKi3esbG7OQgZ2920",
+ "muting" => false,
+ "muting_notifications" => false,
+ "requested" => false,
+ "showing_reblogs" => true,
+ "subscribing" => false
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/actor_type.ex b/lib/pleroma/web/api_spec/schemas/actor_type.ex
new file mode 100644
index 000000000..ac9b46678
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/actor_type.ex
@@ -0,0 +1,13 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ActorType do
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "ActorType",
+ type: :string,
+ enum: ["Application", "Group", "Organization", "Person", "Service"]
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/domain_block_request.ex b/lib/pleroma/web/api_spec/schemas/api_error.ex
index ee9238361..5815df94c 100644
--- a/lib/pleroma/web/api_spec/schemas/domain_block_request.ex
+++ b/lib/pleroma/web/api_spec/schemas/api_error.ex
@@ -2,19 +2,18 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest do
+defmodule Pleroma.Web.ApiSpec.Schemas.ApiError do
alias OpenApiSpex.Schema
+
require OpenApiSpex
OpenApiSpex.schema(%{
- title: "DomainBlockRequest",
+ title: "ApiError",
+ description: "Response schema for API error",
type: :object,
- properties: %{
- domain: %Schema{type: :string}
- },
- required: [:domain],
+ properties: %{error: %Schema{type: :string}},
example: %{
- "domain" => "facebook.com"
+ "error" => "Something went wrong"
}
})
end
diff --git a/lib/pleroma/web/api_spec/schemas/app_create_request.ex b/lib/pleroma/web/api_spec/schemas/app_create_request.ex
deleted file mode 100644
index 8a83abef3..000000000
--- a/lib/pleroma/web/api_spec/schemas/app_create_request.ex
+++ /dev/null
@@ -1,33 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateRequest do
- alias OpenApiSpex.Schema
- require OpenApiSpex
-
- OpenApiSpex.schema(%{
- title: "AppCreateRequest",
- description: "POST body for creating an app",
- type: :object,
- properties: %{
- client_name: %Schema{type: :string, description: "A name for your application."},
- redirect_uris: %Schema{
- type: :string,
- description:
- "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
- },
- scopes: %Schema{
- type: :string,
- description: "Space separated list of scopes. If none is provided, defaults to `read`."
- },
- website: %Schema{type: :string, description: "A URL to the homepage of your app"}
- },
- required: [:client_name, :redirect_uris],
- example: %{
- "client_name" => "My App",
- "redirect_uris" => "https://myapp.com/auth/callback",
- "website" => "https://myapp.com/"
- }
- })
-end
diff --git a/lib/pleroma/web/api_spec/schemas/app_create_response.ex b/lib/pleroma/web/api_spec/schemas/app_create_response.ex
deleted file mode 100644
index f290fb031..000000000
--- a/lib/pleroma/web/api_spec/schemas/app_create_response.ex
+++ /dev/null
@@ -1,33 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateResponse do
- alias OpenApiSpex.Schema
-
- require OpenApiSpex
-
- OpenApiSpex.schema(%{
- title: "AppCreateResponse",
- description: "Response schema for an app",
- type: :object,
- properties: %{
- id: %Schema{type: :string},
- name: %Schema{type: :string},
- client_id: %Schema{type: :string},
- client_secret: %Schema{type: :string},
- redirect_uri: %Schema{type: :string},
- vapid_key: %Schema{type: :string},
- website: %Schema{type: :string, nullable: true}
- },
- example: %{
- "id" => "123",
- "name" => "My App",
- "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
- "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
- "vapid_key" =>
- "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
- "website" => "https://myapp.com/"
- }
- })
-end
diff --git a/lib/pleroma/web/api_spec/schemas/boolean_like.ex b/lib/pleroma/web/api_spec/schemas/boolean_like.ex
new file mode 100644
index 000000000..f3bfb74da
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/boolean_like.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.BooleanLike do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "BooleanLike",
+ description: """
+ The following values will be treated as `false`:
+ - false
+ - 0
+ - "0",
+ - "f",
+ - "F",
+ - "false",
+ - "FALSE",
+ - "off",
+ - "OFF"
+
+ All other non-null values will be treated as `true`
+ """,
+ anyOf: [
+ %Schema{type: :boolean},
+ %Schema{type: :string},
+ %Schema{type: :integer}
+ ]
+ })
+
+ def after_cast(value, _schmea) do
+ {:ok, Pleroma.Web.ControllerHelper.truthy_param?(value)}
+ end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex b/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex
deleted file mode 100644
index d895aca4e..000000000
--- a/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex
+++ /dev/null
@@ -1,16 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse do
- require OpenApiSpex
- alias OpenApiSpex.Schema
-
- OpenApiSpex.schema(%{
- title: "DomainBlocksResponse",
- description: "Response schema for domain blocks",
- type: :array,
- items: %Schema{type: :string},
- example: ["google.com", "facebook.com"]
- })
-end
diff --git a/lib/pleroma/web/api_spec/schemas/emoji.ex b/lib/pleroma/web/api_spec/schemas/emoji.ex
new file mode 100644
index 000000000..26f35e648
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/emoji.ex
@@ -0,0 +1,29 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Emoji do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Emoji",
+ description: "Response schema for an emoji",
+ type: :object,
+ properties: %{
+ shortcode: %Schema{type: :string},
+ url: %Schema{type: :string, format: :uri},
+ static_url: %Schema{type: :string, format: :uri},
+ visible_in_picker: %Schema{type: :boolean}
+ },
+ example: %{
+ "shortcode" => "fatyoshi",
+ "url" =>
+ "https://files.mastodon.social/custom_emojis/images/000/023/920/original/e57ecb623faa0dc9.png",
+ "static_url" =>
+ "https://files.mastodon.social/custom_emojis/images/000/023/920/static/e57ecb623faa0dc9.png",
+ "visible_in_picker" => true
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/flake_id.ex b/lib/pleroma/web/api_spec/schemas/flake_id.ex
new file mode 100644
index 000000000..3b5f6477a
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/flake_id.ex
@@ -0,0 +1,14 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.FlakeID do
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "FlakeID",
+ description:
+ "Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings",
+ type: :string
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex
new file mode 100644
index 000000000..0474b550b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/poll.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Emoji
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Poll",
+ description: "Response schema for account custom fields",
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ expires_at: %Schema{type: :string, format: "date-time"},
+ expired: %Schema{type: :boolean},
+ multiple: %Schema{type: :boolean},
+ votes_count: %Schema{type: :integer},
+ voted: %Schema{type: :boolean},
+ emojis: %Schema{type: :array, items: Emoji},
+ options: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ title: %Schema{type: :string},
+ votes_count: %Schema{type: :integer}
+ }
+ }
+ }
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
new file mode 100644
index 000000000..aef0588d4
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -0,0 +1,226 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Status do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.Emoji
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.Poll
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Status",
+ description: "Response schema for a status",
+ type: :object,
+ properties: %{
+ account: Account,
+ application: %Schema{
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string},
+ website: %Schema{type: :string, nullable: true, format: :uri}
+ }
+ },
+ bookmarked: %Schema{type: :boolean},
+ card: %Schema{
+ type: :object,
+ nullable: true,
+ properties: %{
+ type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
+ provider_name: %Schema{type: :string, nullable: true},
+ provider_url: %Schema{type: :string, format: :uri},
+ url: %Schema{type: :string, format: :uri},
+ image: %Schema{type: :string, nullable: true, format: :uri},
+ title: %Schema{type: :string},
+ description: %Schema{type: :string}
+ }
+ },
+ content: %Schema{type: :string, format: :html},
+ created_at: %Schema{type: :string, format: "date-time"},
+ emojis: %Schema{type: :array, items: Emoji},
+ favourited: %Schema{type: :boolean},
+ favourites_count: %Schema{type: :integer},
+ id: FlakeID,
+ in_reply_to_account_id: %Schema{type: :string, nullable: true},
+ in_reply_to_id: %Schema{type: :string, nullable: true},
+ language: %Schema{type: :string, nullable: true},
+ media_attachments: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ url: %Schema{type: :string, format: :uri},
+ remote_url: %Schema{type: :string, format: :uri},
+ preview_url: %Schema{type: :string, format: :uri},
+ text_url: %Schema{type: :string, format: :uri},
+ description: %Schema{type: :string},
+ type: %Schema{type: :string, enum: ["image", "video", "audio", "unknown"]},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{mime_type: %Schema{type: :string}}
+ }
+ }
+ }
+ },
+ mentions: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ acct: %Schema{type: :string},
+ username: %Schema{type: :string},
+ url: %Schema{type: :string, format: :uri}
+ }
+ }
+ },
+ muted: %Schema{type: :boolean},
+ pinned: %Schema{type: :boolean},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ content: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
+ conversation_id: %Schema{type: :integer},
+ direct_conversation_id: %Schema{type: :string, nullable: true},
+ emoji_reactions: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string},
+ count: %Schema{type: :integer},
+ me: %Schema{type: :boolean}
+ }
+ }
+ },
+ expires_at: %Schema{type: :string, format: "date-time", nullable: true},
+ in_reply_to_account_acct: %Schema{type: :string, nullable: true},
+ local: %Schema{type: :boolean},
+ spoiler_text: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
+ thread_muted: %Schema{type: :boolean}
+ }
+ },
+ poll: %Schema{type: Poll, nullable: true},
+ reblog: %Schema{
+ allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
+ nullable: true
+ },
+ reblogged: %Schema{type: :boolean},
+ reblogs_count: %Schema{type: :integer},
+ replies_count: %Schema{type: :integer},
+ sensitive: %Schema{type: :boolean},
+ spoiler_text: %Schema{type: :string},
+ tags: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string},
+ url: %Schema{type: :string, format: :uri}
+ }
+ }
+ },
+ uri: %Schema{type: :string, format: :uri},
+ url: %Schema{type: :string, nullable: true, format: :uri},
+ visibility: VisibilityScope
+ },
+ example: %{
+ "account" => %{
+ "acct" => "nick6",
+ "avatar" => "http://localhost:4001/images/avi.png",
+ "avatar_static" => "http://localhost:4001/images/avi.png",
+ "bot" => false,
+ "created_at" => "2020-04-07T19:48:51.000Z",
+ "display_name" => "Test テスト User 6",
+ "emojis" => [],
+ "fields" => [],
+ "followers_count" => 1,
+ "following_count" => 0,
+ "header" => "http://localhost:4001/images/banner.png",
+ "header_static" => "http://localhost:4001/images/banner.png",
+ "id" => "9toJCsKN7SmSf3aj5c",
+ "locked" => false,
+ "note" => "Tester Number 6",
+ "pleroma" => %{
+ "background_image" => nil,
+ "confirmation_pending" => false,
+ "hide_favorites" => true,
+ "hide_followers" => false,
+ "hide_followers_count" => false,
+ "hide_follows" => false,
+ "hide_follows_count" => false,
+ "is_admin" => false,
+ "is_moderator" => false,
+ "relationship" => %{
+ "blocked_by" => false,
+ "blocking" => false,
+ "domain_blocking" => false,
+ "endorsed" => false,
+ "followed_by" => false,
+ "following" => true,
+ "id" => "9toJCsKN7SmSf3aj5c",
+ "muting" => false,
+ "muting_notifications" => false,
+ "requested" => false,
+ "showing_reblogs" => true,
+ "subscribing" => false
+ },
+ "skip_thread_containment" => false,
+ "tags" => []
+ },
+ "source" => %{
+ "fields" => [],
+ "note" => "Tester Number 6",
+ "pleroma" => %{"actor_type" => "Person", "discoverable" => false},
+ "sensitive" => false
+ },
+ "statuses_count" => 1,
+ "url" => "http://localhost:4001/users/nick6",
+ "username" => "nick6"
+ },
+ "application" => %{"name" => "Web", "website" => nil},
+ "bookmarked" => false,
+ "card" => nil,
+ "content" => "foobar",
+ "created_at" => "2020-04-07T19:48:51.000Z",
+ "emojis" => [],
+ "favourited" => false,
+ "favourites_count" => 0,
+ "id" => "9toJCu5YZW7O7gfvH6",
+ "in_reply_to_account_id" => nil,
+ "in_reply_to_id" => nil,
+ "language" => nil,
+ "media_attachments" => [],
+ "mentions" => [],
+ "muted" => false,
+ "pinned" => false,
+ "pleroma" => %{
+ "content" => %{"text/plain" => "foobar"},
+ "conversation_id" => 345_972,
+ "direct_conversation_id" => nil,
+ "emoji_reactions" => [],
+ "expires_at" => nil,
+ "in_reply_to_account_acct" => nil,
+ "local" => true,
+ "spoiler_text" => %{"text/plain" => ""},
+ "thread_muted" => false
+ },
+ "poll" => nil,
+ "reblog" => nil,
+ "reblogged" => false,
+ "reblogs_count" => 0,
+ "replies_count" => 0,
+ "sensitive" => false,
+ "spoiler_text" => "",
+ "tags" => [],
+ "uri" => "http://localhost:4001/objects/0f5dad44-0e9e-4610-b377-a2631e499190",
+ "url" => "http://localhost:4001/notice/9toJCu5YZW7O7gfvH6",
+ "visibility" => "private"
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
new file mode 100644
index 000000000..8c81a4d73
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
@@ -0,0 +1,14 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.VisibilityScope do
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "VisibilityScope",
+ description: "Status visibility",
+ type: :string,
+ enum: ["public", "unlisted", "private", "direct"]
+ })
+end
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index c1cd15bb2..244cf2be5 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -84,14 +84,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
%__MODULE__{draft | attachments: attachments}
end
- defp in_reply_to(draft) do
- case Map.get(draft.params, "in_reply_to_status_id") do
- "" -> draft
- nil -> draft
- id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
- end
+ defp in_reply_to(%{params: %{"in_reply_to_status_id" => ""}} = draft), do: draft
+
+ defp in_reply_to(%{params: %{"in_reply_to_status_id" => id}} = draft) when is_binary(id) do
+ %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
end
+ defp in_reply_to(%{params: %{"in_reply_to_status_id" => %Activity{} = in_reply_to}} = draft) do
+ %__MODULE__{draft | in_reply_to: in_reply_to}
+ end
+
+ defp in_reply_to(draft), do: draft
+
defp in_reply_to_conversation(draft) do
in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index c56756a3d..4112e441a 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.FollowingRelationship
+ alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
@@ -61,6 +62,7 @@ defmodule Pleroma.Web.CommonAPI do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
+ {:ok, _notifications} <- Notification.dismiss(follow_activity),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
@@ -86,8 +88,9 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def repeat(id_or_ap_id, user, params \\ %{}) do
- with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
+ def repeat(id, user, params \\ %{}) do
+ with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
+ {:find_activity, Activity.get_by_id(id)},
object <- Object.normalize(activity),
announce_activity <- Utils.get_existing_announce(user.ap_id, object),
public <- public_announce?(object, params) do
@@ -102,8 +105,9 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def unrepeat(id_or_ap_id, user) do
- with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
+ def unrepeat(id, user) do
+ with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
+ {:find_activity, Activity.get_by_id(id)} do
object = Object.normalize(activity)
ActivityPub.unannounce(user, object)
else
@@ -160,8 +164,9 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def unfavorite(id_or_ap_id, user) do
- with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
+ def unfavorite(id, user) do
+ with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
+ {:find_activity, Activity.get_by_id(id)} do
object = Object.normalize(activity)
ActivityPub.unlike(user, object)
else
@@ -332,32 +337,12 @@ defmodule Pleroma.Web.CommonAPI do
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 = Map.put(user.source_data, "tag", emoji)
-
- user =
- case User.update_source_data(user, source_data) do
- {:ok, user} -> user
- _ -> user
- end
-
- ActivityPub.update(%{
- local: true,
- to: [Pleroma.Constants.as_public(), user.follower_address],
- cc: [],
- actor: user.ap_id,
- object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
- })
- end
-
- def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
+ def pin(id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
data: %{"type" => "Create"},
object: %Object{data: %{"type" => object_type}}
- } = activity <- get_by_id_or_ap_id(id_or_ap_id),
+ } = activity <- Activity.get_by_id_with_object(id),
true <- object_type in ["Note", "Article", "Question"],
true <- Visibility.is_public?(activity),
{:ok, _user} <- User.add_pinnned_activity(user, activity) do
@@ -368,8 +353,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def unpin(id_or_ap_id, user) do
- with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
+ def unpin(id, user) do
+ with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
{:ok, _user} <- User.remove_pinnned_activity(user, activity) do
{:ok, activity}
else
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 635e7cd38..945e63e22 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -10,7 +10,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
- alias Pleroma.Emoji
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Plugs.AuthenticationPlug
@@ -18,30 +17,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
- alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
require Logger
require Pleroma.Constants
- # This is a hack for twidere.
- def get_by_id_or_ap_id(id) do
- activity =
- with true <- FlakeId.flake_id?(id),
- %Activity{} = activity <- Activity.get_by_id_with_object(id) do
- activity
- else
- _ -> Activity.get_create_by_object_ap_id_with_object(id)
- end
-
- activity &&
- if activity.data["type"] == "Create" do
- activity
- else
- Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
- end
- end
-
def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
attachments_from_ids_descs(ids, desc)
end
@@ -175,7 +155,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"replies" => %{"type" => "Collection", "totalItems" => 0}
}
- {note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
+ {note, Map.merge(emoji, Pleroma.Emoji.Formatter.get_emoji_map(option))}
end)
end_time =
@@ -431,19 +411,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
- 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()}#{path}"},
- "name" => ":#{shortcode}:"
- }
- end)
- end
-
def maybe_notify_to_recipients(
recipients,
%Activity{data: %{"to" => to, "type" => _type}} = _activity
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 4780081b2..eb97ae975 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -82,8 +82,9 @@ defmodule Pleroma.Web.ControllerHelper do
end
end
- def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do
- case Pleroma.User.get_cached_by_id(id) do
+ def assign_account_by_id(conn, _) do
+ # TODO: use `conn.params[:id]` only after moving to OpenAPI
+ case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do
%Pleroma.User{} = account -> assign(conn, :account, account)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex
index c13518030..0d9d578fc 100644
--- a/lib/pleroma/web/fallback_redirect_controller.ex
+++ b/lib/pleroma/web/fallback_redirect_controller.ex
@@ -4,7 +4,9 @@
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
+
require Logger
+
alias Pleroma.User
alias Pleroma.Web.Metadata
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index fd904ef0a..f5803578d 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -72,19 +72,24 @@ defmodule Pleroma.Web.Federator do
# actor shouldn't be acting on objects outside their own AP server.
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.normalize(params["id"]),
- :ok <- Containment.contain_origin_from_id(params["actor"], params),
+ {_, :ok} <-
+ {:correct_origin?, Containment.contain_origin_from_id(params["actor"], params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, activity}
else
+ {:correct_origin?, _} ->
+ Logger.debug("Origin containment failure for #{params["id"]}")
+ {:error, :origin_containment_failed}
+
%Activity{} ->
Logger.debug("Already had #{params["id"]}")
- :error
+ {:error, :already_present}
- _e ->
+ e ->
# Just drop those for now
Logger.debug("Unhandled activity")
Logger.debug(Jason.encode!(params, pretty: true))
- :error
+ {:error, e}
end
end
diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex
index e18adaea8..1ae03e7e2 100644
--- a/lib/pleroma/web/feed/feed_view.ex
+++ b/lib/pleroma/web/feed/feed_view.ex
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.Feed.FeedView do
def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}")
def prepare_activity(activity, opts \\ []) do
- object = activity_object(activity)
+ object = Object.normalize(activity)
actor =
if opts[:actor] do
@@ -33,7 +33,6 @@ defmodule Pleroma.Web.Feed.FeedView do
%{
activity: activity,
data: Map.get(object, :data),
- object: object,
actor: actor
}
end
@@ -68,9 +67,7 @@ defmodule Pleroma.Web.Feed.FeedView do
def last_activity(activities), do: List.last(activities)
- def activity_object(activity), do: Object.normalize(activity)
-
- def activity_title(%{data: %{"content" => content}}, opts \\ %{}) do
+ def activity_title(%{"content" => content}, opts \\ %{}) do
content
|> Pleroma.Web.Metadata.Utils.scrub_html()
|> Pleroma.Emoji.Formatter.demojify()
@@ -78,7 +75,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|> escape()
end
- def activity_content(%{data: %{"content" => content}}) do
+ def activity_content(%{"content" => content}) do
content
|> String.replace(~r/[\n\r]/, "")
|> escape()
diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex
index 43649ad26..d0d8bc8eb 100644
--- a/lib/pleroma/web/masto_fe_controller.ex
+++ b/lib/pleroma/web/masto_fe_controller.ex
@@ -5,19 +5,25 @@
defmodule Pleroma.Web.MastoFEController do
use Pleroma.Web, :controller
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings)
# Note: :index action handles attempt of unauthenticated access to private instance with redirect
+ plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action == :index)
+
plug(
OAuthScopesPlug,
- %{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true}
+ %{scopes: ["read"], fallback: :proceed_unauthenticated}
when action == :index
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index)
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :manifest
+ )
@doc "GET /web/*path"
def index(%{assigns: %{user: user, token: token}} = conn, _params)
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 7da1a11f6..1eedf02d6 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
skip_relationships?: 1
]
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
@@ -21,20 +22,33 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
+ alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
+ plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+
+ plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
+
+ plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
+
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
- when action == :show
+ when action in [:show, :followers, :following]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
+ when action == :statuses
)
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
- when action in [:endorsements, :verify_credentials, :followers, :following]
+ when action in [:verify_credentials, :endorsements, :identity_proofs]
)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
@@ -53,21 +67,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
- # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
plug(
OAuthScopesPlug,
- %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
+ %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
- plug(
- Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- when action not in [:create, :show, :statuses]
- )
-
@relationship_actions [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
@@ -82,25 +90,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
+
@doc "POST /api/v1/accounts"
- def create(
- %{assigns: %{app: app}} = conn,
- %{"username" => nickname, "password" => _, "agreement" => true} = params
- ) do
+ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
params =
params
|> Map.take([
- "email",
- "captcha_solution",
- "captcha_token",
- "captcha_answer_data",
- "token",
- "password"
+ :email,
+ :bio,
+ :captcha_solution,
+ :captcha_token,
+ :captcha_answer_data,
+ :token,
+ :password,
+ :fullname
])
- |> Map.put("nickname", nickname)
- |> Map.put("fullname", params["fullname"] || nickname)
- |> Map.put("bio", params["bio"] || "")
- |> Map.put("confirm", params["password"])
+ |> Map.put(:nickname, params.username)
+ |> Map.put(:fullname, Map.get(params, :fullname, params.username))
+ |> Map.put(:confirm, params.password)
+ |> Map.put(:trusted_app, app.trusted)
with :ok <- validate_email_param(params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
@@ -124,7 +133,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
render_error(conn, :forbidden, "Invalid credentials")
end
- defp validate_email_param(%{"email" => _}), do: :ok
+ defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
defp validate_email_param(_) do
case Pleroma.Config.get([:instance, :account_activation_required]) do
@@ -146,9 +155,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "PATCH /api/v1/accounts/update_credentials"
- def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
+ def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
user = original_user
+ params =
+ params
+ |> Enum.filter(fn {_, value} -> not is_nil(value) end)
+ |> Enum.into(%{})
+
user_params =
[
:no_rich_text,
@@ -164,28 +178,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
:discoverable
]
|> Enum.reduce(%{}, fn key, acc ->
- add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
+ add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
end)
- |> add_if_present(params, "display_name", :name)
- |> add_if_present(params, "note", :bio)
- |> add_if_present(params, "avatar", :avatar)
- |> add_if_present(params, "header", :banner)
- |> add_if_present(params, "pleroma_background_image", :background)
+ |> add_if_present(params, :display_name, :name)
+ |> add_if_present(params, :note, :bio)
+ |> add_if_present(params, :avatar, :avatar)
+ |> add_if_present(params, :header, :banner)
+ |> add_if_present(params, :pleroma_background_image, :background)
|> add_if_present(
params,
- "fields_attributes",
+ :fields_attributes,
:raw_fields,
&{:ok, normalize_fields_attributes(&1)}
)
- |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
- |> add_if_present(params, "default_scope", :default_scope)
- |> add_if_present(params, "actor_type", :actor_type)
+ |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
+ |> add_if_present(params, :default_scope, :default_scope)
+ |> add_if_present(params, :actor_type, :actor_type)
changeset = User.update_changeset(user, user_params)
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")
@@ -194,7 +206,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
with true <- Map.has_key?(params, params_field),
- {:ok, new_value} <- value_function.(params[params_field]) do
+ {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
Map.put(map, map_field, new_value)
else
_ -> map
@@ -205,12 +217,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end)
else
- fields
+ Enum.map(fields, fn
+ %{} = field -> %{"name" => field.name, "value" => field.value}
+ field -> field
+ end)
end
end
@doc "GET /api/v1/accounts/relationships"
- def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
targets = User.get_all_by_ids(List.wrap(id))
render(conn, "relationships.json", user: user, targets: targets)
@@ -220,7 +235,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id"
- def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
+ def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
true <- User.visible_for?(user, for_user) do
render(conn, "show.json", user: user, for: for_user)
@@ -231,12 +246,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
+ with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
true <- User.visible_for?(user, reading_user) do
params =
params
- |> Map.put("tag", params["tagged"])
- |> Map.delete("godmode")
+ |> Map.delete(:tagged)
+ |> Enum.filter(&(not is_nil(&1)))
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
+ |> Map.put("tag", params[:tagged])
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
@@ -256,6 +273,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "GET /api/v1/accounts/:id/followers"
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ params =
+ params
+ |> Enum.map(fn {key, value} -> {to_string(key), value} end)
+ |> Enum.into(%{})
+
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
@@ -270,6 +292,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "GET /api/v1/accounts/:id/following"
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ params =
+ params
+ |> Enum.map(fn {key, value} -> {to_string(key), value} end)
+ |> Enum.into(%{})
+
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
@@ -293,11 +320,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/follow"
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
- {:error, :not_found}
+ {:error, "Can not follow yourself"}
end
- def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
- with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
+ def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
+ with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -306,7 +333,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/unfollow"
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
- {:error, :not_found}
+ {:error, "Can not unfollow yourself"}
end
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
@@ -316,10 +343,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/accounts/:id/mute"
- def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
- notifications? = params |> Map.get("notifications", true) |> truthy_param?()
-
- with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
+ def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
+ with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -356,14 +381,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/follows"
- def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
- with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
- {_, true} <- {:followed, follower.id != followed.id},
- {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
- render(conn, "show.json", user: followed, for: follower)
- else
- {:followed, _} -> {:error, :not_found}
- {:error, message} -> json_response(conn, :forbidden, %{error: message})
+ def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
+ case User.get_cached_by_nickname(uri) do
+ %User{} = user ->
+ conn
+ |> assign(:account, user)
+ |> follow(%{})
+
+ nil ->
+ {:error, :not_found}
end
end
@@ -380,6 +406,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/endorsements"
- def endorsements(conn, params),
- do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
+ def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
+
+ @doc "GET /api/v1/identity_proofs"
+ def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 005c60444..408e11474 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.AppController do
use Pleroma.Web, :controller
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Repo
alias Pleroma.Web.OAuth.App
@@ -13,7 +14,14 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
+ when action == :create
+ )
+
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
+
plug(OpenApiSpex.Plug.CastAndValidate)
@local_mastodon_name "Mastodon-Local"
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index 37b389382..753b3db3e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -13,10 +13,10 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- @local_mastodon_name "Mastodon-Local"
-
plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
+ @local_mastodon_name "Mastodon-Local"
+
@doc "GET /web/login"
def login(%{assigns: %{user: %User{}}} = conn, _params) do
redirect(conn, to: local_mastodon_root_path(conn))
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
index 7c9b11bf1..c44641526 100644
--- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -14,9 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
- plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :read)
-
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)
@doc "GET /api/v1/conversations"
def index(%{assigns: %{user: user}} = conn, params) do
@@ -28,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
end
@doc "POST /api/v1/conversations/:id/read"
- def read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
+ def mark_as_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
diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
index d82de1db5..000ad743f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -5,6 +5,16 @@
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
use Pleroma.Web, :controller
+ plug(OpenApiSpex.Plug.CastAndValidate)
+
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ when action == :index
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.CustomEmojiOperation
+
def index(conn, _params) do
render(conn, "index.json", custom_emojis: Pleroma.Emoji.get_all())
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
index 84de79413..c4fa383f2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
%{scopes: ["follow", "write:blocks"]} when action != :index
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@doc "GET /api/v1/domain_blocks"
def index(%{assigns: %{user: user}} = conn, _) do
json(conn, Map.get(user, :domain_blocks, []))
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index 7b0b937a2..7fd0562c9 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -17,8 +17,6 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
%{scopes: ["write:filters"]} when action not in @oauth_read_actions
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@doc "GET /api/v1/filters"
def index(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user)
diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
index 1ca86f63f..25f2269b9 100644
--- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
%{scopes: ["follow", "write:follows"]} when action != :index
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@doc "GET /api/v1/follow_requests"
def index(%{assigns: %{user: followed}} = conn, _params) do
follow_requests = User.get_follow_requests(followed)
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
index 27b5b1a52..237f85677 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -5,6 +5,12 @@
defmodule Pleroma.Web.MastodonAPI.InstanceController do
use Pleroma.Web, :controller
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ when action in [:show, :peers]
+ )
+
@doc "GET /api/v1/instance"
def show(conn, _params) do
render(conn, "show.json")
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
index dac4daa7b..bfe856025 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -11,16 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
plug(:list_by_id_and_user when action not in [:index, :create])
- plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:index, :show, :list_accounts])
+ @oauth_read_actions [:index, :show, :list_accounts]
+
+ plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
plug(
OAuthScopesPlug,
%{scopes: ["write:lists"]}
- when action in [:create, :update, :delete, :add_to_list, :remove_from_list]
+ when action not in @oauth_read_actions
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
# GET /api/v1/lists
diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
index 58e8a30c2..9f9d4574e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
# GET /api/v1/markers
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 14075307d..e7767de4e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -3,21 +3,33 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
+ @moduledoc """
+ Contains stubs for unimplemented Mastodon API endpoints.
+
+ Note: instead of routing directly to this controller's action,
+ it's preferable to define an action in relevant (non-generic) controller,
+ set up OAuth rules for it and call this controller's function from it.
+ """
+
use Pleroma.Web, :controller
require Logger
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ when action in [:empty_array, :empty_object]
+ )
+
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- # Stubs for unimplemented mastodon api
- #
def empty_array(conn, _) do
- Logger.debug("Unimplemented, returning an empty array")
+ Logger.debug("Unimplemented, returning an empty array (list)")
json(conn, [])
end
def empty_object(conn, _) do
- Logger.debug("Unimplemented, returning an empty object")
+ Logger.debug("Unimplemented, returning an empty object (map)")
json(conn, %{})
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
index 2b6f00952..e36751220 100644
--- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -15,8 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
plug(OAuthScopesPlug, %{scopes: ["write:media"]})
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@doc "POST /api/v1/media"
def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index 7fb536b09..311405277 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -20,8 +20,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
# GET /api/v1/notifications
def index(conn, %{"account_id" => account_id} = params) do
case Pleroma.User.get_cached_by_id(account_id) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
index d9f894118..af9b66eff 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -22,8 +22,6 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@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),
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
index f5782be13..9fbaa7bd1 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -11,8 +11,6 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@doc "POST /api/v1/reports"
def create(%{assigns: %{user: user}} = conn, params) do
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
index e1e6bd89b..899b78873 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -18,8 +18,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@doc "GET /api/v1/scheduled_statuses"
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index c258742dd..cd49da6ad 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -21,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ # Note: on private instances auth is required (EnsurePublicOrAuthenticatedPlug is not skipped)
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 397dd10e3..9eea2e9eb 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -24,6 +24,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+ plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
+
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
plug(
@@ -77,8 +79,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :show])
-
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
plug(
@@ -127,7 +127,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
def create(
%{assigns: %{user: user}} = conn,
%{"status" => _, "scheduled_at" => scheduled_at} = params
- ) do
+ )
+ when not is_nil(scheduled_at) do
params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
@@ -357,7 +358,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/favourites"
- def favourites(%{assigns: %{user: user}} = conn, params) do
+ def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
activities =
ActivityPub.fetch_favourites(
user,
diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
index 11df6fc4a..d184ea1d0 100644
--- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
@moduledoc "The module represents functions to manage user subscriptions."
use Pleroma.Web, :controller
- alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View
alias Pleroma.Web.Push
alias Pleroma.Web.Push.Subscription
@@ -14,17 +13,15 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(:restrict_push_enabled)
# Creates PushSubscription
# POST /api/v1/push/subscription
#
def create(%{assigns: %{user: user, token: token}} = conn, params) do
- with true <- Push.enabled(),
- {:ok, _} <- Subscription.delete_if_exists(user, token),
+ with {:ok, _} <- Subscription.delete_if_exists(user, token),
{:ok, subscription} <- Subscription.create(user, token, params) do
- view = View.render("push_subscription.json", subscription: subscription)
- json(conn, view)
+ render(conn, "show.json", subscription: subscription)
end
end
@@ -32,10 +29,8 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# GET /api/v1/push/subscription
#
def get(%{assigns: %{user: user, token: token}} = conn, _params) do
- with true <- Push.enabled(),
- {:ok, subscription} <- Subscription.get(user, token) do
- view = View.render("push_subscription.json", subscription: subscription)
- json(conn, view)
+ with {:ok, subscription} <- Subscription.get(user, token) do
+ render(conn, "show.json", subscription: subscription)
end
end
@@ -43,10 +38,8 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# PUT /api/v1/push/subscription
#
def update(%{assigns: %{user: user, token: token}} = conn, params) do
- with true <- Push.enabled(),
- {:ok, subscription} <- Subscription.update(user, token, params) do
- view = View.render("push_subscription.json", subscription: subscription)
- json(conn, view)
+ with {:ok, subscription} <- Subscription.update(user, token, params) do
+ render(conn, "show.json", subscription: subscription)
end
end
@@ -54,11 +47,20 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# DELETE /api/v1/push/subscription
#
def delete(%{assigns: %{user: user, token: token}} = conn, _params) do
- with true <- Push.enabled(),
- {:ok, _response} <- Subscription.delete(user, token),
+ with {:ok, _response} <- Subscription.delete(user, token),
do: json(conn, %{})
end
+ defp restrict_push_enabled(conn, _) do
+ if Push.enabled() do
+ conn
+ else
+ conn
+ |> render_error(:forbidden, "Web push subscription is disabled on this Pleroma instance")
+ |> halt()
+ end
+ end
+
# fallback action
#
def errors(conn, {:error, :not_found}) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
index 0cdc7bd8d..c93a43969 100644
--- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
@@ -5,10 +5,13 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller
+ alias Pleroma.Plugs.OAuthScopesPlug
+
require Logger
+ plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+
@doc "GET /api/v1/suggestions"
- def index(conn, _) do
- json(conn, [])
- end
+ def index(conn, params),
+ do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index b3c58005e..2d67e19da 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -9,11 +9,14 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1]
alias Pleroma.Pagination
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag])
+
# TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
# https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
@@ -26,7 +29,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :public)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+ when action in [:public, :hashtag]
+ )
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
@@ -37,6 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
+ |> Map.put("reply_filtering_user", user)
|> Map.put("user", user)
recipients = [user.ap_id | User.following(user)]
@@ -93,13 +101,16 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key])
- if not (restrict? and is_nil(user)) do
+ if restrict? and is_nil(user) do
+ render_error(conn, :unauthorized, "authorization required for timeline view")
+ else
activities =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
+ |> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
conn
@@ -110,12 +121,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
as: :activity,
skip_relationships: skip_relationships?(params)
)
- else
- render_error(conn, :unauthorized, "authorization required for timeline view")
end
end
- def hashtag_fetching(params, user, local_only) do
+ defp hashtag_fetching(params, user, local_only) do
tags =
[params["tag"], params["any"]]
|> List.flatten()
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 8fb96a22a..b4b61e74c 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -181,13 +181,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bot = user.actor_type in ["Application", "Service"]
emojis =
- (user.source_data["tag"] || [])
- |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
- |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
+ Enum.map(user.emoji, fn {shortcode, url} ->
%{
- "shortcode" => String.trim(name, ":"),
- "url" => MediaProxy.url(url),
- "static_url" => MediaProxy.url(url),
+ "shortcode" => shortcode,
+ "url" => url,
+ "static_url" => url,
"visible_in_picker" => false
}
end)
diff --git a/lib/pleroma/web/mastodon_api/views/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex
index d934e2107..36071cd25 100644
--- a/lib/pleroma/web/mastodon_api/views/app_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/app_view.ex
@@ -7,6 +7,21 @@ defmodule Pleroma.Web.MastodonAPI.AppView do
alias Pleroma.Web.OAuth.App
+ def render("index.json", %{apps: apps, count: count, page_size: page_size, admin: true}) do
+ %{
+ apps: render_many(apps, Pleroma.Web.MastodonAPI.AppView, "show.json", %{admin: true}),
+ count: count,
+ page_size: page_size
+ }
+ end
+
+ def render("show.json", %{admin: true, app: %App{} = app} = assigns) do
+ "show.json"
+ |> render(Map.delete(assigns, :admin))
+ |> Map.put(:trusted, app.trusted)
+ |> Map.put(:id, app.id)
+ end
+
def render("show.json", %{app: %App{} = app}) do
%{
id: app.id |> to_string,
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index 734ffbf39..4da1ab67f 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -117,14 +117,14 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
# Note: :skip_relationships option being applied to _account_ rendering (here)
put_target(response, activity, reading_user, render_opts)
- "follow" ->
- response
-
"pleroma:emoji_reaction" ->
response
|> put_status(parent_activity_fn.(), reading_user, render_opts)
|> put_emoji(activity)
+ type when type in ["follow", "follow_request"] ->
+ response
+
_ ->
nil
end
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
index 40edbb213..59a5deb28 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
expired: expired,
multiple: multiple,
votes_count: votes_count,
+ voters_count: (multiple || nil) && voters_count(object),
options: options,
voted: voted?(params),
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
@@ -62,6 +63,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
end)
end
+ defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do
+ length(voters)
+ end
+
+ defp voters_count(_), do: 0
+
defp voted?(%{object: object} = opts) do
if opts[:for] do
existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index b5850e1ae..24167f66f 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -45,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end)
end
- defp get_user(ap_id) do
+ def get_user(ap_id, fake_record_fallback \\ true) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
user
@@ -53,8 +53,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
user = User.get_by_guessed_nickname(ap_id) ->
user
- true ->
+ fake_record_fallback ->
+ # TODO: refactor (fake records is never a good idea)
User.error_user(ap_id)
+
+ true ->
+ nil
end
end
@@ -97,7 +101,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
UserRelationship.view_relationships_option(nil, [])
true ->
- actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"]))
+ # Note: unresolved users are filtered out
+ actors =
+ (activities ++ parent_activities)
+ |> Enum.map(&get_user(&1.data["actor"], false))
+ |> Enum.filter(& &1)
UserRelationship.view_relationships_option(reading_user, actors,
source_mutes_only: opts[:skip_relationships]
@@ -521,11 +529,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
"""
@spec build_tags(list(any())) :: list(map())
def build_tags(object_tags) when is_list(object_tags) do
- object_tags = for tag when is_binary(tag) <- object_tags, do: tag
-
- Enum.reduce(object_tags, [], fn tag, tags ->
- tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
- end)
+ object_tags
+ |> Enum.filter(&is_binary/1)
+ |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
end
def build_tags(_), do: []
diff --git a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
index d32cef6e2..7c67cc924 100644
--- a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
@@ -2,11 +2,11 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
+defmodule Pleroma.Web.MastodonAPI.SubscriptionView do
use Pleroma.Web, :view
alias Pleroma.Web.Push
- def render("push_subscription.json", %{subscription: subscription}) do
+ def render("show.json", %{subscription: subscription}) do
%{
id: to_string(subscription.id),
endpoint: subscription.endpoint,
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 1a09ac62a..4657a4383 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller
+
alias Pleroma.ReverseProxy
alias Pleroma.Web.MediaProxy
diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
index 04d823b36..1ed6ee521 100644
--- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
+++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
def user_exists(conn, %{"user" => username}) do
- with %User{} <- Repo.get_by(User, nickname: username, local: true) do
+ with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do
conn
|> json(true)
else
@@ -26,7 +26,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
end
def check_password(conn, %{"user" => username, "pass" => password}) do
- with %User{password_hash: password_hash} <-
+ with %User{password_hash: password_hash, deactivated: false} <-
Repo.get_by(User, nickname: username, local: true),
true <- Pbkdf2.checkpw(password, password_hash) do
conn
diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex
index 01ed326f4..6a6d5f2e2 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
+ import Ecto.Query
alias Pleroma.Repo
@type t :: %__MODULE__{}
@@ -16,14 +17,24 @@ defmodule Pleroma.Web.OAuth.App do
field(:website, :string)
field(:client_id, :string)
field(:client_secret, :string)
+ field(:trusted, :boolean, default: false)
+
+ has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all)
+ has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all)
timestamps()
end
+ @spec changeset(App.t(), map()) :: Ecto.Changeset.t()
+ def changeset(struct, params) do
+ cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted])
+ end
+
+ @spec register_changeset(App.t(), map()) :: Ecto.Changeset.t()
def register_changeset(struct, params \\ %{}) do
changeset =
struct
- |> cast(params, [:client_name, :redirect_uris, :scopes, :website])
+ |> changeset(params)
|> validate_required([:client_name, :redirect_uris, :scopes])
if changeset.valid? do
@@ -41,6 +52,21 @@ defmodule Pleroma.Web.OAuth.App do
end
end
+ @spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
+ def create(params) do
+ with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do
+ Repo.insert(changeset)
+ end
+ end
+
+ @spec update(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
+ def update(params) do
+ with %__MODULE__{} = app <- Repo.get(__MODULE__, params["id"]),
+ changeset <- changeset(app, params) do
+ Repo.update(changeset)
+ end
+ end
+
@doc """
Gets app by attrs or create new with attrs.
And updates the scopes if need.
@@ -65,4 +91,58 @@ defmodule Pleroma.Web.OAuth.App do
|> change(%{scopes: scopes})
|> Repo.update()
end
+
+ @spec search(map()) :: {:ok, [App.t()], non_neg_integer()}
+ def search(params) do
+ query = from(a in __MODULE__)
+
+ query =
+ if params[:client_name] do
+ from(a in query, where: a.client_name == ^params[:client_name])
+ else
+ query
+ end
+
+ query =
+ if params[:client_id] do
+ from(a in query, where: a.client_id == ^params[:client_id])
+ else
+ query
+ end
+
+ query =
+ if Map.has_key?(params, :trusted) do
+ from(a in query, where: a.trusted == ^params[:trusted])
+ else
+ query
+ end
+
+ query =
+ from(u in query,
+ limit: ^params[:page_size],
+ offset: ^((params[:page] - 1) * params[:page_size])
+ )
+
+ count = Repo.aggregate(__MODULE__, :count, :id)
+
+ {:ok, Repo.all(query), count}
+ end
+
+ @spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
+ def destroy(id) do
+ with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
+ Repo.delete(app)
+ end
+ end
+
+ @spec errors(Ecto.Changeset.t()) :: map()
+ def errors(changeset) do
+ Enum.reduce(changeset.errors, %{}, fn
+ {:client_name, {error, _}}, acc ->
+ Map.put(acc, :name, error)
+
+ {key, {error, _}}, acc ->
+ Map.put(acc, key, error)
+ end)
+ end
end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 46688db7e..685269877 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -25,6 +25,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
plug(:fetch_session)
plug(:fetch_flash)
+
+ plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug])
+
plug(RateLimiter, [name: :authentication] when action == :create_authorization)
action_fallback(Pleroma.Web.OAuth.FallbackController)
diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex
index 1023f16d4..6f06f1431 100644
--- a/lib/pleroma/web/oauth/scopes.ex
+++ b/lib/pleroma/web/oauth/scopes.ex
@@ -17,12 +17,8 @@ defmodule Pleroma.Web.OAuth.Scopes do
"""
@spec fetch_scopes(map() | struct(), list()) :: list()
- def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do
- parse_scopes(scopes, default)
- end
-
def fetch_scopes(params, default) do
- parse_scopes(params["scope"] || params["scopes"], default)
+ parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default)
end
def parse_scopes(scopes, _default) when is_list(scopes) do
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index 9d0b3b1e4..be7477867 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -9,16 +9,21 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1]
alias Ecto.Changeset
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
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(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend
+ )
+
+ plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
)
@@ -34,15 +39,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
]
)
- plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
-
- # An extra safety measure for possible actions not guarded by OAuth permissions specification
plug(
- Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- when action != :confirmation_resend
+ OAuthScopesPlug,
+ %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
)
plug(RateLimiter, [name: :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)
@@ -58,38 +61,32 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
@doc "PATCH /api/v1/pleroma/accounts/update_avatar"
def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
- {:ok, user} =
+ {: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()
+ {: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
- with {:ok, user} <- User.update_banner(user, %{}) do
- CommonAPI.update(user)
+ with {:ok, _user} <- User.update_banner(user, %{}) do
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),
- {:ok, user} <- User.update_banner(user, object.data) do
- CommonAPI.update(user)
+ {:ok, _user} <- User.update_banner(user, object.data) do
%{"url" => [%{"href" => href} | _]} = object.data
json(conn, %{url: href})
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
index 03e95e020..d276b96a4 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
@@ -1,191 +1,93 @@
defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
-
- require Logger
+ alias Pleroma.Emoji.Pack
plug(
- OAuthScopesPlug,
+ Pleroma.Plugs.OAuthScopesPlug,
%{scopes: ["write"], admin: true}
when action in [
+ :import_from_filesystem,
+ :remote,
+ :download,
:create,
+ :update,
:delete,
- :download_from,
- :list_from,
- :import_from_fs,
+ :add_file,
:update_file,
- :update_metadata
+ :delete_file
]
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
- def emoji_dir_path do
- Path.join(
- Pleroma.Config.get!([:instance, :static_dir]),
- "emoji"
- )
- end
-
- @doc """
- Lists packs from the remote instance.
-
- Since JS cannot ask remote instances for their packs due to CPS, it has to
- be done by the server
- """
- def list_from(conn, %{"instance_address" => address}) do
- address = String.trim(address)
-
- if shareable_packs_available(address) do
- list_resp =
- "#{address}/api/pleroma/emoji/packs" |> Tesla.get!() |> Map.get(:body) |> Jason.decode!()
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
+ when action in [:archive, :show, :list]
+ )
- json(conn, list_resp)
+ def remote(conn, %{"url" => url}) do
+ with {:ok, packs} <- Pack.list_remote(url) do
+ json(conn, packs)
else
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "The requested instance does not support sharing emoji packs"})
+ {:shareable, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "The requested instance does not support sharing emoji packs"})
end
end
- @doc """
- Lists the packs available on the instance as JSON.
-
- The information is public and does not require authentication. The format is
- a map of "pack directory name" to pack.json contents.
- """
- def list_packs(conn, _params) do
- # Create the directory first if it does not exist. This is probably the first request made
- # with the API so it should be sufficient
- with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_dir_path())},
- {:ls, {:ok, results}} <- {:ls, File.ls(emoji_dir_path())} do
- pack_infos =
- results
- |> Enum.filter(&has_pack_json?/1)
- |> Enum.map(&load_pack/1)
- # Check if all the files are in place and can be sent
- |> Enum.map(&validate_pack/1)
- # Transform into a map of pack-name => pack-data
- |> Enum.into(%{})
-
- json(conn, pack_infos)
+ def list(conn, _params) do
+ emoji_path =
+ Path.join(
+ Pleroma.Config.get!([:instance, :static_dir]),
+ "emoji"
+ )
+
+ with {:ok, packs} <- Pack.list_local() do
+ json(conn, packs)
else
{:create_dir, {:error, e}} ->
conn
|> put_status(:internal_server_error)
- |> json(%{error: "Failed to create the emoji pack directory at #{emoji_dir_path()}: #{e}"})
+ |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"})
{:ls, {:error, e}} ->
conn
|> put_status(:internal_server_error)
|> json(%{
- error:
- "Failed to get the contents of the emoji pack directory at #{emoji_dir_path()}: #{e}"
+ error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}"
})
end
end
- defp has_pack_json?(file) do
- dir_path = Path.join(emoji_dir_path(), file)
- # Filter to only use the pack.json packs
- File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
- end
-
- defp load_pack(pack_name) do
- pack_path = Path.join(emoji_dir_path(), pack_name)
- pack_file = Path.join(pack_path, "pack.json")
+ def show(conn, %{"name" => name}) do
+ name = String.trim(name)
- {pack_name, Jason.decode!(File.read!(pack_file))}
- end
-
- defp validate_pack({name, pack}) do
- pack_path = Path.join(emoji_dir_path(), name)
-
- if can_download?(pack, pack_path) do
- archive_for_sha = make_archive(name, pack, pack_path)
- archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()
-
- pack =
- pack
- |> put_in(["pack", "can-download"], true)
- |> put_in(["pack", "download-sha256"], archive_sha)
-
- {name, pack}
+ with {:ok, pack} <- Pack.show(name) do
+ json(conn, pack)
else
- {name, put_in(pack, ["pack", "can-download"], false)}
- end
- end
-
- defp can_download?(pack, pack_path) do
- # If the pack is set as shared, check if it can be downloaded
- # That means that when asked, the pack can be packed and sent to the remote
- # Otherwise, they'd have to download it from external-src
- pack["pack"]["share-files"] &&
- Enum.all?(pack["files"], fn {_, path} ->
- File.exists?(Path.join(pack_path, path))
- end)
- end
-
- defp create_archive_and_cache(name, pack, pack_dir, md5) do
- files =
- ['pack.json'] ++
- (pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))
-
- {:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
-
- cache_seconds_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
- cache_ms = :timer.seconds(cache_seconds_per_file * Enum.count(files))
-
- Cachex.put!(
- :emoji_packs_cache,
- name,
- # if pack.json MD5 changes, the cache is not valid anymore
- %{pack_json_md5: md5, pack_data: zip_result},
- # Add a minute to cache time for every file in the pack
- ttl: cache_ms
- )
-
- Logger.debug("Created an archive for the '#{name}' emoji pack, \
-keeping it in cache for #{div(cache_ms, 1000)}s")
-
- zip_result
- end
-
- defp make_archive(name, pack, pack_dir) do
- # Having a different pack.json md5 invalidates cache
- pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json")))
-
- case Cachex.get!(:emoji_packs_cache, name) do
- %{pack_file_md5: ^pack_file_md5, pack_data: zip_result} ->
- Logger.debug("Using cache for the '#{name}' shared emoji pack")
- zip_result
+ {:loaded, _} ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "Pack #{name} does not exist"})
- _ ->
- create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
end
end
- @doc """
- An endpoint for other instances (via admin UI) or users (via browser)
- to download packs that the instance shares.
- """
- def download_shared(conn, %{"name" => name}) do
- pack_dir = Path.join(emoji_dir_path(), name)
- pack_file = Path.join(pack_dir, "pack.json")
-
- with {_, true} <- {:exists?, File.exists?(pack_file)},
- pack = Jason.decode!(File.read!(pack_file)),
- {_, true} <- {:can_download?, can_download?(pack, pack_dir)} do
- zip_result = make_archive(name, pack, pack_dir)
- send_download(conn, {:binary, zip_result}, filename: "#{name}.zip")
+ def archive(conn, %{"name" => name}) do
+ with {:ok, archive} <- Pack.get_archive(name) do
+ send_download(conn, {:binary, archive}, filename: "#{name}.zip")
else
{:can_download?, _} ->
conn
|> put_status(:forbidden)
|> json(%{
- error: "Pack #{name} cannot be downloaded from this instance, either pack sharing\
- was disabled for this pack or some files are missing"
+ error:
+ "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
})
{:exists?, _} ->
@@ -195,133 +97,67 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
end
end
- defp shareable_packs_available(address) do
- "#{address}/.well-known/nodeinfo"
- |> Tesla.get!()
- |> Map.get(:body)
- |> Jason.decode!()
- |> Map.get("links")
- |> List.last()
- |> Map.get("href")
- # Get the actual nodeinfo address and fetch it
- |> Tesla.get!()
- |> Map.get(:body)
- |> Jason.decode!()
- |> get_in(["metadata", "features"])
- |> Enum.member?("shareable_emoji_packs")
- end
-
- @doc """
- An admin endpoint to request downloading a pack named `pack_name` from the instance
- `instance_address`.
-
- If the requested instance's admin chose to share the pack, it will be downloaded
- from that instance, otherwise it will be downloaded from the fallback source, if there is one.
- """
- def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
- address = String.trim(address)
-
- if shareable_packs_available(address) do
- full_pack =
- "#{address}/api/pleroma/emoji/packs/list"
- |> Tesla.get!()
- |> Map.get(:body)
- |> Jason.decode!()
- |> Map.get(name)
-
- pack_info_res =
- case full_pack["pack"] do
- %{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
- {:ok,
- %{
- sha: sha,
- uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
- }}
-
- %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
- {:ok,
- %{
- sha: sha,
- uri: src,
- fallback: true
- }}
-
- _ ->
- {:error,
- "The pack was not set as shared and there is no fallback src to download from"}
- end
-
- with {:ok, %{sha: sha, uri: uri} = pinfo} <- pack_info_res,
- %{body: emoji_archive} <- Tesla.get!(uri),
- {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do
- local_name = data["as"] || name
- pack_dir = Path.join(emoji_dir_path(), local_name)
- File.mkdir_p!(pack_dir)
-
- files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end)
- # Fallback cannot contain a pack.json file
- files = if pinfo[:fallback], do: files, else: ['pack.json'] ++ files
-
- {:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
-
- # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
- # in it to depend on itself
- if pinfo[:fallback] do
- pack_file_path = Path.join(pack_dir, "pack.json")
-
- File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
- end
-
- json(conn, "ok")
- else
- {:error, e} ->
- conn |> put_status(:internal_server_error) |> json(%{error: e})
-
- {:checksum, _} ->
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
- end
+ def download(conn, %{"url" => url, "name" => name} = params) do
+ with :ok <- Pack.download(name, url, params["as"]) do
+ json(conn, "ok")
else
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "The requested instance does not support sharing emoji packs"})
+ {:shareable, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "The requested instance does not support sharing emoji packs"})
+
+ {:checksum, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
+
+ {:error, e} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: e})
end
end
- @doc """
- Creates an empty pack named `name` which then can be updated via the admin UI.
- """
def create(conn, %{"name" => name}) do
- pack_dir = Path.join(emoji_dir_path(), name)
+ name = String.trim(name)
- if not File.exists?(pack_dir) do
- File.mkdir_p!(pack_dir)
-
- pack_file_p = Path.join(pack_dir, "pack.json")
+ with :ok <- Pack.create(name) do
+ json(conn, "ok")
+ else
+ {:error, :eexist} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{error: "A pack named \"#{name}\" already exists"})
- File.write!(
- pack_file_p,
- Jason.encode!(%{pack: %{}, files: %{}}, pretty: true)
- )
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
- conn |> json("ok")
- else
- conn
- |> put_status(:conflict)
- |> json(%{error: "A pack named \"#{name}\" already exists"})
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while creating pack."
+ )
end
end
- @doc """
- Deletes the pack `name` and all it's files.
- """
def delete(conn, %{"name" => name}) do
- pack_dir = Path.join(emoji_dir_path(), name)
+ name = String.trim(name)
- case File.rm_rf(pack_dir) do
- {:ok, _} ->
- conn |> json("ok")
+ with {:ok, deleted} when deleted != [] <- Pack.delete(name) do
+ json(conn, "ok")
+ else
+ {:ok, []} ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "Pack #{name} does not exist"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
{:error, _, _} ->
conn
@@ -330,265 +166,128 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
end
end
- @doc """
- An endpoint to update `pack_names`'s metadata.
-
- `new_data` is the new metadata for the pack, that will replace the old metadata.
- """
- def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
- pack_file_p = Path.join([emoji_dir_path(), name, "pack.json"])
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- # The new fallback-src is in the new data and it's not the same as it was in the old data
- should_update_fb_sha =
- not is_nil(new_data["fallback-src"]) and
- new_data["fallback-src"] != full_pack["pack"]["fallback-src"]
-
- with {_, true} <- {:should_update?, should_update_fb_sha},
- %{body: pack_arch} <- Tesla.get!(new_data["fallback-src"]),
- {:ok, flist} <- :zip.unzip(pack_arch, [:memory]),
- {_, true} <- {:has_all_files?, has_all_files?(full_pack, flist)} do
- fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()
-
- new_data = Map.put(new_data, "fallback-src-sha256", fallback_sha)
- update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
+ def update(conn, %{"name" => name, "metadata" => metadata}) do
+ with {:ok, pack} <- Pack.update_metadata(name, metadata) do
+ json(conn, pack.pack)
else
- {:should_update?, _} ->
- update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
-
{:has_all_files?, _} ->
conn
|> put_status(:bad_request)
|> json(%{error: "The fallback archive does not have all files specified in pack.json"})
- end
- end
-
- # Check if all files from the pack.json are in the archive
- defp has_all_files?(%{"files" => files}, flist) do
- Enum.all?(files, fn {_, from_manifest} ->
- Enum.find(flist, fn {from_archive, _} ->
- to_string(from_archive) == from_manifest
- end)
- end)
- end
- defp update_metadata_and_send(conn, full_pack, new_data, pack_file_p) do
- full_pack = Map.put(full_pack, "pack", new_data)
- File.write!(pack_file_p, Jason.encode!(full_pack, pretty: true))
-
- # Send new data back with fallback sha filled
- json(conn, new_data)
- end
-
- defp get_filename(%{"filename" => filename}), do: filename
-
- defp get_filename(%{"file" => file}) do
- case file do
- %Plug.Upload{filename: filename} -> filename
- url when is_binary(url) -> Path.basename(url)
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while updating pack metadata."
+ )
end
end
- defp empty?(str), do: String.trim(str) == ""
-
- defp update_file_and_send(conn, updated_full_pack, pack_file_p) do
- # Write the emoji pack file
- File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))
-
- # Return the modified file list
- json(conn, updated_full_pack["files"])
- end
+ def add_file(conn, %{"name" => name} = params) do
+ filename = params["filename"] || get_filename(params["file"])
+ shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename))
- @doc """
- Updates a file in a pack.
-
- Updating can mean three things:
-
- - `add` adds an emoji named `shortcode` to the pack `pack_name`,
- that means that the emoji file needs to be uploaded with the request
- (thus requiring it to be a multipart request) and be named `file`.
- There can also be an optional `filename` that will be the new emoji file name
- (if it's not there, the name will be taken from the uploaded file).
- - `update` changes emoji shortcode (from `shortcode` to `new_shortcode` or moves the file
- (from the current filename to `new_filename`)
- - `remove` removes the emoji named `shortcode` and it's associated file
- """
-
- # Add
- def update_file(
- conn,
- %{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
- ) do
- pack_dir = Path.join(emoji_dir_path(), pack_name)
- pack_file_p = Path.join(pack_dir, "pack.json")
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- with {_, false} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
- filename <- get_filename(params),
- false <- empty?(shortcode),
- false <- empty?(filename) do
- file_path = Path.join(pack_dir, filename)
-
- # If the name contains directories, create them
- if String.contains?(file_path, "/") do
- File.mkdir_p!(Path.dirname(file_path))
- end
-
- case params["file"] do
- %Plug.Upload{path: upload_path} ->
- # Copy the uploaded file from the temporary directory
- File.copy!(upload_path, file_path)
-
- url when is_binary(url) ->
- # Download and write the file
- file_contents = Tesla.get!(url).body
- File.write!(file_path, file_contents)
- end
-
- updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
- update_file_and_send(conn, updated_full_pack, pack_file_p)
+ with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params["file"]) do
+ json(conn, pack.files)
else
- {:has_shortcode, _} ->
+ {:exists, _} ->
conn
|> put_status(:conflict)
|> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
- true ->
+ {:loaded, _} ->
conn
|> put_status(:bad_request)
- |> json(%{error: "shortcode or filename cannot be empty"})
- end
- end
+ |> json(%{error: "pack \"#{name}\" is not found"})
- # Remove
- def update_file(conn, %{
- "pack_name" => pack_name,
- "action" => "remove",
- "shortcode" => shortcode
- }) do
- pack_dir = Path.join(emoji_dir_path(), pack_name)
- pack_file_p = Path.join(pack_dir, "pack.json")
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name, shortcode or filename cannot be empty"})
- full_pack = Jason.decode!(File.read!(pack_file_p))
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while adding file to pack."
+ )
+ end
+ end
- if Map.has_key?(full_pack["files"], shortcode) do
- {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
+ def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do
+ new_shortcode = params["new_shortcode"]
+ new_filename = params["new_filename"]
+ force = params["force"] == true
- emoji_file_path = Path.join(pack_dir, emoji_file_path)
+ with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do
+ json(conn, pack.files)
+ else
+ {:exists, _} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
- # Delete the emoji file
- File.rm!(emoji_file_path)
+ {:not_used, _} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{
+ error:
+ "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option"
+ })
- # If the old directory has no more files, remove it
- if String.contains?(emoji_file_path, "/") do
- dir = Path.dirname(emoji_file_path)
+ {:loaded, _} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack \"#{name}\" is not found"})
- if Enum.empty?(File.ls!(dir)) do
- File.rmdir!(dir)
- end
- end
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "new_shortcode or new_filename cannot be empty"})
- update_file_and_send(conn, updated_full_pack, pack_file_p)
- else
- conn
- |> put_status(:bad_request)
- |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while updating file in pack."
+ )
end
end
- # Update
- def update_file(
- conn,
- %{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params
- ) do
- pack_dir = Path.join(emoji_dir_path(), pack_name)
- pack_file_p = Path.join(pack_dir, "pack.json")
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- with {_, true} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
- %{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params,
- false <- empty?(new_shortcode),
- false <- empty?(new_filename) do
- # First, remove the old shortcode, saving the old path
- {old_emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
- old_emoji_file_path = Path.join(pack_dir, old_emoji_file_path)
- new_emoji_file_path = Path.join(pack_dir, new_filename)
-
- # If the name contains directories, create them
- if String.contains?(new_emoji_file_path, "/") do
- File.mkdir_p!(Path.dirname(new_emoji_file_path))
- end
-
- # Move/Rename the old filename to a new filename
- # These are probably on the same filesystem, so just rename should work
- :ok = File.rename(old_emoji_file_path, new_emoji_file_path)
-
- # If the old directory has no more files, remove it
- if String.contains?(old_emoji_file_path, "/") do
- dir = Path.dirname(old_emoji_file_path)
-
- if Enum.empty?(File.ls!(dir)) do
- File.rmdir!(dir)
- end
- end
-
- # Then, put in the new shortcode with the new path
- updated_full_pack = put_in(updated_full_pack, ["files", new_shortcode], new_filename)
- update_file_and_send(conn, updated_full_pack, pack_file_p)
+ def delete_file(conn, %{"name" => name, "shortcode" => shortcode}) do
+ with {:ok, pack} <- Pack.delete_file(name, shortcode) do
+ json(conn, pack.files)
else
- {:has_shortcode, _} ->
+ {:exists, _} ->
conn
|> put_status(:bad_request)
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
- true ->
+ {:loaded, _} ->
conn
|> put_status(:bad_request)
- |> json(%{error: "new_shortcode or new_filename cannot be empty"})
+ |> json(%{error: "pack \"#{name}\" is not found"})
- _ ->
+ {:error, :empty_values} ->
conn
|> put_status(:bad_request)
- |> json(%{error: "new_shortcode or new_file were not specified"})
- end
- end
+ |> json(%{error: "pack name or shortcode cannot be empty"})
- def update_file(conn, %{"action" => action}) do
- conn
- |> put_status(:bad_request)
- |> json(%{error: "Unknown action: #{action}"})
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while removing file from pack."
+ )
+ end
end
- @doc """
- Imports emoji from the filesystem.
-
- Importing means checking all the directories in the
- `$instance_static/emoji/` for directories which do not have
- `pack.json`. If one has an emoji.txt file, that file will be used
- to create a `pack.json` file with it's contents. If the directory has
- neither, all the files with specific configured extenstions will be
- assumed to be emojis and stored in the new `pack.json` file.
- """
- def import_from_fs(conn, _params) do
- emoji_path = emoji_dir_path()
-
- with {:ok, %{access: :read_write}} <- File.stat(emoji_path),
- {:ok, results} <- File.ls(emoji_path) do
- imported_pack_names =
- results
- |> Enum.filter(fn file ->
- dir_path = Path.join(emoji_path, file)
- # Find the directories that do NOT have pack.json
- File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
- end)
- |> Enum.map(&write_pack_json_contents/1)
-
- json(conn, imported_pack_names)
+ def import_from_filesystem(conn, _params) do
+ with {:ok, names} <- Pack.import_from_filesystem() do
+ json(conn, names)
else
- {:ok, %{access: _}} ->
+ {:error, :no_read_write} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Error: emoji pack directory must be writable"})
@@ -600,44 +299,6 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
end
end
- defp write_pack_json_contents(dir) do
- dir_path = Path.join(emoji_dir_path(), dir)
- emoji_txt_path = Path.join(dir_path, "emoji.txt")
-
- files_for_pack = files_for_pack(emoji_txt_path, dir_path)
- pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack})
-
- File.write!(Path.join(dir_path, "pack.json"), pack_json_contents)
-
- dir
- end
-
- defp files_for_pack(emoji_txt_path, dir_path) do
- if File.exists?(emoji_txt_path) do
- # There's an emoji.txt file, it's likely from a pack installed by the pack manager.
- # Make a pack.json file from the contents of that emoji.txt fileh
-
- # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
-
- # Create a map of shortcodes to filenames from emoji.txt
- File.read!(emoji_txt_path)
- |> String.split("\n")
- |> Enum.map(&String.trim/1)
- |> Enum.map(fn line ->
- case String.split(line, ~r/,\s*/) do
- # This matches both strings with and without tags
- # and we don't care about tags here
- [name, file | _] -> {name, file}
- _ -> nil
- end
- end)
- |> Enum.filter(fn x -> not is_nil(x) end)
- |> Enum.into(%{})
- else
- # If there's no emoji.txt, assume all files
- # that are of certain extensions from the config are emojis and import them all
- pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
- Pleroma.Emoji.Loader.make_shortcode_to_file_map(dir_path, pack_extensions)
- end
- end
+ defp get_filename(%Plug.Upload{filename: filename}), do: filename
+ defp get_filename(url) when is_binary(url), do: Path.basename(url)
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
index d9c1c8636..d4e0d8b7c 100644
--- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -12,8 +12,6 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@doc "GET /api/v1/pleroma/mascot"
def show(%{assigns: %{user: user}} = conn, _params) do
json(conn, User.get_mascot(user))
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
index d4c5c5925..2c1874051 100644
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
@@ -28,18 +28,26 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
plug(
OAuthScopesPlug,
+ %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+ when action == :emoji_reactions_by
+ )
+
+ plug(
+ OAuthScopesPlug,
%{scopes: ["write:statuses"]}
when action in [:react_with_emoji, :unreact_with_emoji]
)
plug(
OAuthScopesPlug,
- %{scopes: ["write:conversations"]} when action == :update_conversation
+ %{scopes: ["write:conversations"]}
+ when action in [:update_conversation, :mark_conversations_as_read]
)
- plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)
-
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
+ )
def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
@@ -167,7 +175,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
end
end
- def read_conversations(%{assigns: %{user: user}} = conn, _params) do
+ def mark_conversations_as_read(%{assigns: %{user: user}} = conn, _params) do
with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
conn
|> add_link_headers(participations)
@@ -176,7 +184,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
end
end
- def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
+ def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
with {:ok, notification} <- Notification.read_one(user, notification_id) do
conn
|> put_view(NotificationView)
@@ -189,7 +197,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
end
end
- def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do
+ def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do
with notifications <- Notification.set_read_up_to(user, max_id) do
notifications = Enum.take(notifications, 80)
diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
index 4463ec477..22da6c0ad 100644
--- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -13,10 +13,12 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
- plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles)
- plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :user_scrobbles
+ )
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
params =
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index afa510f08..a9f893f7b 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -16,6 +16,8 @@ defmodule Pleroma.Web.Push.Impl do
require Logger
import Ecto.Query
+ defdelegate mastodon_notification_type(activity), to: Activity
+
@types ["Create", "Follow", "Announce", "Like", "Move"]
@doc "Performs sending notifications for user subscriptions"
@@ -24,40 +26,41 @@ defmodule Pleroma.Web.Push.Impl do
%{
activity: %{data: %{"type" => activity_type}} = activity,
user: %User{id: user_id}
- } = notif
+ } = notification
)
when activity_type in @types do
- actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
+ actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
- type = Activity.mastodon_notification_type(notif.activity)
+ mastodon_type = mastodon_notification_type(notification.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
object = Object.normalize(activity)
user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user)
- for subscription <- fetch_subsriptions(user_id),
- get_in(subscription.data, ["alerts", type]) do
+ for subscription <- fetch_subscriptions(user_id),
+ Subscription.enabled?(subscription, mastodon_type) do
%{
access_token: subscription.token.token,
- notification_id: notif.id,
- notification_type: type,
+ notification_id: notification.id,
+ notification_type: mastodon_type,
icon: avatar_url,
preferred_locale: "en",
pleroma: %{
- activity_id: notif.activity.id,
+ activity_id: notification.activity.id,
direct_conversation_id: direct_conversation_id
}
}
- |> Map.merge(build_content(notif, actor, object))
+ |> Map.merge(build_content(notification, actor, object, mastodon_type))
|> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription)
end
+ |> (&{:ok, &1}).()
end
def perform(_) do
Logger.warn("Unknown notification type")
- :error
+ {:error, :unknown_type}
end
@doc "Push message to web"
@@ -82,7 +85,7 @@ defmodule Pleroma.Web.Push.Impl do
end
@doc "Gets user subscriptions"
- def fetch_subsriptions(user_id) do
+ def fetch_subscriptions(user_id) do
Subscription
|> where(user_id: ^user_id)
|> preload(:token)
@@ -99,28 +102,36 @@ defmodule Pleroma.Web.Push.Impl do
}
end
+ def build_content(notification, actor, object, mastodon_type \\ nil)
+
def build_content(
%{
activity: %{data: %{"directMessage" => true}},
user: %{notification_settings: %{privacy_option: true}}
},
actor,
- _
+ _object,
+ _mastodon_type
) do
%{title: "New Direct Message", body: "@#{actor.nickname}"}
end
- def build_content(notif, actor, object) do
+ def build_content(notification, actor, object, mastodon_type) do
+ mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+
%{
- title: format_title(notif),
- body: format_body(notif, actor, object)
+ title: format_title(notification, mastodon_type),
+ body: format_body(notification, actor, object, mastodon_type)
}
end
+ def format_body(activity, actor, object, mastodon_type \\ nil)
+
def format_body(
%{activity: %{data: %{"type" => "Create"}}},
actor,
- %{data: %{"content" => content}}
+ %{data: %{"content" => content}},
+ _mastodon_type
) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end
@@ -128,33 +139,44 @@ defmodule Pleroma.Web.Push.Impl do
def format_body(
%{activity: %{data: %{"type" => "Announce"}}},
actor,
- %{data: %{"content" => content}}
+ %{data: %{"content" => content}},
+ _mastodon_type
) do
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
- %{activity: %{data: %{"type" => type}}},
+ %{activity: %{data: %{"type" => type}}} = notification,
actor,
- _object
+ _object,
+ mastodon_type
)
when type in ["Follow", "Like"] do
- case type do
- "Follow" -> "@#{actor.nickname} has followed you"
- "Like" -> "@#{actor.nickname} has favorited your post"
+ mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+
+ case mastodon_type do
+ "follow" -> "@#{actor.nickname} has followed you"
+ "follow_request" -> "@#{actor.nickname} has requested to follow you"
+ "favourite" -> "@#{actor.nickname} has favorited your post"
end
end
- def format_title(%{activity: %{data: %{"directMessage" => true}}}) do
+ def format_title(activity, mastodon_type \\ nil)
+
+ def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) do
"New Direct Message"
end
- def format_title(%{activity: %{data: %{"type" => type}}}) do
- case type do
- "Create" -> "New Mention"
- "Follow" -> "New Follower"
- "Announce" -> "New Repeat"
- "Like" -> "New Favorite"
+ def format_title(%{activity: activity}, mastodon_type) do
+ mastodon_type = mastodon_type || mastodon_notification_type(activity)
+
+ case mastodon_type do
+ "mention" -> "New Mention"
+ "follow" -> "New Follower"
+ "follow_request" -> "New Follow Request"
+ "reblog" -> "New Repeat"
+ "favourite" -> "New Favorite"
+ type -> "New #{String.capitalize(type || "event")}"
end
end
end
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index 5c448d6c9..b99b0c5fb 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -32,6 +32,14 @@ defmodule Pleroma.Web.Push.Subscription do
%{"alerts" => alerts}
end
+ def enabled?(subscription, "follow_request") do
+ enabled?(subscription, "follow")
+ end
+
+ def enabled?(subscription, alert_type) do
+ get_in(subscription.data, ["alerts", alert_type])
+ end
+
def create(
%User{} = user,
%Token{} = token,
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 5f5ec1c81..a7e1f2f57 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -16,78 +16,70 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.UserEnabledPlug)
end
- pipeline :api do
- plug(:accepts, ["json"])
- plug(:fetch_session)
+ pipeline :expect_authentication do
+ plug(Pleroma.Plugs.ExpectAuthenticatedCheckPlug)
+ end
+
+ pipeline :expect_public_instance_or_authentication do
+ plug(Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
+ end
+
+ pipeline :authenticate do
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
+ end
+
+ pipeline :after_auth do
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
- plug(Pleroma.Plugs.IdempotencyPlug)
- plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
- pipeline :authenticated_api do
+ pipeline :base_api do
plug(:accepts, ["json"])
plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.BasicAuthDecoderPlug)
- plug(Pleroma.Plugs.UserFetcherPlug)
- plug(Pleroma.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Plugs.LegacyAuthenticationPlug)
- plug(Pleroma.Plugs.AuthenticationPlug)
- plug(Pleroma.Plugs.UserEnabledPlug)
- plug(Pleroma.Plugs.SetUserSessionIdPlug)
+ plug(:authenticate)
+ plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
+ end
+
+ pipeline :api do
+ plug(:expect_public_instance_or_authentication)
+ plug(:base_api)
+ plug(:after_auth)
+ plug(Pleroma.Plugs.IdempotencyPlug)
+ end
+
+ pipeline :authenticated_api do
+ plug(:expect_authentication)
+ plug(:base_api)
+ plug(:after_auth)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
- plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :admin_api do
- plug(:accepts, ["json"])
- plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.BasicAuthDecoderPlug)
- plug(Pleroma.Plugs.UserFetcherPlug)
- plug(Pleroma.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Plugs.LegacyAuthenticationPlug)
- plug(Pleroma.Plugs.AuthenticationPlug)
+ plug(:expect_authentication)
+ plug(:base_api)
plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)
- plug(Pleroma.Plugs.UserEnabledPlug)
- plug(Pleroma.Plugs.SetUserSessionIdPlug)
+ plug(:after_auth)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.UserIsAdminPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
- plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :mastodon_html do
- plug(:accepts, ["html"])
- plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.BasicAuthDecoderPlug)
- plug(Pleroma.Plugs.UserFetcherPlug)
- plug(Pleroma.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Plugs.LegacyAuthenticationPlug)
- plug(Pleroma.Plugs.AuthenticationPlug)
- plug(Pleroma.Plugs.UserEnabledPlug)
- plug(Pleroma.Plugs.SetUserSessionIdPlug)
- plug(Pleroma.Plugs.EnsureUserKeyPlug)
+ plug(:browser)
+ plug(:authenticate)
+ plug(:after_auth)
end
pipeline :pleroma_html do
- plug(:accepts, ["html"])
- plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.BasicAuthDecoderPlug)
- plug(Pleroma.Plugs.UserFetcherPlug)
- plug(Pleroma.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Plugs.AuthenticationPlug)
+ plug(:browser)
+ plug(:authenticate)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
@@ -203,33 +195,43 @@ defmodule Pleroma.Web.Router do
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
get("/config/descriptions", AdminAPIController, :config_descriptions)
+ get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)
+
+ get("/oauth_app", AdminAPIController, :oauth_app_list)
+ post("/oauth_app", AdminAPIController, :oauth_app_create)
+ patch("/oauth_app/:id", AdminAPIController, :oauth_app_update)
+ delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
+ # Modifying packs
scope "/packs" do
- # Modifying packs
pipe_through(:admin_api)
- post("/import_from_fs", EmojiAPIController, :import_from_fs)
+ get("/import", EmojiAPIController, :import_from_filesystem)
+ get("/remote", EmojiAPIController, :remote)
+ post("/download", EmojiAPIController, :download)
- post("/:pack_name/update_file", EmojiAPIController, :update_file)
- post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
- put("/:name", EmojiAPIController, :create)
+ post("/:name", EmojiAPIController, :create)
+ patch("/:name", EmojiAPIController, :update)
delete("/:name", EmojiAPIController, :delete)
- post("/download_from", EmojiAPIController, :download_from)
- post("/list_from", EmojiAPIController, :list_from)
+
+ post("/:name/files", EmojiAPIController, :add_file)
+ patch("/:name/files", EmojiAPIController, :update_file)
+ delete("/:name/files", EmojiAPIController, :delete_file)
end
+ # Pack info / downloading
scope "/packs" do
- # Pack info / downloading
- get("/", EmojiAPIController, :list_packs)
- get("/:name/download_shared/", EmojiAPIController, :download_shared)
+ get("/", EmojiAPIController, :list)
+ get("/:name", EmojiAPIController, :show)
+ get("/:name/archive", EmojiAPIController, :archive)
end
end
@@ -289,7 +291,7 @@ defmodule Pleroma.Web.Router do
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
- post("/conversations/read", PleromaAPIController, :read_conversations)
+ post("/conversations/read", PleromaAPIController, :mark_conversations_as_read)
end
scope [] do
@@ -298,7 +300,7 @@ defmodule Pleroma.Web.Router do
patch("/conversations/:id", PleromaAPIController, :update_conversation)
put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji)
delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji)
- post("/notifications/read", PleromaAPIController, :read_notification)
+ post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner)
@@ -334,53 +336,84 @@ defmodule Pleroma.Web.Router do
pipe_through(:authenticated_api)
get("/accounts/verify_credentials", AccountController, :verify_credentials)
+ patch("/accounts/update_credentials", AccountController, :update_credentials)
get("/accounts/relationships", AccountController, :relationships)
-
get("/accounts/:id/lists", AccountController, :lists)
- get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
-
- get("/follow_requests", FollowRequestController, :index)
+ get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
+ get("/endorsements", AccountController, :endorsements)
get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes)
- get("/timelines/home", TimelineController, :home)
- get("/timelines/direct", TimelineController, :direct)
+ post("/follows", AccountController, :follow_by_uri)
+ 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)
- get("/favourites", StatusController, :favourites)
- get("/bookmarks", StatusController, :bookmarks)
+ get("/apps/verify_credentials", AppController, :verify_credentials)
+
+ get("/conversations", ConversationController, :index)
+ post("/conversations/:id/read", ConversationController, :mark_as_read)
+
+ get("/domain_blocks", DomainBlockController, :index)
+ post("/domain_blocks", DomainBlockController, :create)
+ delete("/domain_blocks", DomainBlockController, :delete)
+
+ get("/filters", FilterController, :index)
+
+ post("/filters", FilterController, :create)
+ get("/filters/:id", FilterController, :show)
+ put("/filters/:id", FilterController, :update)
+ delete("/filters/:id", FilterController, :delete)
+
+ get("/follow_requests", FollowRequestController, :index)
+ post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
+ post("/follow_requests/:id/reject", FollowRequestController, :reject)
+
+ get("/lists", ListController, :index)
+ get("/lists/:id", ListController, :show)
+ get("/lists/:id/accounts", ListController, :list_accounts)
+
+ delete("/lists/:id", ListController, :delete)
+ post("/lists", ListController, :create)
+ put("/lists/:id", ListController, :update)
+ post("/lists/:id/accounts", ListController, :add_to_list)
+ delete("/lists/:id/accounts", ListController, :remove_from_list)
+
+ get("/markers", MarkerController, :index)
+ post("/markers", MarkerController, :upsert)
+
+ post("/media", MediaController, :create)
+ put("/media/:id", MediaController, :update)
get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show)
+
post("/notifications/:id/dismiss", NotificationController, :dismiss)
post("/notifications/clear", NotificationController, :clear)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
post("/notifications/dismiss", NotificationController, :dismiss)
- 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", DomainBlockController, :index)
-
- get("/filters", FilterController, :index)
+ post("/polls/:id/votes", PollController, :vote)
- get("/suggestions", SuggestionController, :index)
+ post("/reports", ReportController, :create)
- get("/conversations", ConversationController, :index)
- post("/conversations/:id/read", ConversationController, :read)
+ get("/scheduled_statuses", ScheduledActivityController, :index)
+ get("/scheduled_statuses/:id", ScheduledActivityController, :show)
- get("/endorsements", AccountController, :endorsements)
+ put("/scheduled_statuses/:id", ScheduledActivityController, :update)
+ delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
- patch("/accounts/update_credentials", AccountController, :update_credentials)
+ # Unlike `GET /api/v1/accounts/:id/favourites`, demands authentication
+ get("/favourites", StatusController, :favourites)
+ get("/bookmarks", StatusController, :bookmarks)
post("/statuses", StatusController, :create)
delete("/statuses/:id", StatusController, :delete)
-
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
@@ -392,49 +425,16 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
- put("/scheduled_statuses/:id", ScheduledActivityController, :update)
- delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
-
- post("/polls/:id/votes", PollController, :vote)
-
- post("/media", MediaController, :create)
- put("/media/:id", MediaController, :update)
-
- delete("/lists/:id", ListController, :delete)
- post("/lists", ListController, :create)
- put("/lists/:id", ListController, :update)
-
- post("/lists/:id/accounts", ListController, :add_to_list)
- delete("/lists/:id/accounts", ListController, :remove_from_list)
-
- post("/filters", FilterController, :create)
- get("/filters/:id", FilterController, :show)
- put("/filters/:id", FilterController, :update)
- delete("/filters/:id", FilterController, :delete)
-
- post("/reports", ReportController, :create)
-
- post("/follows", AccountController, :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("/follow_requests/:id/authorize", FollowRequestController, :authorize)
- post("/follow_requests/:id/reject", FollowRequestController, :reject)
-
- post("/domain_blocks", DomainBlockController, :create)
- delete("/domain_blocks", DomainBlockController, :delete)
-
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :get)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
- get("/markers", MarkerController, :index)
- post("/markers", MarkerController, :upsert)
+ get("/suggestions", SuggestionController, :index)
+
+ get("/timelines/home", TimelineController, :home)
+ get("/timelines/direct", TimelineController, :direct)
+ get("/timelines/list/:list_id", TimelineController, :list)
end
scope "/api/web", Pleroma.Web do
@@ -446,15 +446,24 @@ defmodule Pleroma.Web.Router do
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
- post("/accounts", AccountController, :create)
get("/accounts/search", SearchController, :account_search)
+ get("/search", SearchController, :search)
+
+ get("/accounts/:id/statuses", AccountController, :statuses)
+ get("/accounts/:id/followers", AccountController, :followers)
+ get("/accounts/:id/following", AccountController, :following)
+ get("/accounts/:id", AccountController, :show)
+
+ post("/accounts", AccountController, :create)
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
post("/apps", AppController, :create)
- get("/apps/verify_credentials", AppController, :verify_credentials)
+ get("/statuses", StatusController, :index)
+ get("/statuses/:id", StatusController, :show)
+ get("/statuses/:id/context", StatusController, :context)
get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
@@ -465,20 +474,8 @@ defmodule Pleroma.Web.Router do
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
- get("/timelines/list/:list_id", TimelineController, :list)
-
- get("/statuses", StatusController, :index)
- get("/statuses/:id", StatusController, :show)
- get("/statuses/:id/context", StatusController, :context)
get("/polls/:id", PollController, :show)
-
- 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)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
@@ -508,7 +505,7 @@ defmodule Pleroma.Web.Router do
end
scope "/api" do
- pipe_through(:api)
+ pipe_through(:base_api)
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
end
@@ -519,11 +516,11 @@ defmodule Pleroma.Web.Router do
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
- post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
- end
-
- pipeline :ap_service_actor do
- plug(:accepts, ["activity+json", "json"])
+ post(
+ "/qvitter/statuses/notifications/read",
+ TwitterAPI.Controller,
+ :mark_notifications_as_read
+ )
end
pipeline :ostatus do
@@ -536,8 +533,7 @@ defmodule Pleroma.Web.Router do
end
scope "/", Pleroma.Web do
- pipe_through(:ostatus)
- pipe_through(:http_signature)
+ pipe_through([:ostatus, :http_signature])
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
@@ -555,13 +551,6 @@ defmodule Pleroma.Web.Router do
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
- # Server to Server (S2S) AP interactions
- pipeline :activitypub do
- plug(:accepts, ["activity+json", "json"])
- plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
- plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
- end
-
scope "/", Pleroma.Web.ActivityPub do
# XXX: not really ostatus
pipe_through(:ostatus)
@@ -569,19 +558,22 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
+ pipeline :ap_service_actor do
+ plug(:accepts, ["activity+json", "json"])
+ end
+
+ # Server to Server (S2S) AP interactions
+ pipeline :activitypub do
+ plug(:ap_service_actor)
+ plug(:http_signature)
+ end
+
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do
- plug(:accepts, ["activity+json", "json"])
+ plug(:ap_service_actor)
plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.BasicAuthDecoderPlug)
- plug(Pleroma.Plugs.UserFetcherPlug)
- plug(Pleroma.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Plugs.LegacyAuthenticationPlug)
- plug(Pleroma.Plugs.AuthenticationPlug)
- plug(Pleroma.Plugs.UserEnabledPlug)
- plug(Pleroma.Plugs.SetUserSessionIdPlug)
- plug(Pleroma.Plugs.EnsureUserKeyPlug)
+ plug(:authenticate)
+ plug(:after_auth)
end
scope "/", Pleroma.Web.ActivityPub do
@@ -653,12 +645,7 @@ defmodule Pleroma.Web.Router do
get("/web/*path", MastoFEController, :index)
end
- pipeline :remote_media do
- end
-
scope "/proxy/", Pleroma.Web.MediaProxy do
- pipe_through(:remote_media)
-
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
@@ -671,6 +658,34 @@ defmodule Pleroma.Web.Router do
end
end
+ # Test-only routes needed to test action dispatching and plug chain execution
+ if Pleroma.Config.get(:env) == :test do
+ @test_actions [
+ :do_oauth_check,
+ :fallback_oauth_check,
+ :skip_oauth_check,
+ :fallback_oauth_skip_publicity_check,
+ :skip_oauth_skip_publicity_check,
+ :missing_oauth_check_definition
+ ]
+
+ scope "/test/api", Pleroma.Tests do
+ pipe_through(:api)
+
+ for action <- @test_actions do
+ get("/#{action}", AuthTestController, action)
+ end
+ end
+
+ scope "/test/authenticated_api", Pleroma.Tests do
+ pipe_through(:authenticated_api)
+
+ for action <- @test_actions do
+ get("/#{action}", AuthTestController, action)
+ end
+ end
+ end
+
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)
diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex
index 66d87620c..b3d1d1ec8 100644
--- a/lib/pleroma/web/static_fe/static_fe_view.ex
+++ b/lib/pleroma/web/static_fe/static_fe_view.ex
@@ -18,15 +18,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEView do
@media_types ["image", "audio", "video"]
- def emoji_for_user(%User{} = user) do
- user.source_data
- |> Map.get("tag", [])
- |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
- |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
- {String.trim(name, ":"), url}
- end)
- end
-
def fetch_media_type(%{"mediaType" => mediaType}) do
Utils.fetch_media_type(@media_types, mediaType)
end
diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex
index abfed21c8..f6160fa4d 100644
--- a/lib/pleroma/web/streamer/worker.ex
+++ b/lib/pleroma/web/streamer/worker.ex
@@ -158,24 +158,6 @@ defmodule Pleroma.Web.Streamer.Worker do
should_send?(user, activity)
end
- def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do
- Enum.each(topics[topic] || [], fn %StreamerSocket{
- transport_pid: transport_pid,
- user: socket_user
- } ->
- # Get the current user so we have up-to-date blocks etc.
- if socket_user do
- user = User.get_cached_by_ap_id(socket_user.ap_id)
-
- if should_send?(user, item) do
- send(transport_pid, {:text, StreamerView.render("update.json", item, user)})
- end
- else
- send(transport_pid, {:text, StreamerView.render("update.json", item)})
- end
- end)
- end
-
def push_to_socket(topics, topic, %Participation{} = participation) do
Enum.each(topics[topic] || [], fn %StreamerSocket{transport_pid: transport_pid} ->
send(transport_pid, {:text, StreamerView.render("conversation.json", participation)})
diff --git a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
index ac8a75009..78350f2aa 100644
--- a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
+++ b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
@@ -2,10 +2,10 @@
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<id><%= @data["id"] %></id>
- <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
- <content type="html"><%= activity_content(@object) %></content>
- <published><%= @data["published"] %></published>
- <updated><%= @data["published"] %></updated>
+ <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
+ <content type="html"><%= activity_content(@data) %></content>
+ <published><%= @activity.data["published"] %></published>
+ <updated><%= @activity.data["published"] %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>
diff --git a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
index a4dbed638..a304a16af 100644
--- a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
+++ b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
@@ -2,10 +2,10 @@
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<guid><%= @data["id"] %></guid>
- <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
- <description><%= activity_content(@object) %></description>
- <pubDate><%= @data["published"] %></pubDate>
- <updated><%= @data["published"] %></updated>
+ <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
+ <description><%= activity_content(@data) %></description>
+ <pubDate><%= @activity.data["published"] %></pubDate>
+ <updated><%= @activity.data["published"] %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>
diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex
index da4fa6d6c..cf5874a91 100644
--- a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex
+++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex
@@ -1,12 +1,12 @@
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
-
+
<%= render @view_module, "_tag_author.atom", assigns %>
-
+
<id><%= @data["id"] %></id>
- <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
- <content type="html"><%= activity_content(@object) %></content>
+ <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
+ <content type="html"><%= activity_content(@data) %></content>
<%= if @activity.local do %>
<link type="application/atom+xml" href='<%= @data["id"] %>' rel="self"/>
@@ -15,8 +15,8 @@
<link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/>
<% end %>
- <published><%= @data["published"] %></published>
- <updated><%= @data["published"] %></updated>
+ <published><%= @activity.data["published"] %></published>
+ <updated><%= @activity.data["published"] %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
@@ -26,7 +26,7 @@
<%= if @data["summary"] do %>
<summary><%= @data["summary"] %></summary>
<% end %>
-
+
<%= for id <- @activity.recipients do %>
<%= if id == Pleroma.Constants.as_public() do %>
<link rel="mentioned"
@@ -40,7 +40,7 @@
<% end %>
<% end %>
<% end %>
-
+
<%= for tag <- @data["tag"] || [] do %>
<category term="<%= tag %>"></category>
<% end %>
diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex
index 295574df1..2334e24a2 100644
--- a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex
+++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex
@@ -1,15 +1,14 @@
<item>
- <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
-
-
+ <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
+
+
<guid isPermalink="true"><%= activity_context(@activity) %></guid>
<link><%= activity_context(@activity) %></link>
- <pubDate><%= pub_date(@data["published"]) %></pubDate>
-
- <description><%= activity_content(@object) %></description>
+ <pubDate><%= pub_date(@activity.data["published"]) %></pubDate>
+
+ <description><%= activity_content(@data) %></description>
<%= for attachment <- @data["attachment"] || [] do %>
<enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>"/>
<% end %>
-
-</item>
+</item>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
index 7e04e9550..4853e7f4b 100644
--- a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
@@ -1,8 +1,8 @@
<%= case @mediaType do %>
<% "audio" -> %>
-<audio src="<%= @url %>" controls="controls"></audio>
+<audio class="u-audio" src="<%= @url %>" controls="controls"></audio>
<% "video" -> %>
-<video src="<%= @url %>" controls="controls"></video>
+<video class="u-video" src="<%= @url %>" controls="controls"></video>
<% _ -> %>
-<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
+<img class="u-photo" src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
<% end %>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
index df5e5eedd..df0244795 100644
--- a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
@@ -1,12 +1,16 @@
-<div class="activity" <%= if @selected do %> id="selected" <% end %>>
+<div class="activity h-entry" <%= if @selected do %> id="selected" <% end %>>
<p class="pull-right">
- <%= link format_date(@published), to: @link, class: "activity-link" %>
+ <a class="activity-link u-url u-uid" href="<%= @link %>">
+ <time class="dt-published" datetime="<%= @published %>">
+ <%= format_date(@published) %>
+ </time>
+ </a>
</p>
<%= render("_user_card.html", %{user: @user}) %>
<div class="activity-content">
<%= if @title != "" do %>
<details <%= if open_content?() do %>open<% end %>>
- <summary><%= raw @title %></summary>
+ <summary class="p-name"><%= raw @title %></summary>
<div class="e-content"><%= raw @content %></div>
</details>
<% else %>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
index 2a7582d45..977b894d3 100644
--- a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
@@ -1,10 +1,10 @@
<div class="p-author h-card">
<a class="u-url" rel="author noopener" href="<%= (@user.uri || @user.ap_id) %>">
<div class="avatar">
- <img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
+ <img class="u-photo" src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
</div>
<span class="display-name">
- <bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi>
+ <bdi class="p-name"><%= raw Formatter.emojify(@user.name, @user.emoji) %></bdi>
<span class="nickname"><%= @user.nickname %></span>
</span>
</a>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
index e7d2aecad..3191bf450 100644
--- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
+++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
@@ -7,7 +7,7 @@
<input type="hidden" name="profile" value="">
<button type="submit" class="collapse">Remote follow</button>
</form>
- <%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> |
+ <%= raw Formatter.emojify(@user.name, @user.emoji) %> |
<%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>
</h3>
<p><%= raw @user.bio %></p>
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index 537f9f778..fd2aee175 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -25,13 +25,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
when action == :follow_import
)
- # Note: follower can submit the form (with password auth) not being signed in (having no token)
- plug(
- OAuthScopesPlug,
- %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]}
- when action == :do_remote_follow
- )
-
plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import)
plug(
@@ -199,15 +192,16 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
- with lines <- String.split(list, "\n"),
- followed_identifiers <-
- Enum.map(lines, fn line ->
- String.split(line, ",") |> List.first()
- end)
- |> List.delete("Account address") do
- User.follow_import(follower, followed_identifiers)
- json(conn, "job started")
- end
+ followed_identifiers =
+ list
+ |> String.split("\n")
+ |> Enum.map(&(&1 |> String.split(",") |> List.first()))
+ |> List.delete("Account address")
+ |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@")))
+ |> Enum.reject(&(&1 == ""))
+
+ User.follow_import(follower, followed_identifiers)
+ json(conn, "job started")
end
def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
@@ -215,10 +209,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
- with blocked_identifiers <- String.split(list) do
- User.blocks_import(blocker, blocked_identifiers)
- json(conn, "job started")
- end
+ blocked_identifiers = list |> String.split() |> Enum.map(&String.trim_leading(&1, "@"))
+ User.blocks_import(blocker, blocked_identifiers)
+ json(conn, "job started")
end
def change_password(%{assigns: %{user: user}} = conn, params) do
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index f9c0994da..cf1d9c74c 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -12,72 +12,57 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
require Pleroma.Constants
def register_user(params, opts \\ []) do
- token = params["token"]
-
- params = %{
- nickname: params["nickname"],
- name: params["fullname"],
- bio: User.parse_bio(params["bio"]),
- email: params["email"],
- password: params["password"],
- password_confirmation: params["confirm"],
- captcha_solution: params["captcha_solution"],
- captcha_token: params["captcha_token"],
- captcha_answer_data: params["captcha_answer_data"]
- }
-
- captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])
- # true if captcha is disabled or enabled and valid, false otherwise
- captcha_ok =
- if not captcha_enabled do
- :ok
- else
- Pleroma.Captcha.validate(
- params[:captcha_token],
- params[:captcha_solution],
- params[:captcha_answer_data]
- )
- end
-
- # Captcha invalid
- if captcha_ok != :ok do
- {:error, error} = captcha_ok
- # I have no idea how this error handling works
- {:error, %{error: Jason.encode!(%{captcha: [error]})}}
- else
- registration_process(
- params,
- %{
- registrations_open: Pleroma.Config.get([:instance, :registrations_open]),
- token: token
- },
- opts
- )
+ params =
+ params
+ |> Map.take([
+ :nickname,
+ :password,
+ :captcha_solution,
+ :captcha_token,
+ :captcha_answer_data,
+ :token,
+ :email,
+ :trusted_app
+ ])
+ |> Map.put(:bio, User.parse_bio(params[:bio] || ""))
+ |> Map.put(:name, params.fullname)
+ |> Map.put(:password_confirmation, params[:confirm])
+
+ case validate_captcha(params) do
+ :ok ->
+ if Pleroma.Config.get([:instance, :registrations_open]) do
+ create_user(params, opts)
+ else
+ create_user_with_invite(params, opts)
+ end
+
+ {:error, error} ->
+ # I have no idea how this error handling works
+ {:error, %{error: Jason.encode!(%{captcha: [error]})}}
end
end
- defp registration_process(params, %{registrations_open: true}, opts) do
- create_user(params, opts)
+ defp validate_captcha(params) do
+ if params[:trusted_app] || not Pleroma.Config.get([Pleroma.Captcha, :enabled]) do
+ :ok
+ else
+ Pleroma.Captcha.validate(
+ params.captcha_token,
+ params.captcha_solution,
+ params.captcha_answer_data
+ )
+ end
end
- defp registration_process(params, %{token: token}, opts) do
- invite =
- unless is_nil(token) do
- Repo.get_by(UserInviteToken, %{token: token})
- end
-
- valid_invite? = invite && UserInviteToken.valid_invite?(invite)
-
- case invite do
- nil ->
- {:error, "Invalid token"}
-
- invite when valid_invite? ->
- UserInviteToken.update_usage!(invite)
- create_user(params, opts)
-
- _ ->
- {:error, "Expired token"}
+ defp create_user_with_invite(params, opts) do
+ with %{token: token} when is_binary(token) <- params,
+ %UserInviteToken{} = invite <- Repo.get_by(UserInviteToken, %{token: token}),
+ true <- UserInviteToken.valid_invite?(invite) do
+ UserInviteToken.update_usage!(invite)
+ create_user(params, opts)
+ else
+ nil -> {:error, "Invalid token"}
+ _ -> {:error, "Expired token"}
end
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 0229aea97..c2de26b0b 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller
alias Pleroma.Notification
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
@@ -13,9 +14,17 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
require Logger
- plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
+ )
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirm_email
+ )
+
+ plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
action_fallback(:errors)
@@ -44,13 +53,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
json_reply(conn, 201, "")
end
- def errors(conn, {:param_cast, _}) do
+ defp errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
end
- def errors(conn, _) do
+ defp errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
@@ -62,7 +71,10 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> send_resp(status, json)
end
- def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
+ def mark_notifications_as_read(
+ %{assigns: %{user: user}} = conn,
+ %{"latest_id" => latest_id} = params
+ ) do
Notification.set_read_up_to(user, latest_id)
notifications = Notification.for_user(user, params)
@@ -73,7 +85,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> render("index.json", %{notifications: notifications, for: user})
end
- def notifications_read(%{assigns: %{user: _user}} = conn, _) do
+ def mark_notifications_as_read(%{assigns: %{user: _user}} = conn, _) do
bad_request_reply(conn, "You need to specify latest_id")
end
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex
index cf3ac1287..08e42a7e5 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -2,6 +2,11 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.Web.Plug do
+ # Substitute for `call/2` which is defined with `use Pleroma.Web, :plug`
+ @callback perform(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
+end
+
defmodule Pleroma.Web do
@moduledoc """
A module that keeps using definitions for controllers,
@@ -20,11 +25,19 @@ defmodule Pleroma.Web do
below.
"""
+ alias Pleroma.Plugs.EnsureAuthenticatedPlug
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug
+ alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Plugs.PlugHelper
+
def controller do
quote do
use Phoenix.Controller, namespace: Pleroma.Web
import Plug.Conn
+
import Pleroma.Web.Gettext
import Pleroma.Web.Router.Helpers
import Pleroma.Web.TranslationHelpers
@@ -34,6 +47,79 @@ defmodule Pleroma.Web do
defp set_put_layout(conn, _) do
put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
end
+
+ # Marks plugs intentionally skipped and blocks their execution if present in plugs chain
+ defp skip_plug(conn, plug_modules) do
+ plug_modules
+ |> List.wrap()
+ |> Enum.reduce(
+ conn,
+ fn plug_module, conn ->
+ try do
+ plug_module.skip_plug(conn)
+ rescue
+ UndefinedFunctionError ->
+ raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code."
+ end
+ end
+ )
+ end
+
+ # Executed just before actual controller action, invokes before-action hooks (callbacks)
+ defp action(conn, params) do
+ with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn),
+ %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn),
+ %{halted: false} = conn <- maybe_perform_authenticated_check(conn),
+ %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do
+ super(conn, params)
+ end
+ end
+
+ # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored
+ # (neither performed nor explicitly skipped)
+ defp maybe_drop_authentication_if_oauth_check_ignored(conn) do
+ if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and
+ not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
+ OAuthScopesPlug.drop_auth_info(conn)
+ else
+ conn
+ end
+ end
+
+ # Ensures instance is public -or- user is authenticated if such check was scheduled
+ defp maybe_perform_public_or_authenticated_check(conn) do
+ if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do
+ EnsurePublicOrAuthenticatedPlug.call(conn, %{})
+ else
+ conn
+ end
+ end
+
+ # Ensures user is authenticated if such check was scheduled
+ # Note: runs prior to action even if it was already executed earlier in plug chain
+ # (since OAuthScopesPlug has option of proceeding unauthenticated)
+ defp maybe_perform_authenticated_check(conn) do
+ if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do
+ EnsureAuthenticatedPlug.call(conn, %{})
+ else
+ conn
+ end
+ end
+
+ # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
+ defp maybe_halt_on_missing_oauth_scopes_check(conn) do
+ if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and
+ not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
+ conn
+ |> render_error(
+ :forbidden,
+ "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
+ )
+ |> halt()
+ else
+ conn
+ end
+ end
end
end
@@ -96,6 +182,44 @@ defmodule Pleroma.Web do
end
end
+ def plug do
+ quote do
+ @behaviour Pleroma.Web.Plug
+ @behaviour Plug
+
+ @doc """
+ Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
+ """
+ def skip_plug(conn) do
+ PlugHelper.append_to_private_list(
+ conn,
+ PlugHelper.skipped_plugs_list_id(),
+ __MODULE__
+ )
+ end
+
+ @impl Plug
+ @doc """
+ If marked as skipped, returns `conn`, otherwise calls `perform/2`.
+ Note: multiple invocations of the same plug (with different or same options) are allowed.
+ """
+ def call(%Plug.Conn{} = conn, options) do
+ if PlugHelper.plug_skipped?(conn, __MODULE__) do
+ conn
+ else
+ conn =
+ PlugHelper.append_to_private_list(
+ conn,
+ PlugHelper.called_plugs_list_id(),
+ __MODULE__
+ )
+
+ apply(__MODULE__, :perform, [conn, options])
+ end
+ end
+ end
+ end
+
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex
index 0f8ece2c4..57c3a9c3a 100644
--- a/lib/pleroma/workers/background_worker.ex
+++ b/lib/pleroma/workers/background_worker.ex
@@ -35,7 +35,7 @@ defmodule Pleroma.Workers.BackgroundWorker do
_job
) do
blocker = User.get_cached_by_id(blocker_id)
- User.perform(:blocks_import, blocker, blocked_identifiers)
+ {:ok, User.perform(:blocks_import, blocker, blocked_identifiers)}
end
def perform(
@@ -47,7 +47,7 @@ defmodule Pleroma.Workers.BackgroundWorker do
_job
) do
follower = User.get_cached_by_id(follower_id)
- User.perform(:follow_import, follower, followed_identifiers)
+ {:ok, User.perform(:follow_import, follower, followed_identifiers)}
end
def perform(%{"op" => "media_proxy_preload", "message" => message}, _job) do