diff options
Diffstat (limited to 'lib')
88 files changed, 2433 insertions, 535 deletions
diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index 6ab7fe8ef..dd2b9c8f2 100644 --- a/lib/mix/tasks/pleroma/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -67,8 +67,7 @@ defmodule Mix.Tasks.Pleroma.Benchmark do Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ activities: activities, for: user, - as: :activity, - skip_relationships: true + as: :activity }) end }, diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex index 7d09e70c5..3595f912d 100644 --- a/lib/mix/tasks/pleroma/digest.ex +++ b/lib/mix/tasks/pleroma/digest.ex @@ -1,5 +1,6 @@ defmodule Mix.Tasks.Pleroma.Digest do use Mix.Task + import Mix.Pleroma @shortdoc "Manages digest emails" @moduledoc File.read!("docs/administration/CLI_tasks/digest.md") @@ -22,12 +23,10 @@ defmodule Mix.Tasks.Pleroma.Digest do with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do {:ok, _} = Pleroma.Emails.Mailer.deliver(email) - Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})") + shell_info("Digest email have been sent to #{nickname} (#{user.email})") else _ -> - Mix.shell().info( - "Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}" - ) + shell_info("Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}") end end end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index a00bc0624..9d3d92b38 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -56,7 +56,7 @@ defmodule Pleroma.Application do if (major == 22 and minor < 2) or major < 22 do raise " !!!OTP VERSION WARNING!!! - You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. + You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2. " end else diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex index e5b37f33e..815de7002 100644 --- a/lib/pleroma/bbs/authenticator.ex +++ b/lib/pleroma/bbs/authenticator.ex @@ -4,7 +4,7 @@ defmodule Pleroma.BBS.Authenticator do use Sshd.PasswordAuthenticator - alias Comeonin.Pbkdf2 + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.User def authenticate(username, password) do @@ -12,7 +12,7 @@ defmodule Pleroma.BBS.Authenticator do password = to_string(password) with %User{} = user <- User.get_by_nickname(username) do - Pbkdf2.checkpw(password, user.password_hash) + AuthenticationPlug.checkpw(password, user.password_hash) else _e -> false end diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex index c7bc8ef6c..12d64c2fe 100644 --- a/lib/pleroma/bbs/handler.ex +++ b/lib/pleroma/bbs/handler.ex @@ -66,7 +66,7 @@ defmodule Pleroma.BBS.Handler do with %Activity{} <- Activity.get_by_id(activity_id), {:ok, _activity} <- - CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do + CommonAPI.post(user, %{status: rest, in_reply_to_status_id: activity_id}) do IO.puts("Replied!") else _e -> IO.puts("Could not reply...") @@ -78,7 +78,7 @@ defmodule Pleroma.BBS.Handler do def handle_command(%{user: user} = state, "p " <> text) do text = String.trim(text) - with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do + with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do IO.puts("Posted!") else _e -> IO.puts("Could not post...") diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex index 4097ee5b7..2b43d4c36 100644 --- a/lib/pleroma/config/config_db.ex +++ b/lib/pleroma/config/config_db.ex @@ -278,6 +278,8 @@ defmodule Pleroma.ConfigDB do } end + defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]} + defp do_convert(entity) when is_tuple(entity) do value = entity @@ -321,6 +323,15 @@ defmodule Pleroma.ConfigDB do {:proxy_url, {do_transform_string(type), parse_host(host), port}} end + defp do_transform(%{"tuple" => [":partial_chain", entity]}) do + {partial_chain, []} = + entity + |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "") + |> Code.eval_string() + + {:partial_chain, partial_chain} + end + defp do_transform(%{"tuple" => entity}) do Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end) end diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 3a9eec5ea..06174f624 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -17,7 +17,8 @@ defmodule Pleroma.Constants do "announcement_count", "emoji", "context_id", - "deleted_activity_id" + "deleted_activity_id", + "pleroma_internal" ] ) diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex index 74f8b2615..d1cf1f487 100644 --- a/lib/pleroma/docs/json.ex +++ b/lib/pleroma/docs/json.ex @@ -18,7 +18,6 @@ defmodule Pleroma.Docs.JSON do with config <- Pleroma.Config.Loader.read("config/description.exs") do config[:pleroma][:config_description] |> Pleroma.Docs.Generator.convert_to_strings() - |> Jason.encode!() end end end diff --git a/lib/pleroma/healthcheck.ex b/lib/pleroma/healthcheck.ex index 8f7f43ec2..92ce83cb7 100644 --- a/lib/pleroma/healthcheck.ex +++ b/lib/pleroma/healthcheck.ex @@ -29,7 +29,7 @@ defmodule Pleroma.Healthcheck do @spec system_info() :: t() def system_info do %Healthcheck{ - memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2) + memory_used: Float.round(:recon_alloc.memory(:allocated) / 1024 / 1024, 2) } |> assign_db_info() |> assign_job_queue_stats() diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex index d353a4dad..01b743f4f 100644 --- a/lib/pleroma/mfa.ex +++ b/lib/pleroma/mfa.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA do @@ -7,7 +7,6 @@ defmodule Pleroma.MFA do The MFA context. """ - alias Comeonin.Pbkdf2 alias Pleroma.User alias Pleroma.MFA.BackupCodes @@ -72,7 +71,7 @@ defmodule Pleroma.MFA do @spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()} def generate_backup_codes(%User{} = user) do with codes <- BackupCodes.generate(), - hashed_codes <- Enum.map(codes, &Pbkdf2.hashpwsalt/1), + hashed_codes <- Enum.map(codes, &Pbkdf2.hash_pwd_salt/1), changeset <- Changeset.cast_backup_codes(user, hashed_codes), {:ok, _} <- User.update_and_set_cache(changeset) do {:ok, codes} diff --git a/lib/pleroma/mfa/backup_codes.ex b/lib/pleroma/mfa/backup_codes.ex index 2b5ec34f8..9875310ff 100644 --- a/lib/pleroma/mfa/backup_codes.ex +++ b/lib/pleroma/mfa/backup_codes.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA.BackupCodes do diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex index 9b020aa8e..77c4fa202 100644 --- a/lib/pleroma/mfa/changeset.ex +++ b/lib/pleroma/mfa/changeset.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA.Changeset do diff --git a/lib/pleroma/mfa/settings.ex b/lib/pleroma/mfa/settings.ex index 2764b889c..de6e2228f 100644 --- a/lib/pleroma/mfa/settings.ex +++ b/lib/pleroma/mfa/settings.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA.Settings do diff --git a/lib/pleroma/mfa/token.ex b/lib/pleroma/mfa/token.ex index 25ff7fb29..0b2449971 100644 --- a/lib/pleroma/mfa/token.ex +++ b/lib/pleroma/mfa/token.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA.Token do diff --git a/lib/pleroma/mfa/totp.ex b/lib/pleroma/mfa/totp.ex index 1407afc57..d2ea2b3aa 100644 --- a/lib/pleroma/mfa/totp.ex +++ b/lib/pleroma/mfa/totp.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA.TOTP do diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index c135306ca..8aa9ed2d4 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -368,13 +368,7 @@ defmodule Pleroma.Notification do def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do - potential_receiver_ap_ids = - [] - |> Utils.maybe_notify_to_recipients(activity) - |> Utils.maybe_notify_mentioned_recipients(activity) - |> Utils.maybe_notify_subscribers(activity) - |> Utils.maybe_notify_followers(activity) - |> Enum.uniq() + potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity) potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only) @@ -392,6 +386,27 @@ defmodule Pleroma.Notification do def get_notified_from_activity(_, _local_only), do: {[], []} + # For some activities, only notify the author of the object + def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}}) + when type in ~w{Like Announce EmojiReact} do + case Object.get_cached_by_ap_id(object_id) do + %Object{data: %{"actor" => actor}} -> + [actor] + + _ -> + [] + end + end + + def get_potential_receiver_ap_ids(activity) do + [] + |> Utils.maybe_notify_to_recipients(activity) + |> Utils.maybe_notify_mentioned_recipients(activity) + |> Utils.maybe_notify_subscribers(activity) + |> Utils.maybe_notify_followers(activity) + |> Enum.uniq() + end + @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 \\ []) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index e678fd415..546c4ea01 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -9,11 +9,13 @@ defmodule Pleroma.Object do import Ecto.Changeset alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.Object alias Pleroma.Object.Fetcher alias Pleroma.ObjectTombstone alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Workers.AttachmentsCleanupWorker require Logger @@ -138,12 +140,17 @@ defmodule Pleroma.Object do def normalize(_, _, _), do: nil - # Owned objects can only be mutated by their owner - def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}), - do: actor == ap_id + # Owned objects can only be accessed by their owner + def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do + if actor == ap_id do + :ok + else + {:error, :forbidden} + end + end - # Legacy objects can be mutated by anybody - def authorize_mutation(%Object{}, %User{}), do: true + # Legacy objects can be accessed by anybody + def authorize_access(%Object{}, %User{}), do: :ok @spec get_cached_by_ap_id(String.t()) :: Object.t() | nil def get_cached_by_ap_id(ap_id) do @@ -183,27 +190,37 @@ defmodule Pleroma.Object do def delete(%Object{data: %{"id" => id}} = object) do with {:ok, _obj} = swap_object_with_tombstone(object), deleted_activity = Activity.delete_all_by_object_ap_id(id), - {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), - {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do - with true <- Pleroma.Config.get([:instance, :cleanup_attachments]) do - {:ok, _} = - Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{ - "object" => object - }) - end + {:ok, _} <- invalid_object_cache(object) do + cleanup_attachments( + Config.get([:instance, :cleanup_attachments]), + %{"object" => object} + ) {:ok, object, deleted_activity} end end - def prune(%Object{data: %{"id" => id}} = object) do + @spec cleanup_attachments(boolean(), %{required(:object) => map()}) :: + {:ok, Oban.Job.t() | nil} + def cleanup_attachments(true, %{"object" => _} = params) do + AttachmentsCleanupWorker.enqueue("cleanup_attachments", params) + end + + def cleanup_attachments(_, _), do: {:ok, nil} + + def prune(%Object{data: %{"id" => _id}} = object) do with {:ok, object} <- Repo.delete(object), - {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), - {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do + {:ok, _} <- invalid_object_cache(object) do {:ok, object} end end + def invalid_object_cache(%Object{data: %{"id" => id}}) do + with {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do + Cachex.del(:web_resp_cache, URI.parse(id).path) + end + end + def set_cache(%Object{data: %{"id" => ap_id}} = object) do Cachex.put(:object_cache, "object:#{ap_id}", object) {:ok, object} diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex index 0061c69dc..057ea42f1 100644 --- a/lib/pleroma/plugs/authentication_plug.ex +++ b/lib/pleroma/plugs/authentication_plug.ex @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Plugs.AuthenticationPlug do - alias Comeonin.Pbkdf2 alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User @@ -17,8 +16,13 @@ defmodule Pleroma.Plugs.AuthenticationPlug do :crypt.crypt(password, password_hash) == password_hash end + def checkpw(password, "$2" <> _ = password_hash) do + # Handle bcrypt passwords for Mastodon migration + Bcrypt.verify_pass(password, password_hash) + end + def checkpw(password, "$pbkdf2" <> _ = password_hash) do - Pbkdf2.checkpw(password, password_hash) + Pbkdf2.verify_pass(password, password_hash) end def checkpw(_password, _password_hash) do @@ -26,6 +30,25 @@ defmodule Pleroma.Plugs.AuthenticationPlug do false end + def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do + do_update_password(user, password) + end + + def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do + do_update_password(user, password) + end + + def maybe_update_password(user, _), do: {:ok, user} + + defp do_update_password(user, password) do + user + |> User.password_update_changeset(%{ + "password" => password, + "password_confirmation" => password + }) + |> Pleroma.Repo.update() + end + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call( @@ -37,7 +60,9 @@ defmodule Pleroma.Plugs.AuthenticationPlug do } = conn, _ ) do - if Pbkdf2.checkpw(password, password_hash) do + if checkpw(password, password_hash) do + {:ok, auth_user} = maybe_update_password(auth_user, password) + conn |> assign(:user, auth_user) |> OAuthScopesPlug.skip_plug() @@ -47,7 +72,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do end def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do - Pbkdf2.dummy_checkpw() + Pbkdf2.no_user_verify() conn end diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex index 8ff06a462..0937cb7db 100644 --- a/lib/pleroma/scheduled_activity.ex +++ b/lib/pleroma/scheduled_activity.ex @@ -40,7 +40,7 @@ defmodule Pleroma.ScheduledActivity do %{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset ) when is_list(media_ids) do - media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids}) + media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids}) params = params diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 762d813d9..1be1a3a5b 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -134,7 +134,7 @@ defmodule Pleroma.Upload do end end - defp prepare_upload(%{"img" => "data:image/" <> image_data}, opts) do + defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data) data = Base.decode64!(parsed["data"], ignore: :whitespace) hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data))) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 278129ad2..e8013bf40 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -9,7 +9,6 @@ defmodule Pleroma.User do import Ecto.Query import Ecto, only: [assoc: 2] - alias Comeonin.Pbkdf2 alias Ecto.Multi alias Pleroma.Activity alias Pleroma.Config @@ -1205,7 +1204,9 @@ defmodule Pleroma.User 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}) + query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false}) + + query |> Repo.all() end @@ -1566,10 +1567,23 @@ defmodule Pleroma.User do |> Stream.run() end - defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do - {:ok, delete_data, _} = Builder.delete(user, object) + defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do + with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)}, + {:ok, delete_data, _} <- Builder.delete(user, object) do + Pipeline.common_pipeline(delete_data, local: user.local) + else + {:find_object, nil} -> + # We have the create activity, but not the object, it was probably pruned. + # Insert a tombstone and try again + with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object), + {:ok, _tombstone} <- Object.create(tombstone_data) do + delete_activity(activity, user) + end - Pipeline.common_pipeline(delete_data, local: user.local) + e -> + Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}") + Logger.error("Error: #{inspect(e)}") + end end defp delete_activity(%{data: %{"type" => type}} = activity, user) @@ -1925,7 +1939,7 @@ defmodule Pleroma.User do defp put_password_hash( %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset ) do - change(changeset, password_hash: Pbkdf2.hashpwsalt(password)) + change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password)) end defp put_password_hash(changeset), do: changeset diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 3a3b04793..293bbc082 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -167,20 +167,18 @@ defmodule Pleroma.User.Query do end defp compose_query({:recipients_from_activity, to}, query) do - query - |> join(:left, [u], r in FollowingRelationship, - as: :relationships, - on: r.follower_id == u.id - ) - |> join(:left, [relationships: r], f in User, - as: :following, - on: f.id == r.following_id - ) - |> where( - [u, following: f, relationships: r], - u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept) + following_query = + from(u in User, + join: f in FollowingRelationship, + on: u.id == f.following_id, + where: f.state == ^:follow_accept, + where: u.follower_address in ^to, + select: f.follower_id + ) + + from(u in query, + where: u.ap_id in ^to or u.id in subquery(following_query) ) - |> distinct(true) end defp compose_query({:order_by, key}, query) do diff --git a/lib/pleroma/user/welcome_message.ex b/lib/pleroma/user/welcome_message.ex index f0ac8ebae..f8f520285 100644 --- a/lib/pleroma/user/welcome_message.ex +++ b/lib/pleroma/user/welcome_message.ex @@ -10,8 +10,8 @@ defmodule Pleroma.User.WelcomeMessage do with %User{} = sender_user <- welcome_user(), message when is_binary(message) <- welcome_message() do CommonAPI.post(sender_user, %{ - "visibility" => "direct", - "status" => "@#{user.nickname}\n#{message}" + visibility: "direct", + status: "@#{user.nickname}\n#{message}" }) else _ -> {:ok, nil} diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 235ad427c..6dfdd2860 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -87,6 +87,22 @@ defmodule Pleroma.UserRelationship do source_to_target_rel_types \\ nil, target_to_source_rel_types \\ nil ) + + def dictionary( + _source_users, + _target_users, + [] = _source_to_target_rel_types, + [] = _target_to_source_rel_types + ) do + [] + end + + def dictionary( + source_users, + target_users, + source_to_target_rel_types, + target_to_source_rel_types + ) when is_list(source_users) and is_list(target_users) do source_user_ids = User.binary_id(source_users) target_user_ids = User.binary_id(target_users) @@ -138,11 +154,16 @@ defmodule Pleroma.UserRelationship do def view_relationships_option(%User{} = reading_user, actors, opts) do {source_to_target_rel_types, target_to_source_rel_types} = - if opts[:source_mutes_only] do - # This option is used for rendering statuses (FE needs `muted` flag for each one anyways) - {[:mute], []} - else - {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]} + case opts[:subset] do + :source_mutes -> + # Used for statuses rendering (FE needs `muted` flag for each status when statuses load) + {[:mute], []} + + nil -> + {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]} + + unknown -> + raise "Unsupported :subset option value: #{inspect(unknown)}" end user_relationships = @@ -153,7 +174,17 @@ defmodule Pleroma.UserRelationship do target_to_source_rel_types ) - following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors) + following_relationships = + case opts[:subset] do + :source_mutes -> + [] + + nil -> + FollowingRelationship.all_between_user_sets([reading_user], actors) + + unknown -> + raise "Unsupported :subset option value: #{inspect(unknown)}" + end %{user_relationships: user_relationships, following_relationships: following_relationships} end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 4955243ab..d752f4f04 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -439,7 +439,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end defp do_block(blocker, blocked, activity_id, local) do - outgoing_blocks = Config.get([:activitypub, :outgoing_blocks]) unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) if unfollow_blocked do @@ -447,8 +446,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do if follow_activity, do: unfollow(blocker, blocked, nil, local) end - with true <- outgoing_blocks, - block_data <- make_block_data(blocker, blocked, activity_id), + with block_data <- make_block_data(blocker, blocked, activity_id), {:ok, activity} <- insert(block_data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 922a444a9..4a247ad0c 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -62,6 +62,16 @@ defmodule Pleroma.Web.ActivityPub.Builder do }, []} end + @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()} + def tombstone(actor, id) do + {:ok, + %{ + "id" => id, + "actor" => actor, + "type" => "Tombstone" + }, []} + end + @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()} def like(actor, object) do with {:ok, data, meta} <- object_action(actor, object) do diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex index e06de3dff..f42c03510 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -51,6 +51,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do Page Question Video + Tombstone } def validate_data(cng) do cng diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index be7b57f13..80701bb63 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -14,7 +14,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility @@ -590,6 +592,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do + User.update_follower_count(followed) + User.update_following_count(follower) + ActivityPub.accept(%{ to: follow_activity.data["to"], type: "Accept", @@ -599,7 +604,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do activity_id: id }) else - _e -> :error + _e -> + :error end end @@ -720,6 +726,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do ) do with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} + else + {:error, {:validate_object, _}} = e -> + # Check if we have a create activity for this + with {:ok, object_id} <- Types.ObjectID.cast(data["object"]), + %Activity{data: %{"actor" => actor}} <- + Activity.create_by_object_ap_id(object_id) |> Repo.one(), + # We have one, insert a tombstone and retry + {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id), + {:ok, _tombstone} <- Object.create(tombstone_data) do + handle_incoming(data) + else + _ -> e + end end end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 09b80fa57..f2375bcc4 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do alias Ecto.Changeset alias Ecto.UUID alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -169,8 +170,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do Enqueues an activity for federation if it's local """ @spec maybe_federate(any()) :: :ok - def maybe_federate(%Activity{local: true} = activity) do - if Pleroma.Config.get!([:instance, :federating]) do + def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do + outgoing_blocks = Config.get([:activitypub, :outgoing_blocks]) + + with true <- Config.get!([:instance, :federating]), + true <- type != "Block" || outgoing_blocks do Pleroma.Web.Federator.publish(activity) end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 9f1fd3aeb..647ceb3ba 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -22,6 +22,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ModerationLogView @@ -30,14 +31,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint + alias Pleroma.Web.MastodonAPI alias Pleroma.Web.MastodonAPI.AppView - alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.OAuth.App alias Pleroma.Web.Router require Logger - @descriptions_json Pleroma.Docs.JSON.compile() + @descriptions Pleroma.Docs.JSON.compile() @users_page_size 50 plug( @@ -280,8 +281,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do }) conn - |> put_view(Pleroma.Web.AdminAPI.StatusView) - |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false}) + |> put_view(AdminAPI.StatusView) + |> render("index.json", %{activities: activities, as: :activity}) end def list_user_statuses(conn, %{"nickname" => nickname} = params) do @@ -299,8 +300,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do }) conn - |> put_view(StatusView) - |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false}) + |> put_view(MastodonAPI.StatusView) + |> render("index.json", %{activities: activities, as: :activity}) else _ -> {:error, :not_found} end @@ -829,14 +830,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do }) conn - |> put_view(Pleroma.Web.AdminAPI.StatusView) - |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false}) + |> put_view(AdminAPI.StatusView) + |> render("index.json", %{activities: activities, as: :activity}) end def status_show(conn, %{"id" => id}) do with %Activity{} = activity <- Activity.get_by_id(id) do conn - |> put_view(StatusView) + |> put_view(MastodonAPI.StatusView) |> render("show.json", %{activity: activity}) else _ -> errors(conn, {:error, :not_found}) @@ -844,19 +845,24 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do + params = + params + |> Map.take(["sensitive", "visibility"]) + |> Map.new(fn {key, value} -> {String.to_existing_atom(key), value} end) + with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do - {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"]) + {:ok, sensitive} = Ecto.Type.cast(:boolean, params[:sensitive]) ModerationLog.insert_log(%{ action: "status_update", actor: admin, subject: activity, sensitive: sensitive, - visibility: params["visibility"] + visibility: params[:visibility] }) conn - |> put_view(StatusView) + |> put_view(MastodonAPI.StatusView) |> render("show.json", %{activity: activity}) end end @@ -892,9 +898,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def config_descriptions(conn, _params) do - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.send_resp(200, @descriptions_json) + descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) + + json(conn, descriptions) end def config_show(conn, %{"only_db" => true}) do @@ -949,7 +955,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do def config_update(conn, %{"configs" => configs}) do with :ok <- configurable_from_database(conn) do {_errors, results} = - Enum.map(configs, fn + configs + |> Enum.filter(&whitelisted_config?/1) + |> Enum.map(fn %{"group" => group, "key" => key, "delete" => true} = params -> ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]}) @@ -1011,6 +1019,28 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end + defp whitelisted_config?(group, key) do + if whitelisted_configs = Config.get(:database_config_whitelist) do + Enum.any?(whitelisted_configs, fn + {whitelisted_group} -> + group == inspect(whitelisted_group) + + {whitelisted_group, whitelisted_key} -> + group == inspect(whitelisted_group) && key == inspect(whitelisted_key) + end) + else + true + end + end + + defp whitelisted_config?(%{"group" => group, "key" => key}) do + whitelisted_config?(group, key) + end + + defp whitelisted_config?(%{:group => group} = config) do + whitelisted_config?(group, config[:key]) + end + def reload_emoji(conn, _params) do Pleroma.Emoji.reload() diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index a16a3ebf0..46dadb5ee 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -6,7 +6,9 @@ defmodule Pleroma.Web.AdminAPI.AccountView do use Pleroma.Web, :view alias Pleroma.User + alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.MastodonAPI alias Pleroma.Web.MediaProxy def render("index.json", %{users: users, count: count, page_size: page_size}) do @@ -119,6 +121,13 @@ defmodule Pleroma.Web.AdminAPI.AccountView do } end + def merge_account_views(%User{} = user) do + MastodonAPI.AccountView.render("show.json", %{user: user}) + |> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user})) + end + + def merge_account_views(_), do: %{} + defp parse_error([]), do: "" defp parse_error(errors) do diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index d50969b2a..f432b8c2c 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -7,10 +7,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do alias Pleroma.HTML alias Pleroma.User + alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.StatusView + defdelegate merge_account_views(user), to: AdminAPI.AccountView + def render("index.json", %{reports: reports}) do %{ reports: @@ -41,8 +44,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do statuses: StatusView.render("index.json", %{ activities: statuses, - as: :activity, - skip_relationships: false + as: :activity }), state: report.data["state"], notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}) @@ -70,11 +72,4 @@ defmodule Pleroma.Web.AdminAPI.ReportView do created_at: Utils.to_masto_date(inserted_at) } end - - defp merge_account_views(%User{} = user) do - Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) - |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) - end - - defp merge_account_views(_), do: %{} end diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex index 3637dee24..500800be2 100644 --- a/lib/pleroma/web/admin_api/views/status_view.ex +++ b/lib/pleroma/web/admin_api/views/status_view.ex @@ -7,24 +7,19 @@ defmodule Pleroma.Web.AdminAPI.StatusView do require Pleroma.Constants - alias Pleroma.User - alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.AdminAPI + alias Pleroma.Web.MastodonAPI + + defdelegate merge_account_views(user), to: AdminAPI.AccountView 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 = StatusView.get_user(activity.data["actor"]) + user = MastodonAPI.StatusView.get_user(activity.data["actor"]) - StatusView.render("show.json", opts) + MastodonAPI.StatusView.render("show.json", opts) |> Map.merge(%{account: merge_account_views(user)}) end - - defp merge_account_views(%User{} = user) do - Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) - |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) - end - - defp merge_account_views(_), do: %{} end diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex index 183df43ee..a9cfe0fed 100644 --- a/lib/pleroma/web/api_spec/helpers.ex +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ApiSpec.Helpers do alias OpenApiSpex.Operation alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike def request_body(description, schema_ref, opts \\ []) do media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"] @@ -47,6 +48,15 @@ defmodule Pleroma.Web.ApiSpec.Helpers do ] end + def with_relationships_param do + Operation.parameter( + :with_relationships, + :query, + BooleanLike, + "Embed relationships into accounts." + ) + end + def empty_object_response do Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}}) end @@ -54,4 +64,8 @@ defmodule Pleroma.Web.ApiSpec.Helpers do def empty_array_response do Operation.response("Empty array", "application/json", %Schema{type: :array, example: []}) end + + def no_content_response do + Operation.response("No Content", "application/json", %Schema{type: :string, example: ""}) + end end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 70069d6f9..20572f8ea 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -155,8 +155,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do 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(), + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, + with_relationships_param() | pagination_params() + ], responses: %{ 200 => Operation.response("Accounts", "application/json", array_of_accounts()) } @@ -171,8 +173,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do 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(), + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, + with_relationships_param() | pagination_params() + ], responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())} } end @@ -367,15 +371,18 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do title: "AccountCreateRequest", description: "POST body for creating an account", type: :object, + required: [:username, :password, :agreement], properties: %{ reason: %Schema{ type: :string, + nullable: true, 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, + nullable: true, description: "The email address to be used for login. Required when `account_activation_required` is enabled.", format: :email @@ -386,29 +393,39 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do format: :password }, agreement: %Schema{ - type: :boolean, + allOf: [BooleanLike], 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, + nullable: true, 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: ""}, + fullname: %Schema{type: :string, nullable: true, description: "Full name"}, + bio: %Schema{type: :string, description: "Bio", nullable: true, default: ""}, captcha_solution: %Schema{ type: :string, + nullable: true, 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"}, + captcha_token: %Schema{ + type: :string, + nullable: true, + description: "Provider-specific captcha token" + }, + captcha_answer_data: %Schema{ + type: :string, + nullable: true, + description: "Provider-specific captcha data" + }, token: %Schema{ type: :string, + nullable: true, description: "Invite token required when the registrations aren't public" } }, - required: [:username, :password, :agreement], example: %{ "username" => "cofe", "email" => "cofe@example.com", @@ -446,29 +463,35 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do type: :object, properties: %{ bot: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Whether the account has a bot flag." }, display_name: %Schema{ type: :string, + nullable: true, description: "The display name to use for the profile." }, note: %Schema{type: :string, description: "The account bio."}, avatar: %Schema{ type: :string, + nullable: true, description: "Avatar image encoded using multipart/form-data", format: :binary }, header: %Schema{ type: :string, + nullable: true, description: "Header image encoded using multipart/form-data", format: :binary }, locked: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Whether manual approval of follow requests is required." }, fields_attributes: %Schema{ + nullable: true, oneOf: [ %Schema{type: :array, items: attribute_field()}, %Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}} @@ -487,48 +510,66 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do # Pleroma-specific fields no_rich_text: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, 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: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "user's followers will be hidden" + }, + hide_follows: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "user's follows will be hidden" + }, hide_followers_count: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "user's follower count will be hidden" }, hide_follows_count: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "user's follow count will be hidden" }, hide_favorites: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "user's favorites timeline will be hidden" }, show_role: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, 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, + nullable: true, description: "Opaque user settings to be saved on the backend." }, skip_thread_containment: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Skip filtering out broken threads" }, allow_following_move: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Allows automatically follow moved following accounts" }, pleroma_background_image: %Schema{ type: :string, + nullable: true, description: "Sets the background image of the user.", format: :binary }, discoverable: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Discovery of this account in search results and other services is allowed." }, @@ -624,7 +665,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do description: "POST body for muting an account", type: :object, properties: %{ - uri: %Schema{type: :string, format: :uri} + uri: %Schema{type: :string, nullable: true, format: :uri} }, required: [:uri] } @@ -637,7 +678,8 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do type: :object, properties: %{ notifications: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Mute notifications in addition to statuses? Defaults to true.", default: true } diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex index f6ccd073f..ae01cbbec 100644 --- a/lib/pleroma/web/api_spec/operations/app_operation.ex +++ b/lib/pleroma/web/api_spec/operations/app_operation.ex @@ -105,7 +105,11 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do description: "Space separated list of scopes", default: "read" }, - website: %Schema{type: :string, description: "A URL to the homepage of your app"} + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of your app" + } }, required: [:client_name, :redirect_uris], example: %{ diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex index 53e57b46b..31e576f99 100644 --- a/lib/pleroma/web/api_spec/operations/filter_operation.ex +++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -171,7 +172,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do type: :object, properties: %{ irreversible: %Schema{ - type: :bolean, + allOf: [BooleanLike], description: "Should the server irreversibly drop matching entities from home and notifications?", default: false @@ -199,12 +200,14 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified." }, irreversible: %Schema{ - type: :bolean, + allOf: [BooleanLike], + nullable: true, description: "Should the server irreversibly drop matching entities from home and notifications?" }, whole_word: %Schema{ - type: :bolean, + allOf: [BooleanLike], + nullable: true, description: "Consider word boundaries?", default: true } diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 880bd3f1b..d5c335d0c 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -125,11 +125,17 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do }, avatar_upload_limit: %Schema{type: :integer, description: "The title of the website"}, background_upload_limit: %Schema{type: :integer, description: "The title of the website"}, - banner_upload_limit: %Schema{type: :integer, description: "The title of the website"} + banner_upload_limit: %Schema{type: :integer, description: "The title of the website"}, + background_image: %Schema{ + type: :string, + format: :uri, + description: "The background image for the website" + } }, example: %{ "avatar_upload_limit" => 2_000_000, "background_upload_limit" => 4_000_000, + "background_image" => "/static/image.png", "banner_upload_limit" => 4_000_000, "description" => "A Pleroma instance, an alternative fediverse server", "email" => "lain@lain.com", diff --git a/lib/pleroma/web/api_spec/operations/marker_operation.ex b/lib/pleroma/web/api_spec/operations/marker_operation.ex index 06620492a..714ef1f99 100644 --- a/lib/pleroma/web/api_spec/operations/marker_operation.ex +++ b/lib/pleroma/web/api_spec/operations/marker_operation.ex @@ -110,14 +110,16 @@ defmodule Pleroma.Web.ApiSpec.MarkerOperation do properties: %{ notifications: %Schema{ type: :object, + nullable: true, properties: %{ - last_read_id: %Schema{type: :string} + last_read_id: %Schema{nullable: true, type: :string} } }, home: %Schema{ type: :object, + nullable: true, properties: %{ - last_read_id: %Schema{type: :string} + last_read_id: %Schema{nullable: true, type: :string} } } }, diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex new file mode 100644 index 000000000..d9c3c42db --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/media_operation.ex @@ -0,0 +1,132 @@ +# 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.MediaOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.Attachment + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def create_operation do + %Operation{ + tags: ["media"], + summary: "Upload media as attachment", + description: "Creates an attachment to be used with a new status.", + operationId: "MediaController.create", + security: [%{"oAuth" => ["write:media"]}], + requestBody: Helpers.request_body("Parameters", create_request()), + responses: %{ + 200 => Operation.response("Media", "application/json", Attachment), + 401 => Operation.response("Media", "application/json", ApiError), + 422 => Operation.response("Media", "application/json", ApiError) + } + } + end + + defp create_request do + %Schema{ + title: "MediaCreateRequest", + description: "POST body for creating an attachment", + type: :object, + required: [:file], + properties: %{ + file: %Schema{ + type: :string, + format: :binary, + description: "The file to be attached, using multipart form data." + }, + description: %Schema{ + type: :string, + description: "A plain-text description of the media, for accessibility purposes." + }, + focus: %Schema{ + type: :string, + description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0." + } + } + } + end + + def update_operation do + %Operation{ + tags: ["media"], + summary: "Upload media as attachment", + description: "Creates an attachment to be used with a new status.", + operationId: "MediaController.update", + security: [%{"oAuth" => ["write:media"]}], + parameters: [id_param()], + requestBody: Helpers.request_body("Parameters", update_request()), + responses: %{ + 200 => Operation.response("Media", "application/json", Attachment), + 400 => Operation.response("Media", "application/json", ApiError), + 401 => Operation.response("Media", "application/json", ApiError), + 422 => Operation.response("Media", "application/json", ApiError) + } + } + end + + defp update_request do + %Schema{ + title: "MediaUpdateRequest", + description: "POST body for updating an attachment", + type: :object, + properties: %{ + file: %Schema{ + type: :string, + format: :binary, + description: "The file to be attached, using multipart form data." + }, + description: %Schema{ + type: :string, + description: "A plain-text description of the media, for accessibility purposes." + }, + focus: %Schema{ + type: :string, + description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0." + } + } + } + end + + def show_operation do + %Operation{ + tags: ["media"], + summary: "Show Uploaded media attachment", + operationId: "MediaController.show", + parameters: [id_param()], + security: [%{"oAuth" => ["read:media"]}], + responses: %{ + 200 => Operation.response("Media", "application/json", Attachment), + 401 => Operation.response("Media", "application/json", ApiError), + 422 => Operation.response("Media", "application/json", ApiError) + } + } + end + + def create2_operation do + %Operation{ + tags: ["media"], + summary: "Upload media as attachment", + description: "Creates an attachment to be used with a new status.", + operationId: "MediaController.create2", + security: [%{"oAuth" => ["write:media"]}], + requestBody: Helpers.request_body("Parameters", create_request()), + responses: %{ + 202 => Operation.response("Media", "application/json", Attachment), + 422 => Operation.response("Media", "application/json", ApiError), + 500 => Operation.response("Media", "application/json", ApiError) + } + } + end + + defp id_param do + Operation.parameter(:id, :path, :string, "The ID of the Attachment entity") + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex new file mode 100644 index 000000000..90922c064 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex @@ -0,0 +1,187 @@ +# 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.PleromaAccountOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.StatusOperation + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def confirmation_resend_operation do + %Operation{ + tags: ["Accounts"], + summary: "Resend confirmation email. Expects `email` or `nickname`", + operationId: "PleromaAPI.AccountController.confirmation_resend", + parameters: [ + Operation.parameter(:email, :query, :string, "Email of that needs to be verified", + example: "cofe@cofe.io" + ), + Operation.parameter( + :nickname, + :query, + :string, + "Nickname of user that needs to be verified", + example: "cofefe" + ) + ], + responses: %{ + 204 => no_content_response() + } + } + end + + def update_avatar_operation do + %Operation{ + tags: ["Accounts"], + summary: "Set/clear user avatar image", + operationId: "PleromaAPI.AccountController.update_avatar", + requestBody: + request_body("Parameters", update_avatar_or_background_request(), required: true), + security: [%{"oAuth" => ["write:accounts"]}], + responses: %{ + 200 => update_response(), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def update_banner_operation do + %Operation{ + tags: ["Accounts"], + summary: "Set/clear user banner image", + operationId: "PleromaAPI.AccountController.update_banner", + requestBody: request_body("Parameters", update_banner_request(), required: true), + security: [%{"oAuth" => ["write:accounts"]}], + responses: %{ + 200 => update_response() + } + } + end + + def update_background_operation do + %Operation{ + tags: ["Accounts"], + summary: "Set/clear user background image", + operationId: "PleromaAPI.AccountController.update_background", + security: [%{"oAuth" => ["write:accounts"]}], + requestBody: + request_body("Parameters", update_avatar_or_background_request(), required: true), + responses: %{ + 200 => update_response() + } + } + end + + def favourites_operation do + %Operation{ + tags: ["Accounts"], + summary: "Returns favorites timeline of any user", + operationId: "PleromaAPI.AccountController.favourites", + parameters: [id_param() | pagination_params()], + security: [%{"oAuth" => ["read:favourites"]}], + responses: %{ + 200 => + Operation.response( + "Array of Statuses", + "application/json", + StatusOperation.array_of_statuses() + ), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def subscribe_operation do + %Operation{ + tags: ["Accounts"], + summary: "Subscribe to receive notifications for all statuses posted by a user", + operationId: "PleromaAPI.AccountController.subscribe", + parameters: [id_param()], + security: [%{"oAuth" => ["follow", "write:follows"]}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def unsubscribe_operation do + %Operation{ + tags: ["Accounts"], + summary: "Unsubscribe to stop receiving notifications from user statuses", + operationId: "PleromaAPI.AccountController.unsubscribe", + parameters: [id_param()], + security: [%{"oAuth" => ["follow", "write:follows"]}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp id_param do + Operation.parameter(:id, :path, FlakeID, "Account ID", + example: "9umDrYheeY451cQnEe", + required: true + ) + end + + defp update_avatar_or_background_request do + %Schema{ + title: "PleromaAccountUpdateAvatarOrBackgroundRequest", + type: :object, + properties: %{ + img: %Schema{ + nullable: true, + type: :string, + format: :binary, + description: "Image encoded using `multipart/form-data` or an empty string to clear" + } + } + } + end + + defp update_banner_request do + %Schema{ + title: "PleromaAccountUpdateBannerRequest", + type: :object, + properties: %{ + banner: %Schema{ + type: :string, + nullable: true, + format: :binary, + description: "Image encoded using `multipart/form-data` or an empty string to clear" + } + } + } + end + + defp update_response do + Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{ + type: :object, + properties: %{ + url: %Schema{ + type: :string, + format: :uri, + nullable: true, + description: "Image URL" + } + }, + example: %{ + "url" => + "https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif" + } + }) + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex new file mode 100644 index 000000000..8c5f37ea6 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex @@ -0,0 +1,79 @@ +# 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.PleromaMascotOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def show_operation do + %Operation{ + tags: ["Mascot"], + summary: "Gets user mascot image", + security: [%{"oAuth" => ["read:accounts"]}], + operationId: "PleromaAPI.MascotController.show", + responses: %{ + 200 => Operation.response("Mascot", "application/json", mascot()) + } + } + end + + def update_operation do + %Operation{ + tags: ["Mascot"], + summary: "Set/clear user avatar image", + description: + "Behaves exactly the same as `POST /api/v1/upload`. Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`.", + operationId: "PleromaAPI.MascotController.update", + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + file: %Schema{type: :string, format: :binary} + } + }, + required: true + ), + security: [%{"oAuth" => ["write:accounts"]}], + responses: %{ + 200 => Operation.response("Mascot", "application/json", mascot()), + 415 => Operation.response("Unsupported Media Type", "application/json", ApiError) + } + } + end + + defp mascot do + %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + url: %Schema{type: :string, format: :uri}, + type: %Schema{type: :string}, + pleroma: %Schema{ + type: :object, + properties: %{ + mime_type: %Schema{type: :string} + } + } + }, + example: %{ + "id" => "abcdefg", + "url" => "https://pleroma.example.org/media/abcdefg.png", + "type" => "image", + "pleroma" => %{ + "mime_type" => "image/png" + } + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex new file mode 100644 index 000000000..85a22aa0b --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex @@ -0,0 +1,102 @@ +# 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.PleromaScrobbleOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Reference + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def create_operation do + %Operation{ + tags: ["Scrobbles"], + summary: "Creates a new Listen activity for an account", + security: [%{"oAuth" => ["write"]}], + operationId: "PleromaAPI.ScrobbleController.create", + requestBody: request_body("Parameters", create_request(), requried: true), + responses: %{ + 200 => Operation.response("Scrobble", "application/json", scrobble()) + } + } + end + + def index_operation do + %Operation{ + tags: ["Scrobbles"], + summary: "Requests a list of current and recent Listen activities for an account", + operationId: "PleromaAPI.ScrobbleController.index", + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params() + ], + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => + Operation.response("Array of Scrobble", "application/json", %Schema{ + type: :array, + items: scrobble() + }) + } + } + end + + defp create_request do + %Schema{ + type: :object, + required: [:title], + properties: %{ + title: %Schema{type: :string, description: "The title of the media playing"}, + album: %Schema{type: :string, description: "The album of the media playing"}, + artist: %Schema{type: :string, description: "The artist of the media playing"}, + length: %Schema{type: :integer, description: "The length of the media playing"}, + visibility: %Schema{ + allOf: [VisibilityScope], + default: "public", + description: "Scrobble visibility" + } + }, + example: %{ + "title" => "Some Title", + "artist" => "Some Artist", + "album" => "Some Album", + "length" => 180_000 + } + } + end + + defp scrobble do + %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + account: Account, + title: %Schema{type: :string, description: "The title of the media playing"}, + album: %Schema{type: :string, description: "The album of the media playing"}, + artist: %Schema{type: :string, description: "The artist of the media playing"}, + length: %Schema{ + type: :integer, + description: "The length of the media playing", + nullable: true + }, + created_at: %Schema{type: :string, format: :"date-time"} + }, + example: %{ + "id" => "1234", + "account" => Account.schema().example, + "title" => "Some Title", + "artist" => "Some Artist", + "album" => "Some Album", + "length" => 180_000, + "created_at" => "2019-09-28T12:40:45.000Z" + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/report_operation.ex b/lib/pleroma/web/api_spec/operations/report_operation.ex index da4d50703..b9b4c4f79 100644 --- a/lib/pleroma/web/api_spec/operations/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/report_operation.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -37,15 +38,18 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do account_id: %Schema{type: :string, description: "ID of the account to report"}, status_ids: %Schema{ type: :array, + nullable: true, items: %Schema{type: :string}, description: "Array of Statuses to attach to the report, for context" }, comment: %Schema{ type: :string, + nullable: true, description: "Reason for the report" }, forward: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, default: false, description: "If the account is remote, should the report be forwarded to the remote admin?" diff --git a/lib/pleroma/web/api_spec/operations/search_operation.ex b/lib/pleroma/web/api_spec/operations/search_operation.ex index 6ea00a9a8..169c36d87 100644 --- a/lib/pleroma/web/api_spec/operations/search_operation.ex +++ b/lib/pleroma/web/api_spec/operations/search_operation.ex @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do apply(__MODULE__, operation, []) end + # Note: `with_relationships` param is not supported (PleromaFE uses this op for autocomplete) def account_search_operation do %Operation{ tags: ["Search"], @@ -96,8 +97,8 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do :query, %Schema{type: :integer}, "Offset" - ) - | pagination_params() + ), + with_relationships_param() | pagination_params() ], responses: %{ 200 => Operation.response("Results", "application/json", results()) @@ -138,8 +139,8 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do :query, %Schema{allOf: [BooleanLike], default: false}, "Only include accounts that the user is following" - ) - | pagination_params() + ), + with_relationships_param() | pagination_params() ], responses: %{ 200 => Operation.response("Results", "application/json", results2()) diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex new file mode 100644 index 000000000..0682ca6e5 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -0,0 +1,518 @@ +# 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.StatusOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.AccountOperation + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus + alias Pleroma.Web.ApiSpec.Schemas.Status + alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Statuses"], + summary: "Get multiple statuses by IDs", + security: [%{"oAuth" => ["read:statuses"]}], + parameters: [ + Operation.parameter( + :ids, + :query, + %Schema{type: :array, items: FlakeID}, + "Array of status IDs" + ) + ], + operationId: "StatusController.index", + responses: %{ + 200 => Operation.response("Array of Status", "application/json", array_of_statuses()) + } + } + end + + def create_operation do + %Operation{ + tags: ["Statuses"], + summary: "Publish new status", + security: [%{"oAuth" => ["write:statuses"]}], + description: "Post a new status", + operationId: "StatusController.create", + requestBody: request_body("Parameters", create_request(), required: true), + responses: %{ + 200 => + Operation.response( + "Status. When `scheduled_at` is present, ScheduledStatus is returned instead", + "application/json", + %Schema{oneOf: [Status, ScheduledStatus]} + ), + 422 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def show_operation do + %Operation{ + tags: ["Statuses"], + summary: "View specific status", + description: "View information about a status", + operationId: "StatusController.show", + security: [%{"oAuth" => ["read:statuses"]}], + parameters: [id_param()], + responses: %{ + 200 => status_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Statuses"], + summary: "Delete status", + security: [%{"oAuth" => ["write:statuses"]}], + description: "Delete one of your own statuses", + operationId: "StatusController.delete", + parameters: [id_param()], + responses: %{ + 200 => empty_object_response(), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def reblog_operation do + %Operation{ + tags: ["Statuses"], + summary: "Boost", + security: [%{"oAuth" => ["write:statuses"]}], + description: "Share a status", + operationId: "StatusController.reblog", + parameters: [id_param()], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + visibility: %Schema{allOf: [VisibilityScope], default: "public"} + } + }), + responses: %{ + 200 => status_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def unreblog_operation do + %Operation{ + tags: ["Statuses"], + summary: "Undo boost", + security: [%{"oAuth" => ["write:statuses"]}], + description: "Undo a reshare of a status", + operationId: "StatusController.unreblog", + parameters: [id_param()], + responses: %{ + 200 => status_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def favourite_operation do + %Operation{ + tags: ["Statuses"], + summary: "Favourite", + security: [%{"oAuth" => ["write:favourites"]}], + description: "Add a status to your favourites list", + operationId: "StatusController.favourite", + parameters: [id_param()], + responses: %{ + 200 => status_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def unfavourite_operation do + %Operation{ + tags: ["Statuses"], + summary: "Undo favourite", + security: [%{"oAuth" => ["write:favourites"]}], + description: "Remove a status from your favourites list", + operationId: "StatusController.unfavourite", + parameters: [id_param()], + responses: %{ + 200 => status_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def pin_operation do + %Operation{ + tags: ["Statuses"], + summary: "Pin to profile", + security: [%{"oAuth" => ["write:accounts"]}], + description: "Feature one of your own public statuses at the top of your profile", + operationId: "StatusController.pin", + parameters: [id_param()], + responses: %{ + 200 => status_response(), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def unpin_operation do + %Operation{ + tags: ["Statuses"], + summary: "Unpin to profile", + security: [%{"oAuth" => ["write:accounts"]}], + description: "Unfeature a status from the top of your profile", + operationId: "StatusController.unpin", + parameters: [id_param()], + responses: %{ + 200 => status_response(), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def bookmark_operation do + %Operation{ + tags: ["Statuses"], + summary: "Bookmark", + security: [%{"oAuth" => ["write:bookmarks"]}], + description: "Privately bookmark a status", + operationId: "StatusController.bookmark", + parameters: [id_param()], + responses: %{ + 200 => status_response() + } + } + end + + def unbookmark_operation do + %Operation{ + tags: ["Statuses"], + summary: "Undo bookmark", + security: [%{"oAuth" => ["write:bookmarks"]}], + description: "Remove a status from your private bookmarks", + operationId: "StatusController.unbookmark", + parameters: [id_param()], + responses: %{ + 200 => status_response() + } + } + end + + def mute_conversation_operation do + %Operation{ + tags: ["Statuses"], + summary: "Mute conversation", + security: [%{"oAuth" => ["write:mutes"]}], + description: "Do not receive notifications for the thread that this status is part of.", + operationId: "StatusController.mute_conversation", + parameters: [id_param()], + responses: %{ + 200 => status_response(), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def unmute_conversation_operation do + %Operation{ + tags: ["Statuses"], + summary: "Unmute conversation", + security: [%{"oAuth" => ["write:mutes"]}], + description: + "Start receiving notifications again for the thread that this status is part of", + operationId: "StatusController.unmute_conversation", + parameters: [id_param()], + responses: %{ + 200 => status_response(), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def card_operation do + %Operation{ + tags: ["Statuses"], + deprecated: true, + summary: "Preview card", + description: "Deprecated in favor of card property inlined on Status entity", + operationId: "StatusController.card", + parameters: [id_param()], + security: [%{"oAuth" => ["read:statuses"]}], + responses: %{ + 200 => + Operation.response("Card", "application/json", %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} + } + }) + } + } + end + + def favourited_by_operation do + %Operation{ + tags: ["Statuses"], + summary: "Favourited by", + description: "View who favourited a given status", + operationId: "StatusController.favourited_by", + security: [%{"oAuth" => ["read:accounts"]}], + parameters: [id_param()], + responses: %{ + 200 => + Operation.response( + "Array of Accounts", + "application/json", + AccountOperation.array_of_accounts() + ), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def reblogged_by_operation do + %Operation{ + tags: ["Statuses"], + summary: "Boosted by", + description: "View who boosted a given status", + operationId: "StatusController.reblogged_by", + security: [%{"oAuth" => ["read:accounts"]}], + parameters: [id_param()], + responses: %{ + 200 => + Operation.response( + "Array of Accounts", + "application/json", + AccountOperation.array_of_accounts() + ), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def context_operation do + %Operation{ + tags: ["Statuses"], + summary: "Parent and child statuses", + description: "View statuses above and below this status in the thread", + operationId: "StatusController.context", + security: [%{"oAuth" => ["read:statuses"]}], + parameters: [id_param()], + responses: %{ + 200 => Operation.response("Context", "application/json", context()) + } + } + end + + def favourites_operation do + %Operation{ + tags: ["Statuses"], + summary: "Favourited statuses", + description: "Statuses the user has favourited", + operationId: "StatusController.favourites", + parameters: pagination_params(), + security: [%{"oAuth" => ["read:favourites"]}], + responses: %{ + 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses()) + } + } + end + + def bookmarks_operation do + %Operation{ + tags: ["Statuses"], + summary: "Bookmarked statuses", + description: "Statuses the user has bookmarked", + operationId: "StatusController.bookmarks", + parameters: pagination_params(), + security: [%{"oAuth" => ["read:bookmarks"]}], + responses: %{ + 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses()) + } + } + end + + def array_of_statuses do + %Schema{type: :array, items: Status, example: [Status.schema().example]} + end + + defp create_request do + %Schema{ + title: "StatusCreateRequest", + type: :object, + properties: %{ + status: %Schema{ + type: :string, + nullable: true, + description: + "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided." + }, + media_ids: %Schema{ + nullable: true, + type: :array, + items: %Schema{type: :string}, + description: "Array of Attachment ids to be attached as media." + }, + poll: %Schema{ + nullable: true, + type: :object, + required: [:options], + properties: %{ + options: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: "Array of possible answers. Must be provided with `poll[expires_in]`." + }, + expires_in: %Schema{ + type: :integer, + nullable: true, + description: + "Duration the poll should be open, in seconds. Must be provided with `poll[options]`" + }, + multiple: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Allow multiple choices?" + }, + hide_totals: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Hide vote counts until the poll ends?" + } + } + }, + in_reply_to_id: %Schema{ + nullable: true, + allOf: [FlakeID], + description: "ID of the status being replied to, if status is a reply" + }, + sensitive: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Mark status and attached media as sensitive?" + }, + spoiler_text: %Schema{ + type: :string, + nullable: true, + description: + "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field." + }, + scheduled_at: %Schema{ + type: :string, + format: :"date-time", + nullable: true, + description: + "ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future." + }, + language: %Schema{ + type: :string, + nullable: true, + description: "ISO 639 language code for this status." + }, + # Pleroma-specific properties: + preview: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: + "If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example" + }, + content_type: %Schema{ + type: :string, + nullable: true, + description: + "The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint." + }, + to: %Schema{ + type: :array, + nullable: true, + items: %Schema{type: :string}, + description: + "A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply" + }, + visibility: %Schema{ + nullable: true, + anyOf: [ + VisibilityScope, + %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"} + ], + description: + "Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`" + }, + expires_in: %Schema{ + nullable: true, + type: :integer, + description: + "The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour." + }, + in_reply_to_conversation_id: %Schema{ + nullable: true, + type: :string, + description: + "Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`." + } + }, + example: %{ + "status" => "What time is it?", + "sensitive" => "false", + "poll" => %{ + "options" => ["Cofe", "Adventure"], + "expires_in" => 420 + } + } + } + end + + defp id_param do + Operation.parameter(:id, :path, FlakeID, "Status ID", + example: "9umDrYheeY451cQnEe", + required: true + ) + end + + defp status_response do + Operation.response("Status", "application/json", Status) + end + + defp context do + %Schema{ + title: "StatusContext", + description: + "Represents the tree around a given status. Used for reconstructing threads of statuses.", + type: :object, + required: [:ancestors, :descendants], + properties: %{ + ancestors: array_of_statuses(), + descendants: array_of_statuses() + }, + example: %{ + "ancestors" => [Status.schema().example], + "descendants" => [Status.schema().example] + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex index 663b8fa11..c575a87e6 100644 --- a/lib/pleroma/web/api_spec/operations/subscription_operation.ex +++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike alias Pleroma.Web.ApiSpec.Schemas.PushSubscription def open_api_operation(action) do @@ -109,19 +110,38 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do required: [:endpoint, :keys] }, data: %Schema{ + nullable: true, type: :object, properties: %{ alerts: %Schema{ + nullable: true, type: :object, properties: %{ - follow: %Schema{type: :boolean, description: "Receive follow notifications?"}, + follow: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive follow notifications?" + }, favourite: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Receive favourite notifications?" }, - reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"}, - mention: %Schema{type: :boolean, description: "Receive mention notifications?"}, - poll: %Schema{type: :boolean, description: "Receive poll notifications?"} + reblog: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive reblog notifications?" + }, + mention: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive mention notifications?" + }, + poll: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive poll notifications?" + } } } } @@ -154,19 +174,38 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do type: :object, properties: %{ data: %Schema{ + nullable: true, type: :object, properties: %{ alerts: %Schema{ + nullable: true, type: :object, properties: %{ - follow: %Schema{type: :boolean, description: "Receive follow notifications?"}, + follow: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive follow notifications?" + }, favourite: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Receive favourite notifications?" }, - reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"}, - mention: %Schema{type: :boolean, description: "Receive mention notifications?"}, - poll: %Schema{type: :boolean, description: "Receive poll notifications?"} + reblog: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive reblog notifications?" + }, + mention: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive mention notifications?" + }, + poll: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive poll notifications?" + } } } } diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex new file mode 100644 index 000000000..8e19bace7 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex @@ -0,0 +1,191 @@ +# 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.TimelineOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + 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 + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def home_operation do + %Operation{ + tags: ["Timelines"], + summary: "Home timeline", + description: "View statuses from followed users", + security: [%{"oAuth" => ["read:statuses"]}], + parameters: [ + local_param(), + with_muted_param(), + exclude_visibilities_param(), + reply_visibility_param() | pagination_params() + ], + operationId: "TimelineController.home", + responses: %{ + 200 => Operation.response("Array of Status", "application/json", array_of_statuses()) + } + } + end + + def direct_operation do + %Operation{ + tags: ["Timelines"], + summary: "Direct timeline", + description: + "View statuses with a “direct” privacy, from your account or in your notifications", + deprecated: true, + parameters: [with_muted_param() | pagination_params()], + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "TimelineController.direct", + responses: %{ + 200 => Operation.response("Array of Status", "application/json", array_of_statuses()) + } + } + end + + def public_operation do + %Operation{ + tags: ["Timelines"], + summary: "Public timeline", + security: [%{"oAuth" => ["read:statuses"]}], + parameters: [ + local_param(), + only_media_param(), + with_muted_param(), + exclude_visibilities_param(), + reply_visibility_param() | pagination_params() + ], + operationId: "TimelineController.public", + responses: %{ + 200 => Operation.response("Array of Status", "application/json", array_of_statuses()), + 401 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def hashtag_operation do + %Operation{ + tags: ["Timelines"], + summary: "Hashtag timeline", + description: "View public statuses containing the given hashtag", + security: [%{"oAuth" => ["read:statuses"]}], + parameters: [ + Operation.parameter( + :tag, + :path, + %Schema{type: :string}, + "Content of a #hashtag, not including # symbol.", + required: true + ), + Operation.parameter( + :any, + :query, + %Schema{type: :array, items: %Schema{type: :string}}, + "Statuses that also includes any of these tags" + ), + Operation.parameter( + :all, + :query, + %Schema{type: :array, items: %Schema{type: :string}}, + "Statuses that also includes all of these tags" + ), + Operation.parameter( + :none, + :query, + %Schema{type: :array, items: %Schema{type: :string}}, + "Statuses that do not include these tags" + ), + local_param(), + only_media_param(), + with_muted_param(), + exclude_visibilities_param() | pagination_params() + ], + operationId: "TimelineController.hashtag", + responses: %{ + 200 => Operation.response("Array of Status", "application/json", array_of_statuses()) + } + } + end + + def list_operation do + %Operation{ + tags: ["Timelines"], + summary: "List timeline", + description: "View statuses in the given list timeline", + security: [%{"oAuth" => ["read:lists"]}], + parameters: [ + Operation.parameter( + :list_id, + :path, + %Schema{type: :string}, + "Local ID of the list in the database", + required: true + ), + with_muted_param(), + exclude_visibilities_param() | pagination_params() + ], + operationId: "TimelineController.list", + responses: %{ + 200 => Operation.response("Array of Status", "application/json", array_of_statuses()) + } + } + end + + defp array_of_statuses do + %Schema{ + title: "ArrayOfStatuses", + type: :array, + items: Status, + example: [Status.schema().example] + } + end + + defp local_param do + Operation.parameter( + :local, + :query, + %Schema{allOf: [BooleanLike], default: false}, + "Show only local statuses?" + ) + end + + defp with_muted_param do + Operation.parameter(:with_muted, :query, BooleanLike, "Includeactivities by muted users") + end + + defp exclude_visibilities_param do + Operation.parameter( + :exclude_visibilities, + :query, + %Schema{type: :array, items: VisibilityScope}, + "Exclude the statuses with the given visibilities" + ) + end + + defp reply_visibility_param do + Operation.parameter( + :reply_visibility, + :query, + %Schema{type: :string, enum: ["following", "self"]}, + "Filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you." + ) + end + + defp only_media_param do + Operation.parameter( + :only_media, + :query, + %Schema{allOf: [BooleanLike], default: false}, + "Show only statuses with media attached?" + ) + end +end diff --git a/lib/pleroma/web/api_spec/schemas/attachment.ex b/lib/pleroma/web/api_spec/schemas/attachment.ex index c146c416e..c6edf6d36 100644 --- a/lib/pleroma/web/api_spec/schemas/attachment.ex +++ b/lib/pleroma/web/api_spec/schemas/attachment.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do type: :object, requried: [:id, :url, :preview_url], properties: %{ - id: %Schema{type: :string}, + id: %Schema{type: :string, description: "The ID of the attachment in the database."}, url: %Schema{ type: :string, format: :uri, diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 2572c9641..8b87cb25b 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -19,60 +19,127 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do description: "Response schema for a status", type: :object, properties: %{ - account: Account, + account: %Schema{allOf: [Account], description: "The account that authored this status"}, application: %Schema{ + description: "The application used to post this status", type: :object, properties: %{ name: %Schema{type: :string}, website: %Schema{type: :string, nullable: true, format: :uri} } }, - bookmarked: %Schema{type: :boolean}, + bookmarked: %Schema{type: :boolean, description: "Have you bookmarked this status?"}, card: %Schema{ type: :object, nullable: true, + description: "Preview card for links included within status content", + required: [:url, :title, :description, :type], 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} + type: %Schema{ + type: :string, + enum: ["link", "photo", "video", "rich"], + description: "The type of the preview card" + }, + provider_name: %Schema{ + type: :string, + nullable: true, + description: "The provider of the original resource" + }, + provider_url: %Schema{ + type: :string, + format: :uri, + description: "A link to the provider of the original resource" + }, + url: %Schema{type: :string, format: :uri, description: "Location of linked resource"}, + image: %Schema{ + type: :string, + nullable: true, + format: :uri, + description: "Preview thumbnail" + }, + title: %Schema{type: :string, description: "Title of linked resource"}, + description: %Schema{type: :string, description: "Description of preview"} } }, - 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}, + content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"}, + created_at: %Schema{ + type: :string, + format: "date-time", + description: "The date when this status was created" + }, + emojis: %Schema{ + type: :array, + items: Emoji, + description: "Custom emoji to be used when rendering status content" + }, + favourited: %Schema{type: :boolean, description: "Have you favourited this status?"}, + favourites_count: %Schema{ + type: :integer, + description: "How many favourites this status has received" + }, 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}, + in_reply_to_account_id: %Schema{ + allOf: [FlakeID], + nullable: true, + description: "ID of the account being replied to" + }, + in_reply_to_id: %Schema{ + allOf: [FlakeID], + nullable: true, + description: "ID of the status being replied" + }, + language: %Schema{ + type: :string, + nullable: true, + description: "Primary language of this status" + }, media_attachments: %Schema{ type: :array, - items: Attachment + items: Attachment, + description: "Media that is attached to this status" }, mentions: %Schema{ type: :array, + description: "Mentions of users within the status content", items: %Schema{ type: :object, properties: %{ - id: %Schema{type: :string}, - acct: %Schema{type: :string}, - username: %Schema{type: :string}, - url: %Schema{type: :string, format: :uri} + id: %Schema{allOf: [FlakeID], description: "The account id of the mentioned user"}, + acct: %Schema{ + type: :string, + description: + "The webfinger acct: URI of the mentioned user. Equivalent to `username` for local users, or `username@domain` for remote users." + }, + username: %Schema{type: :string, description: "The username of the mentioned user"}, + url: %Schema{ + type: :string, + format: :uri, + description: "The location of the mentioned user's profile" + } } } }, - muted: %Schema{type: :boolean}, - pinned: %Schema{type: :boolean}, + muted: %Schema{ + type: :boolean, + description: "Have you muted notifications for this status's conversation?" + }, + pinned: %Schema{ + type: :boolean, + description: "Have you pinned this status? Only appears if the status is pinnable." + }, pleroma: %Schema{ type: :object, properties: %{ - content: %Schema{type: :object, additionalProperties: %Schema{type: :string}}, - conversation_id: %Schema{type: :integer}, + content: %Schema{ + type: :object, + additionalProperties: %Schema{type: :string}, + description: + "A map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`" + }, + conversation_id: %Schema{ + type: :integer, + description: "The ID of the AP context the status is associated with (if any)" + }, direct_conversation_id: %Schema{ type: :integer, nullable: true, @@ -81,6 +148,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do }, emoji_reactions: %Schema{ type: :array, + description: + "A list with emoji / reaction maps. Contains no information about the reacting users, for that use the /statuses/:id/reactions endpoint.", items: %Schema{ type: :object, properties: %{ @@ -90,27 +159,74 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do } } }, - 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} + expires_at: %Schema{ + type: :string, + format: "date-time", + nullable: true, + description: + "A datetime (ISO 8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire" + }, + in_reply_to_account_acct: %Schema{ + type: :string, + nullable: true, + description: "The `acct` property of User entity for replied user (if any)" + }, + local: %Schema{ + type: :boolean, + description: "`true` if the post was made on the local instance" + }, + spoiler_text: %Schema{ + type: :object, + additionalProperties: %Schema{type: :string}, + description: + "A map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`." + }, + thread_muted: %Schema{ + type: :boolean, + description: "`true` if the thread the post belongs to is muted" + } } }, - poll: %Schema{type: Poll, nullable: true}, + poll: %Schema{allOf: [Poll], nullable: true, description: "The poll attached to the status"}, reblog: %Schema{ allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}], - nullable: true + nullable: true, + description: "The status being reblogged" + }, + reblogged: %Schema{type: :boolean, description: "Have you boosted this status?"}, + reblogs_count: %Schema{ + type: :integer, + description: "How many boosts this status has received" + }, + replies_count: %Schema{ + type: :integer, + description: "How many replies this status has received" + }, + sensitive: %Schema{ + type: :boolean, + description: "Is this status marked as sensitive content?" + }, + spoiler_text: %Schema{ + type: :string, + description: + "Subject or summary line, below which status content is collapsed until expanded" }, - 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: Tag}, - uri: %Schema{type: :string, format: :uri}, - url: %Schema{type: :string, nullable: true, format: :uri}, - visibility: VisibilityScope + uri: %Schema{ + type: :string, + format: :uri, + description: "URI of the status used for federation" + }, + url: %Schema{ + type: :string, + nullable: true, + format: :uri, + description: "A link to the status's HTML representation" + }, + visibility: %Schema{ + allOf: [VisibilityScope], + description: "Visibility of this status" + } }, example: %{ "account" => %{ diff --git a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex index 8c81a4d73..831734e27 100644 --- a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex +++ b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex @@ -9,6 +9,6 @@ defmodule Pleroma.Web.ApiSpec.Schemas.VisibilityScope do title: "VisibilityScope", description: "Status visibility", type: :string, - enum: ["public", "unlisted", "private", "direct"] + enum: ["public", "unlisted", "private", "direct", "list"] }) end diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index a8f554aa3..200ca03dc 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -16,7 +16,8 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do def get_user(%Plug.Conn{} = conn) do with {:ok, {name, password}} <- fetch_credentials(conn), {_, %User{} = user} <- {:user, fetch_user(name)}, - {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do + {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)}, + {:ok, user} <- AuthenticationPlug.maybe_update_password(user, password) do {:ok, user} else {:error, _reason} = error -> error diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex index 98aca9a51..1794e407c 100644 --- a/lib/pleroma/web/auth/totp_authenticator.ex +++ b/lib/pleroma/web/auth/totp_authenticator.ex @@ -1,11 +1,11 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Auth.TOTPAuthenticator do - alias Comeonin.Pbkdf2 alias Pleroma.MFA alias Pleroma.MFA.TOTP + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.User @doc "Verify code or check backup code." @@ -31,7 +31,7 @@ defmodule Pleroma.Web.Auth.TOTPAuthenticator do code ) when is_list(codes) and is_binary(code) do - hash_code = Enum.find(codes, fn hash -> Pbkdf2.checkpw(code, hash) end) + hash_code = Enum.find(codes, fn hash -> AuthenticationPlug.checkpw(code, hash) end) if hash_code do MFA.invalidate_backup_code(user, hash_code) diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex index 38ec774f7..bce27897f 100644 --- a/lib/pleroma/web/chat_channel.ex +++ b/lib/pleroma/web/chat_channel.ex @@ -23,6 +23,7 @@ defmodule Pleroma.Web.ChatChannel do if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do author = User.get_cached_by_nickname(user_name) author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author) + message = ChatChannelState.add_message(%{text: text, author: author}) broadcast!(socket, "new_msg", message) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 244cf2be5..3f1a50b96 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -58,16 +58,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end defp put_params(draft, params) do - params = Map.put_new(params, "in_reply_to_status_id", params["in_reply_to_id"]) + params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id]) %__MODULE__{draft | params: params} end - defp status(%{params: %{"status" => status}} = draft) do + defp status(%{params: %{status: status}} = draft) do %__MODULE__{draft | status: String.trim(status)} end defp summary(%{params: params} = draft) do - %__MODULE__{draft | summary: Map.get(params, "spoiler_text", "")} + %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")} end defp full_payload(%{status: status, summary: summary} = draft) do @@ -84,20 +84,20 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | attachments: attachments} end - defp in_reply_to(%{params: %{"in_reply_to_status_id" => ""}} = draft), do: draft + 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 + 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 + 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"]) + in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id]) %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} end @@ -112,7 +112,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end defp expires_at(draft) do - case CommonAPI.check_expiry_date(draft.params["expires_in"]) do + case CommonAPI.check_expiry_date(draft.params[:expires_in]) do {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} {:error, message} -> add_error(draft, message) end @@ -144,7 +144,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do addressed_users = draft.mentions |> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end) - |> Utils.get_addressed_users(draft.params["to"]) + |> Utils.get_addressed_users(draft.params[:to]) {to, cc} = Utils.get_to_and_cc( @@ -164,7 +164,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end defp sensitive(draft) do - sensitive = draft.params["sensitive"] || Enum.member?(draft.tags, {"#nsfw", "nsfw"}) + sensitive = draft.params[:sensitive] || Enum.member?(draft.tags, {"#nsfw", "nsfw"}) %__MODULE__{draft | sensitive: sensitive} end @@ -191,7 +191,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end defp preview?(draft) do - preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) + preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params[:preview]) %__MODULE__{draft | preview?: preview?} end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index c538a634f..447dbe4e6 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -25,10 +25,21 @@ defmodule Pleroma.Web.CommonAPI do require Logger def unblock(blocker, blocked) do - with %Activity{} = block <- Utils.fetch_latest_block(blocker, blocked), + with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)}, {:ok, unblock_data, _} <- Builder.undo(blocker, block), {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do {:ok, unblock} + else + {:fetch_block, nil} -> + if User.blocks?(blocker, blocked) do + User.unblock(blocker, blocked) + {:ok, :no_activity} + else + {:error, :not_blocking} + end + + e -> + e end end @@ -83,33 +94,51 @@ defmodule Pleroma.Web.CommonAPI do end def delete(activity_id, user) do - with {_, %Activity{data: %{"object" => _}} = activity} <- - {:find_activity, Activity.get_by_id_with_object(activity_id)}, - %Object{} = object <- Object.normalize(activity), + with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <- + {:find_activity, Activity.get_by_id(activity_id)}, + {_, %Object{} = object, _} <- + {:find_object, Object.normalize(activity, false), activity}, true <- User.superuser?(user) || user.ap_id == object.data["actor"], {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do {:ok, delete} else - {:find_activity, _} -> {:error, :not_found} - _ -> {:error, dgettext("errors", "Could not delete")} + {:find_activity, _} -> + {:error, :not_found} + + {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} -> + # We have the create activity, but not the object, it was probably pruned. + # Insert a tombstone and try again + with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object), + {:ok, _tombstone} <- Object.create(tombstone_data) do + delete(activity_id, user) + else + _ -> + Logger.error( + "Could not insert tombstone for missing object on deletion. Object is #{object}." + ) + + {:error, dgettext("errors", "Could not delete")} + end + + _ -> + {:error, dgettext("errors", "Could not delete")} end end 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 + with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id) do + object = Object.normalize(activity) + announce_activity = Utils.get_existing_announce(user.ap_id, object) + public = public_announce?(object, params) + if announce_activity do {:ok, announce_activity, object} else ActivityPub.announce(user, object, nil, true, public) end else - {:find_activity, _} -> {:error, :not_found} - _ -> {:error, dgettext("errors", "Could not repeat")} + _ -> {:error, :not_found} end end @@ -267,7 +296,7 @@ defmodule Pleroma.Web.CommonAPI do end end - def public_announce?(_, %{"visibility" => visibility}) + def public_announce?(_, %{visibility: visibility}) when visibility in ~w{public unlisted private direct}, do: visibility in ~w(public unlisted) @@ -277,11 +306,11 @@ defmodule Pleroma.Web.CommonAPI do def get_visibility(_, _, %Participation{}), do: {"direct", "direct"} - def get_visibility(%{"visibility" => visibility}, in_reply_to, _) + def get_visibility(%{visibility: visibility}, in_reply_to, _) when visibility in ~w{public unlisted private direct}, do: {visibility, get_replied_to_visibility(in_reply_to)} - def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do + def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do visibility = {:list, String.to_integer(list_id)} {visibility, get_replied_to_visibility(in_reply_to)} end @@ -318,11 +347,14 @@ defmodule Pleroma.Web.CommonAPI do |> check_expiry_date() end - def listen(user, %{"title" => _} = data) do - with visibility <- data["visibility"] || "public", - {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil), + def listen(user, data) do + visibility = Map.get(data, :visibility, "public") + + with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil), listen_data <- - Map.take(data, ["album", "artist", "title", "length"]) + data + |> Map.take([:album, :artist, :title, :length]) + |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Audio") |> Map.put("to", to) |> Map.put("cc", cc) @@ -339,7 +371,7 @@ defmodule Pleroma.Web.CommonAPI do end end - def post(user, %{"status" => _} = data) do + def post(user, %{status: _} = data) do with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do draft.changes |> ActivityPub.create(draft.preview?) @@ -448,11 +480,11 @@ defmodule Pleroma.Web.CommonAPI do end end - defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do - toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)}) + defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do + toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)}) end - defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive}) + defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive}) when is_boolean(sensitive) do new_data = Map.put(object.data, "sensitive", sensitive) @@ -466,7 +498,7 @@ defmodule Pleroma.Web.CommonAPI do defp toggle_sensitive(activity, _), do: {:ok, activity} - defp set_visibility(activity, %{"visibility" => visibility}) do + defp set_visibility(activity, %{visibility: visibility}) do Utils.update_activity_visibility(activity, visibility) end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 793f2e7f8..e8deee223 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -22,11 +22,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do require Logger require Pleroma.Constants - def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do + def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do attachments_from_ids_descs(ids, desc) end - def attachments_from_ids(%{"media_ids" => ids} = _) do + def attachments_from_ids(%{media_ids: ids}) do attachments_from_ids_no_descs(ids) end @@ -37,11 +37,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do def attachments_from_ids_no_descs(ids) do Enum.map(ids, fn media_id -> case Repo.get(Object, media_id) do - %Object{data: data} = _ -> data + %Object{data: data} -> data _ -> nil end end) - |> Enum.filter(& &1) + |> Enum.reject(&is_nil/1) end def attachments_from_ids_descs([], _), do: [] @@ -51,14 +51,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do Enum.map(ids, fn media_id -> case Repo.get(Object, media_id) do - %Object{data: data} = _ -> + %Object{data: data} -> Map.put(data, "name", descs[media_id]) _ -> nil end end) - |> Enum.filter(& &1) + |> Enum.reject(&is_nil/1) end @spec get_to_and_cc( @@ -140,7 +140,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> make_poll_data() end - def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) + def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data) when is_list(options) do limits = Pleroma.Config.get([:instance, :poll_limits]) @@ -163,7 +163,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> DateTime.add(expires_in) |> DateTime.to_iso8601() - key = if truthy_param?(data["poll"]["multiple"]), do: "anyOf", else: "oneOf" + key = if truthy_param?(data.poll[:multiple]), do: "anyOf", else: "oneOf" poll = %{"type" => "Question", key => option_notes, "closed" => end_time} {:ok, {poll, emoji}} @@ -213,7 +213,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Map.get("attachment_links", Config.get([:instance, :attachment_links])) |> truthy_param?() - content_type = get_content_type(data["content_type"]) + content_type = get_content_type(data[:content_type]) options = if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index eb97ae975..5a1316a5f 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -5,8 +5,6 @@ defmodule Pleroma.Web.ControllerHelper do use Pleroma.Web, :controller - alias Pleroma.Config - # As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"] @@ -106,13 +104,16 @@ defmodule Pleroma.Web.ControllerHelper do def put_if_exist(map, key, value), do: Map.put(map, key, value) - @doc "Whether to skip rendering `[:account][:pleroma][:relationship]`for statuses/notifications" - def skip_relationships?(params) do - if Config.get([:extensions, :output_relationships_in_statuses_by_default]) do - false - else - # BREAKING: older PleromaFE versions do not send this param but _do_ expect relationships. - not truthy_param?(params["with_relationships"]) - end + @doc """ + Returns true if request specifies to include embedded relationships in account objects. + May only be used in selected account-related endpoints; has no effect for status- or + notification-related endpoints. + """ + # Intended for PleromaFE: https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838 + def embed_relationships?(params) do + # To do once OpenAPI transition mess is over: just `truthy_param?(params[:with_relationships])` + params + |> Map.get(:with_relationships, params["with_relationships"]) + |> truthy_param?() end end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index b9ed2d7b2..75512442d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -10,8 +10,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, - json_response: 3, - skip_relationships?: 1 + embed_relationships?: 1, + json_response: 3 ] alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug @@ -177,6 +177,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do ) |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store) |> add_if_present(params, :default_scope, :default_scope) + |> add_if_present(params["source"], "privacy", :default_scope) |> add_if_present(params, :actor_type, :actor_type) changeset = User.update_changeset(user, user_params) @@ -189,7 +190,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do - with true <- Map.has_key?(params, params_field), + with true <- is_map(params), + true <- Map.has_key?(params, params_field), {:ok, new_value} <- value_function.(Map.get(params, params_field)) do Map.put(map, map_field, new_value) else @@ -247,8 +249,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do |> render("index.json", activities: activities, for: reading_user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) else _e -> render_error(conn, :not_found, "Can't find user") @@ -271,7 +272,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do conn |> add_link_headers(followers) - |> render("index.json", for: for_user, users: followers, as: :user) + # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223 + |> render("index.json", + for: for_user, + users: followers, + as: :user, + embed_relationships: embed_relationships?(params) + ) end @doc "GET /api/v1/accounts/:id/following" @@ -290,7 +297,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do conn |> add_link_headers(followers) - |> render("index.json", for: for_user, users: followers, as: :user) + # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223 + |> render("index.json", + for: for_user, + users: followers, + as: :user, + embed_relationships: embed_relationships?(params) + ) end @doc "GET /api/v1/accounts/:id/lists" diff --git a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex index 0a257f604..8af557b61 100644 --- a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex @@ -20,6 +20,10 @@ defmodule Pleroma.Web.MastodonAPI.FallbackController do render_error(conn, :not_found, "Record not found") end + def call(conn, {:error, :forbidden}) do + render_error(conn, :forbidden, "Access denied") + end + def call(conn, {:error, error_message}) do conn |> put_status(:bad_request) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index e36751220..513de279f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -11,17 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do alias Pleroma.Web.ActivityPub.ActivityPub action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) - plug(OAuthScopesPlug, %{scopes: ["write:media"]}) + plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show) + plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation @doc "POST /api/v1/media" - def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do + def create(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do with {:ok, object} <- ActivityPub.upload( file, actor: User.ap_id(user), - description: Map.get(data, "description") + description: Map.get(data, :description) ) do attachment_data = Map.put(object.data, "id", object.id) @@ -29,11 +33,30 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do end end + def create(_conn, _data), do: {:error, :bad_request} + + @doc "POST /api/v2/media" + def create2(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do + with {:ok, object} <- + ActivityPub.upload( + file, + actor: User.ap_id(user), + description: Map.get(data, :description) + ) do + attachment_data = Map.put(object.data, "id", object.id) + + conn + |> put_status(202) + |> render("attachment.json", %{attachment: attachment_data}) + end + end + + def create2(_conn, _data), do: {:error, :bad_request} + @doc "PUT /api/v1/media/:id" - def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => description}) - when is_binary(description) do + def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do with %Object{} = object <- Object.get_by_id(id), - true <- Object.authorize_mutation(object, user), + :ok <- Object.authorize_access(object, user), {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do attachment_data = Map.put(data, "id", object.id) @@ -41,5 +64,17 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do end end - def update(_conn, _data), do: {:error, :bad_request} + def update(conn, data), do: show(conn, data) + + @doc "GET /api/v1/media/:id" + def show(%{assigns: %{user: user}} = conn, %{id: id}) do + with %Object{data: data, id: object_id} = object <- Object.get_by_id(id), + :ok <- Object.authorize_access(object, user) do + attachment_data = Map.put(data, "id", object_id) + + render(conn, "attachment.json", %{attachment: attachment_data}) + end + end + + def show(_conn, _data), do: {:error, :bad_request} end diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 596b85617..bcd12c73f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1] + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Notification alias Pleroma.Plugs.OAuthScopesPlug @@ -50,8 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do |> add_link_headers(notifications) |> render("index.json", notifications: notifications, - for: user, - skip_relationships: skip_relationships?(params) + for: user ) end diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 0e0d54ba4..77e2224e4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -5,14 +5,13 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [skip_relationships?: 1] - alias Pleroma.Activity alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web + alias Pleroma.Web.ControllerHelper alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView @@ -34,7 +33,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do conn |> put_view(AccountView) - |> render("index.json", users: accounts, for: user, as: :user) + |> render("index.json", + users: accounts, + for: user, + as: :user + ) end def search2(conn, params), do: do_search(:v2, conn, params) @@ -71,13 +74,13 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do defp search_options(params, user) do [ - skip_relationships: skip_relationships?(params), resolve: params[:resolve], following: params[:following], limit: params[:limit], offset: params[:offset], type: params[:type], author: get_author(params), + embed_relationships: ControllerHelper.embed_relationships?(params), for_user: user ] |> Enum.filter(&elem(&1, 1)) @@ -90,7 +93,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do users: accounts, for: options[:for_user], as: :user, - skip_relationships: false + embed_relationships: options[:embed_relationships] ) end @@ -100,8 +103,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do StatusView.render("index.json", activities: statuses, for: options[:for_user], - as: :activity, - skip_relationships: options[:skip_relationships] + as: :activity ) end diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 12e3ba15e..9dbf4f33c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, - only: [try_render: 3, add_link_headers: 2, skip_relationships?: 1] + only: [try_render: 3, add_link_headers: 2] require Ecto.Query @@ -24,6 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.ScheduledActivityView + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show]) @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} @@ -97,12 +98,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation + @doc """ GET `/api/v1/statuses?ids[]=1&ids[]=2` `ids` query param is required """ - def index(%{assigns: %{user: user}} = conn, %{"ids" => ids} = params) do + def index(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do limit = 100 activities = @@ -114,8 +117,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do render(conn, "index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end @@ -125,21 +127,29 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do Creates a scheduled status when `scheduled_at` param is present and it's far enough """ def create( - %{assigns: %{user: user}} = conn, - %{"status" => _, "scheduled_at" => scheduled_at} = params + %{ + assigns: %{user: user}, + body_params: %{status: _, scheduled_at: scheduled_at} = params + } = conn, + _ ) when not is_nil(scheduled_at) do - params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"]) + params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id]) + + attrs = %{ + params: Map.new(params, fn {key, value} -> {to_string(key), value} end), + scheduled_at: scheduled_at + } with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)}, - attrs <- %{"params" => params, "scheduled_at" => scheduled_at}, {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do conn |> put_view(ScheduledActivityView) |> render("show.json", scheduled_activity: scheduled_activity) else {:far_enough, _} -> - create(conn, Map.drop(params, ["scheduled_at"])) + params = Map.drop(params, [:scheduled_at]) + create(%Plug.Conn{conn | body_params: params}, %{}) error -> error @@ -151,8 +161,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do Creates a regular status """ - def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do - params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"]) + def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do + params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id]) with {:ok, activity} <- CommonAPI.post(user, params) do try_render(conn, "show.json", @@ -169,12 +179,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end end - def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do - create(conn, Map.put(params, "status", "")) + def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do + params = Map.put(params, :status, "") + create(%Plug.Conn{conn | body_params: params}, %{}) end @doc "GET /api/v1/statuses/:id" - def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do + def show(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), true <- Visibility.visible_for_user?(activity, user) do try_render(conn, "show.json", @@ -188,7 +199,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "DELETE /api/v1/statuses/:id" - def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do + def delete(%{assigns: %{user: user}} = conn, %{id: id}) do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do json(conn, %{}) else @@ -198,7 +209,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "POST /api/v1/statuses/:id/reblog" - def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do + def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params), %Activity{} = announce <- Activity.normalize(announce.data) do try_render(conn, "show.json", %{activity: announce, for: user, as: :activity}) @@ -206,7 +217,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "POST /api/v1/statuses/:id/unreblog" - def unreblog(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do + def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user), %Activity{} = activity <- Activity.get_by_id(activity_id) do try_render(conn, "show.json", %{activity: activity, for: user, as: :activity}) @@ -214,7 +225,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "POST /api/v1/statuses/:id/favourite" - def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do + def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do with {:ok, _fav} <- CommonAPI.favorite(user, activity_id), %Activity{} = activity <- Activity.get_by_id(activity_id) do try_render(conn, "show.json", activity: activity, for: user, as: :activity) @@ -222,7 +233,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "POST /api/v1/statuses/:id/unfavourite" - def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do + def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user), %Activity{} = activity <- Activity.get_by_id(activity_id) do try_render(conn, "show.json", activity: activity, for: user, as: :activity) @@ -230,21 +241,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "POST /api/v1/statuses/:id/pin" - def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do try_render(conn, "show.json", activity: activity, for: user, as: :activity) end end @doc "POST /api/v1/statuses/:id/unpin" - def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do try_render(conn, "show.json", activity: activity, for: user, as: :activity) end end @doc "POST /api/v1/statuses/:id/bookmark" - def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do + def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), @@ -254,7 +265,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "POST /api/v1/statuses/:id/unbookmark" - def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do + def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), @@ -264,7 +275,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "POST /api/v1/statuses/:id/mute" - def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do + def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id(id), {:ok, activity} <- CommonAPI.add_mute(user, activity) do try_render(conn, "show.json", activity: activity, for: user, as: :activity) @@ -272,7 +283,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "POST /api/v1/statuses/:id/unmute" - def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do + def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id(id), {:ok, activity} <- CommonAPI.remove_mute(user, activity) do try_render(conn, "show.json", activity: activity, for: user, as: :activity) @@ -281,7 +292,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do @doc "GET /api/v1/statuses/:id/card" @deprecated "https://github.com/tootsuite/mastodon/pull/11213" - def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do + def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do with %Activity{} = activity <- Activity.get_by_id(status_id), true <- Visibility.visible_for_user?(activity, user) do data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) @@ -292,7 +303,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "GET /api/v1/statuses/:id/favourited_by" - def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do + def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)}, %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do @@ -312,7 +323,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "GET /api/v1/statuses/:id/reblogged_by" - def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do + def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)}, %Object{data: %{"announcements" => announces, "id" => ap_id}} <- @@ -344,7 +355,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "GET /api/v1/statuses/:id/context" - def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do + def context(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id(id) do activities = ActivityPub.fetch_activities_for_context(activity.data["context"], %{ @@ -359,19 +370,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do @doc "GET /api/v1/favourites" def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do - activities = - ActivityPub.fetch_favourites( - user, - Map.take(params, Pleroma.Pagination.page_keys()) - ) + params = + params + |> Map.new(fn {key, value} -> {to_string(key), value} end) + |> Map.take(Pleroma.Pagination.page_keys()) + + activities = ActivityPub.fetch_favourites(user, params) conn |> add_link_headers(activities) |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end @@ -393,8 +404,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex index c93a43969..f91df9ab7 100644 --- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex @@ -5,11 +5,26 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug - require Logger - plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index) + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index) + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %OpenApiSpex.Operation{ + tags: ["Suggestions"], + summary: "Follow suggestions (Not implemented)", + operationId: "SuggestionController.index", + responses: %{ + 200 => Pleroma.Web.ApiSpec.Helpers.empty_array_response() + } + } + end @doc "GET /api/v1/suggestions" def index(conn, params), diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 2d67e19da..958567510 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, - only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1] + only: [add_link_headers: 2, add_link_headers: 3] alias Pleroma.Pagination alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug @@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + plug(Pleroma.Web.ApiSpec.CastAndValidate) 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: @@ -37,10 +38,13 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation + # GET /api/v1/timelines/home def home(%{assigns: %{user: user}} = conn, params) do params = params + |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", ["Create", "Announce"]) |> Map.put("blocking_user", user) |> Map.put("muting_user", user) @@ -59,8 +63,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end @@ -68,6 +71,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do def direct(%{assigns: %{user: user}} = conn, params) do params = params + |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Create") |> Map.put("blocking_user", user) |> Map.put("user", user) @@ -83,14 +87,15 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end # GET /api/v1/timelines/public def public(%{assigns: %{user: user}} = conn, params) do - local_only = truthy_param?(params["local"]) + params = Map.new(params, fn {key, value} -> {to_string(key), value} end) + + local_only = params["local"] cfg_key = if local_only do @@ -118,8 +123,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end end @@ -157,8 +161,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do # GET /api/v1/timelines/tag/:tag def hashtag(%{assigns: %{user: user}} = conn, params) do - local_only = truthy_param?(params["local"]) - + params = Map.new(params, fn {key, value} -> {to_string(key), value} end) + local_only = params["local"] activities = hashtag_fetching(params, user, local_only) conn @@ -166,16 +170,16 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end # GET /api/v1/timelines/list/:list_id - def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do + def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do params = params + |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Create") |> Map.put("blocking_user", user) |> Map.put("user", user) @@ -195,8 +199,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do render(conn, "index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) else _e -> render_error(conn, :forbidden, "Error.") diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 420bd586f..45fffaad2 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -15,13 +15,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do def render("index.json", %{users: users} = opts) do reading_user = opts[:for] - # Note: :skip_relationships option is currently intentionally not supported for accounts relationships_opt = cond do Map.has_key?(opts, :relationships) -> opts[:relationships] - is_nil(reading_user) -> + is_nil(reading_user) || !opts[:embed_relationships] -> UserRelationship.view_relationships_option(nil, []) true -> @@ -193,14 +192,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end) relationship = - if opts[:skip_relationships] do - %{} - else + if opts[:embed_relationships] do render("relationship.json", %{ user: opts[:for], target: user, relationships: opts[:relationships] }) + else + %{} end %{ @@ -261,7 +260,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp prepare_user_bio(%User{bio: ""}), do: "" defp prepare_user_bio(%User{bio: bio}) when is_binary(bio) do - bio |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags() + bio + |> String.replace(~r(<br */?>), "\n") + |> Pleroma.HTML.strip_tags() + |> HtmlEntities.decode() end defp prepare_user_bio(_), do: "" @@ -334,7 +336,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_role(data, _, _), do: data defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do - Kernel.put_in(data, [:pleroma, :notification_settings], user.notification_settings) + Kernel.put_in( + data, + [:pleroma, :notification_settings], + Map.from_struct(user.notification_settings) + ) end defp maybe_put_notification_settings(data, _, _), do: data diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index a329ffc28..8088306c3 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -33,6 +33,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit), background_upload_limit: Keyword.get(instance, :background_upload_limit), banner_upload_limit: Keyword.get(instance, :banner_upload_limit), + background_image: Keyword.get(instance, :background_image), pleroma: %{ metadata: %{ features: features(), diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 4da1ab67f..c46ddcf55 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -51,9 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do |> Enum.filter(& &1) |> Kernel.++(move_activities_targets) - UserRelationship.view_relationships_option(reading_user, actors, - source_mutes_only: opts[:skip_relationships] - ) + UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes) end opts = @@ -83,15 +81,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do mastodon_type = Activity.mastodon_notification_type(activity) - render_opts = %{ - relationships: opts[:relationships], - skip_relationships: opts[:skip_relationships] - } + # Note: :relationships contain user mutes (needed for :muted flag in :status) + status_render_opts = %{relationships: opts[:relationships]} with %{id: _} = account <- AccountView.render( "show.json", - Map.merge(render_opts, %{user: actor, for: reading_user}) + %{user: actor, for: reading_user} ) do response = %{ id: to_string(notification.id), @@ -105,21 +101,20 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do case mastodon_type do "mention" -> - put_status(response, activity, reading_user, render_opts) + put_status(response, activity, reading_user, status_render_opts) "favourite" -> - put_status(response, parent_activity_fn.(), reading_user, render_opts) + put_status(response, parent_activity_fn.(), reading_user, status_render_opts) "reblog" -> - put_status(response, parent_activity_fn.(), reading_user, render_opts) + put_status(response, parent_activity_fn.(), reading_user, status_render_opts) "move" -> - # Note: :skip_relationships option being applied to _account_ rendering (here) - put_target(response, activity, reading_user, render_opts) + put_target(response, activity, reading_user, %{}) "pleroma:emoji_reaction" -> response - |> put_status(parent_activity_fn.(), reading_user, render_opts) + |> put_status(parent_activity_fn.(), reading_user, status_render_opts) |> put_emoji(activity) type when type in ["follow", "follow_request"] -> diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 24167f66f..8e3715093 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -107,9 +107,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do |> Enum.map(&get_user(&1.data["actor"], false)) |> Enum.filter(& &1) - UserRelationship.view_relationships_option(reading_user, actors, - source_mutes_only: opts[:skip_relationships] - ) + UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes) end opts = @@ -162,9 +160,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do account: AccountView.render("show.json", %{ user: user, - for: opts[:for], - relationships: opts[:relationships], - skip_relationships: opts[:skip_relationships] + for: opts[:for] }), in_reply_to_id: nil, in_reply_to_account_id: nil, @@ -330,9 +326,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do account: AccountView.render("show.json", %{ user: user, - for: opts[:for], - relationships: opts[:relationships], - skip_relationships: opts[:skip_relationships] + for: opts[:for] }), in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), @@ -442,27 +436,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do } end - def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do - object = Object.normalize(activity) - - user = get_user(activity.data["actor"]) - created_at = Utils.to_masto_date(activity.data["published"]) - - %{ - id: activity.id, - account: AccountView.render("show.json", %{user: user, for: opts[:for]}), - created_at: created_at, - title: object.data["title"] |> HTML.strip_tags(), - artist: object.data["artist"] |> HTML.strip_tags(), - album: object.data["album"] |> HTML.strip_tags(), - length: object.data["length"] - } - end - - def render("listens.json", opts) do - safe_render_many(opts.activities, StatusView, "listen.json", opts) - end - def render("context.json", %{activity: activity, activities: activities, user: user}) do %{ancestors: ancestors, descendants: descendants} = activities diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index e2ffd02d0..94e4595d8 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -12,31 +12,19 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do @behaviour :cowboy_websocket + # Client ping period. + @tick :timer.seconds(30) # Cowboy timeout period. - @timeout :timer.seconds(30) + @timeout :timer.seconds(60) # Hibernate every X messages @hibernate_every 100 - @streams [ - "public", - "public:local", - "public:media", - "public:local:media", - "user", - "user:notification", - "direct", - "list", - "hashtag" - ] - @anonymous_streams ["public", "public:local", "hashtag"] - def init(%{qs: qs} = req, state) do - with params <- :cow_qs.parse_qs(qs), + with params <- Enum.into(:cow_qs.parse_qs(qs), %{}), sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil), - access_token <- List.keyfind(params, "access_token", 0), - {_, stream} <- List.keyfind(params, "stream", 0), - {:ok, user} <- allow_request(stream, [access_token, sec_websocket]), - topic when is_binary(topic) <- expand_topic(stream, params) do + access_token <- Map.get(params, "access_token"), + {:ok, user} <- authenticate_request(access_token, sec_websocket), + {:ok, topic} <- Streamer.get_topic(Map.get(params, "stream"), user, params) do req = if sec_websocket do :cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req) @@ -44,16 +32,17 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do req end - {:cowboy_websocket, req, %{user: user, topic: topic, count: 0}, %{idle_timeout: @timeout}} + {:cowboy_websocket, req, %{user: user, topic: topic, count: 0, timer: nil}, + %{idle_timeout: @timeout}} else - {:error, code} -> - Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}") - {:ok, req} = :cowboy_req.reply(code, req) + {:error, :bad_topic} -> + Logger.debug("#{__MODULE__} bad topic #{inspect(req)}") + {:ok, req} = :cowboy_req.reply(404, req) {:ok, req, state} - error -> - Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}") - {:ok, req} = :cowboy_req.reply(400, req) + {:error, :unauthorized} -> + Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}") + {:ok, req} = :cowboy_req.reply(401, req) {:ok, req, state} end end @@ -66,11 +55,18 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do ) Streamer.add_socket(state.topic, state.user) - {:ok, state} + {:ok, %{state | timer: timer()}} + end + + # Client's Pong frame. + def websocket_handle(:pong, state) do + if state.timer, do: Process.cancel_timer(state.timer) + {:ok, %{state | timer: timer()}} end # We never receive messages. - def websocket_handle(_frame, state) do + def websocket_handle(frame, state) do + Logger.error("#{__MODULE__} received frame: #{inspect(frame)}") {:ok, state} end @@ -94,6 +90,14 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do end end + # Ping tick. We don't re-queue a timer there, it is instead queued when :pong is received. + # As we hibernate there, reset the count to 0. + # If the client misses :pong, Cowboy will automatically timeout the connection after + # `@idle_timeout`. + def websocket_info(:tick, state) do + {:reply, :ping, %{state | timer: nil, count: 0}, :hibernate} + end + def terminate(reason, _req, state) do Logger.debug( "#{__MODULE__} terminating websocket connection for user #{ @@ -106,47 +110,24 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do end # Public streams without authentication. - defp allow_request(stream, [nil, nil]) when stream in @anonymous_streams do + defp authenticate_request(nil, nil) do {:ok, nil} end # Authenticated streams. - defp allow_request(stream, [access_token, sec_websocket]) when stream in @streams do - token = - with {"access_token", token} <- access_token do - token - else - _ -> sec_websocket - end + defp authenticate_request(access_token, sec_websocket) do + token = access_token || sec_websocket with true <- is_bitstring(token), %Token{user_id: user_id} <- Repo.get_by(Token, token: token), user = %User{} <- User.get_cached_by_id(user_id) do {:ok, user} else - _ -> {:error, 403} + _ -> {:error, :unauthorized} end end - # Not authenticated. - defp allow_request(stream, _) when stream in @streams, do: {:error, 403} - - # No matching stream. - defp allow_request(_, _), do: {:error, 404} - - defp expand_topic("hashtag", params) do - case List.keyfind(params, "tag", 0) do - {_, tag} -> "hashtag:#{tag}" - _ -> nil - end - end - - defp expand_topic("list", params) do - case List.keyfind(params, "list", 0) do - {_, list} -> "list:#{list}" - _ -> nil - end + defp timer do + Process.send_after(self(), :tick, @tick) end - - defp expand_topic(topic, _), do: topic end diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex new file mode 100644 index 000000000..c037ff13e --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidation.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.MediaProxy.Invalidation do + @moduledoc false + + @callback purge(list(String.t()), map()) :: {:ok, String.t()} | {:error, String.t()} + + alias Pleroma.Config + + @spec purge(list(String.t())) :: {:ok, String.t()} | {:error, String.t()} + def purge(urls) do + [:media_proxy, :invalidation, :enabled] + |> Config.get() + |> do_purge(urls) + end + + defp do_purge(true, urls) do + provider = Config.get([:media_proxy, :invalidation, :provider]) + options = Config.get(provider) + provider.purge(urls, options) + end + + defp do_purge(_, _), do: :ok +end diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex new file mode 100644 index 000000000..07248df6e --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidations/http.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.Web.MediaProxy.Invalidation.Http do + @moduledoc false + @behaviour Pleroma.Web.MediaProxy.Invalidation + + require Logger + + @impl Pleroma.Web.MediaProxy.Invalidation + def purge(urls, opts) do + method = Map.get(opts, :method, :purge) + headers = Map.get(opts, :headers, []) + options = Map.get(opts, :options, []) + + Logger.debug("Running cache purge: #{inspect(urls)}") + + Enum.each(urls, fn url -> + with {:error, error} <- do_purge(method, url, headers, options) do + Logger.error("Error while cache purge: url - #{url}, error: #{inspect(error)}") + end + end) + + {:ok, "success"} + end + + defp do_purge(method, url, headers, options) do + case Pleroma.HTTP.request(method, url, "", headers, options) do + {:ok, %{status: status} = env} when 400 <= status and status < 500 -> + {:error, env} + + {:error, error} = error -> + error + + _ -> + {:ok, "success"} + end + end +end diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex new file mode 100644 index 000000000..6be782132 --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidations/script.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.Invalidation.Script do + @moduledoc false + + @behaviour Pleroma.Web.MediaProxy.Invalidation + + require Logger + + @impl Pleroma.Web.MediaProxy.Invalidation + def purge(urls, %{script_path: script_path} = _options) do + args = + urls + |> List.wrap() + |> Enum.uniq() + |> Enum.join(" ") + + path = Path.expand(script_path) + + Logger.debug("Running cache purge: #{inspect(urls)}, #{path}") + + case do_purge(path, [args]) do + {result, exit_status} when exit_status > 0 -> + Logger.error("Error while cache purge: #{inspect(result)}") + {:error, inspect(result)} + + _ -> + {:ok, "success"} + end + end + + def purge(_, _), do: {:error, "not found script path"} + + defp do_purge(path, args) do + System.cmd(path, args) + rescue + error -> {inspect(error), 1} + end +end diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex index 1ed6ee521..6cbbe8fd8 100644 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do use Pleroma.Web, :controller - alias Comeonin.Pbkdf2 + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User @@ -28,7 +28,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do def check_password(conn, %{"user" => username, "pass" => password}) do with %User{password_hash: password_hash, deactivated: false} <- Repo.get_by(User, nickname: username, local: true), - true <- Pbkdf2.checkpw(password, password_hash) do + true <- AuthenticationPlug.checkpw(password, password_hash) do conn |> json(true) else diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex index e52cccd85..53e19f82e 100644 --- a/lib/pleroma/web/oauth/mfa_controller.ex +++ b/lib/pleroma/web/oauth/mfa_controller.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.OAuth.MFAController do diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex index e88e7066b..41d5578dc 100644 --- a/lib/pleroma/web/oauth/mfa_view.ex +++ b/lib/pleroma/web/oauth/mfa_view.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.OAuth.MFAView do diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex index 2c3bb9ded..e3aa4eb7e 100644 --- a/lib/pleroma/web/oauth/token/clean_worker.ex +++ b/lib/pleroma/web/oauth/token/clean_worker.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.OAuth.Token.CleanWorker do diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index be7477867..0a3f45620 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, - only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1] + only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] alias Ecto.Changeset alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug @@ -19,6 +19,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do require Pleroma.Constants plug( + OpenApiSpex.Plug.PutApiSpec, + [module: Pleroma.Web.ApiSpec] when action == :confirmation_resend + ) + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( :skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend ) @@ -49,9 +56,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation + @doc "POST /api/v1/pleroma/accounts/confirmation_resend" def confirmation_resend(conn, params) do - nickname_or_email = params["email"] || params["nickname"] + nickname_or_email = params[:email] || params[:nickname] with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email), {:ok, _} <- User.try_send_confirmation_email(user) do @@ -60,7 +69,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do end @doc "PATCH /api/v1/pleroma/accounts/update_avatar" - def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do {:ok, _user} = user |> Changeset.change(%{avatar: nil}) @@ -69,7 +78,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do json(conn, %{url: nil}) end - def update_avatar(%{assigns: %{user: user}} = conn, params) do + def update_avatar(%{assigns: %{user: user}, body_params: params} = conn, _params) do {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar) {:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache() %{"url" => [%{"href" => href} | _]} = data @@ -78,14 +87,14 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do end @doc "PATCH /api/v1/pleroma/accounts/update_banner" - def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do + def update_banner(%{assigns: %{user: user}, body_params: %{banner: ""}} = conn, _) do 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), + def update_banner(%{assigns: %{user: user}, body_params: params} = conn, _) do + with {:ok, object} <- ActivityPub.upload(%{img: params[:banner]}, type: :banner), {:ok, _user} <- User.update_banner(user, object.data) do %{"url" => [%{"href" => href} | _]} = object.data @@ -94,13 +103,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do end @doc "PATCH /api/v1/pleroma/accounts/update_background" - def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + def update_background(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do with {:ok, _user} <- User.update_background(user, %{}) do json(conn, %{url: nil}) end end - def update_background(%{assigns: %{user: user}} = conn, params) do + def update_background(%{assigns: %{user: user}, body_params: params} = conn, _) do with {:ok, object} <- ActivityPub.upload(params, type: :background), {:ok, _user} <- User.update_background(user, object.data) do %{"url" => [%{"href" => href} | _]} = object.data @@ -117,6 +126,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do params = params + |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Create") |> Map.put("favorited_by", user.ap_id) |> Map.put("blocking_user", for_user) @@ -139,8 +149,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do |> render("index.json", activities: activities, for: for_user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index d4e0d8b7c..df6c50ca5 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -9,16 +9,19 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation + @doc "GET /api/v1/pleroma/mascot" def show(%{assigns: %{user: user}} = conn, _params) do json(conn, User.get_mascot(user)) end @doc "PUT /api/v1/pleroma/mascot" - def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do + def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), # Reject if not an image %{type: "image"} = attachment <- render_attachment(object) do 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 8bc77b75e..e834133b2 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1] + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Activity alias Pleroma.Conversation.Participation @@ -69,7 +69,12 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do %{ name: emoji, count: length(users), - accounts: AccountView.render("index.json", %{users: users, for: user, as: :user}), + accounts: + AccountView.render("index.json", %{ + users: users, + for: user, + as: :user + }), me: !!(user && user.ap_id in user_ap_ids) } end @@ -145,8 +150,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) else _error -> @@ -201,7 +205,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do end end - def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do + def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do with notifications <- Notification.set_read_up_to(user, max_id) do notifications = Enum.take(notifications, 80) @@ -209,8 +213,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do |> put_view(NotificationView) |> render("index.json", notifications: notifications, - for: user, - skip_relationships: skip_relationships?(params) + for: user ) end end diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index 22da6c0ad..8665ca56c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -5,34 +5,27 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2] + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.StatusView + + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug( OAuthScopesPlug, - %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :user_scrobbles + %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :index ) - plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles) + plug(OAuthScopesPlug, %{scopes: ["write"]} when action == :create) - def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do - params = - if !params["length"] do - params - else - params - |> Map.put("length", fetch_integer_param(params, "length")) - end + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation + def create(%{assigns: %{user: user}, body_params: params} = conn, _) do with {:ok, activity} <- CommonAPI.listen(user, params) do - conn - |> put_view(StatusView) - |> render("listen.json", %{activity: activity, for: user}) + render(conn, "show.json", activity: activity, for: user) else {:error, message} -> conn @@ -41,16 +34,18 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do end end - def user_scrobbles(%{assigns: %{user: reading_user}} = conn, params) do - with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do - params = Map.put(params, "type", ["Listen"]) + def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do + with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do + params = + params + |> Map.new(fn {key, value} -> {to_string(key), value} end) + |> Map.put("type", ["Listen"]) activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params) conn |> add_link_headers(activities) - |> put_view(StatusView) - |> render("listens.json", %{ + |> render("index.json", %{ activities: activities, for: reading_user, as: :activity diff --git a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex index eb9989cdf..b86791d09 100644 --- a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex new file mode 100644 index 000000000..bbff93abe --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ScrobbleView do + use Pleroma.Web, :view + + require Pleroma.Constants + + alias Pleroma.Activity + alias Pleroma.HTML + alias Pleroma.Object + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + + def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do + object = Object.normalize(activity) + + user = StatusView.get_user(activity.data["actor"]) + created_at = Utils.to_masto_date(activity.data["published"]) + + %{ + id: activity.id, + account: AccountView.render("show.json", %{user: user, for: opts[:for]}), + created_at: created_at, + title: object.data["title"] |> HTML.strip_tags(), + artist: object.data["artist"] |> HTML.strip_tags(), + album: object.data["album"] |> HTML.strip_tags(), + length: object.data["length"] + } + end + + def render("index.json", opts) do + safe_render_many(opts.activities, __MODULE__, "show.json", opts) + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 7a171f9fb..369c54cf4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -325,7 +325,7 @@ defmodule Pleroma.Web.Router do get("/mascot", MascotController, :show) put("/mascot", MascotController, :update) - post("/scrobble", ScrobbleController, :new_scrobble) + post("/scrobble", ScrobbleController, :create) end scope [] do @@ -345,7 +345,7 @@ defmodule Pleroma.Web.Router do scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do pipe_through(:api) - get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles) + get("/accounts/:id/scrobbles", ScrobbleController, :index) end scope "/api/v1", Pleroma.Web.MastodonAPI do @@ -403,6 +403,7 @@ defmodule Pleroma.Web.Router do post("/markers", MarkerController, :upsert) post("/media", MediaController, :create) + get("/media/:id", MediaController, :show) put("/media/:id", MediaController, :update) get("/notifications", NotificationController, :index) @@ -497,6 +498,8 @@ defmodule Pleroma.Web.Router do scope "/api/v2", Pleroma.Web.MastodonAPI do pipe_through(:api) get("/search", SearchController, :search2) + + post("/media", MediaController, :create2) end scope "/api", Pleroma.Web do diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index 5ad4aa936..49a400df7 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -21,12 +21,68 @@ defmodule Pleroma.Web.Streamer do def registry, do: @registry - def add_socket(topic, %User{} = user) do - if should_env_send?(), do: Registry.register(@registry, user_topic(topic, user), true) + @public_streams ["public", "public:local", "public:media", "public:local:media"] + @user_streams ["user", "user:notification", "direct"] + + @doc "Expands and authorizes a stream, and registers the process for streaming." + @spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) :: + {:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized} + def get_topic_and_add_socket(stream, user, params \\ %{}) do + case get_topic(stream, user, params) do + {:ok, topic} -> add_socket(topic, user) + error -> error + end + end + + @doc "Expand and authorizes a stream" + @spec get_topic(stream :: String.t(), User.t() | nil, Map.t()) :: + {:ok, topic :: String.t()} | {:error, :bad_topic} + def get_topic(stream, user, params \\ %{}) + + # Allow all public steams. + def get_topic(stream, _, _) when stream in @public_streams do + {:ok, stream} end - def add_socket(topic, _) do - if should_env_send?(), do: Registry.register(@registry, topic, false) + # Allow all hashtags streams. + def get_topic("hashtag", _, %{"tag" => tag}) do + {:ok, "hashtag:" <> tag} + end + + # Expand user streams. + def get_topic(stream, %User{} = user, _) when stream in @user_streams do + {:ok, stream <> ":" <> to_string(user.id)} + end + + def get_topic(stream, _, _) when stream in @user_streams do + {:error, :unauthorized} + end + + # List streams. + def get_topic("list", %User{} = user, %{"list" => id}) do + if Pleroma.List.get(id, user) do + {:ok, "list:" <> to_string(id)} + else + {:error, :bad_topic} + end + end + + def get_topic("list", _, _) do + {:error, :unauthorized} + end + + def get_topic(_, _, _) do + {:error, :bad_topic} + end + + @doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic." + def add_socket(topic, user) do + if should_env_send?() do + auth? = if user, do: true + Registry.register(@registry, topic, auth?) + end + + {:ok, topic} end def remove_socket(topic) do @@ -231,13 +287,4 @@ defmodule Pleroma.Web.Streamer do true -> def should_env_send?, do: true end - - defp user_topic(topic, user) - when topic in ~w[user user:notification direct] do - "#{topic}:#{user.id}" - end - - defp user_topic(topic, _) do - topic - end end diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex index 3c5820a86..49352db2a 100644 --- a/lib/pleroma/workers/attachments_cleanup_worker.ex +++ b/lib/pleroma/workers/attachments_cleanup_worker.ex @@ -27,8 +27,20 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + prefix = + case Pleroma.Config.get([Pleroma.Upload, :base_url]) do + nil -> "media" + _ -> "" + end + + base_url = + String.trim_trailing( + Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()), + "/" + ) + # find all objects for copies of the attachments, name and actor doesn't matter here - delete_ids = + object_ids_and_hrefs = from(o in Object, where: fragment( @@ -67,29 +79,28 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do |> Enum.map(fn {href, %{id: id, count: count}} -> # only delete files that have single instance with 1 <- count do - prefix = - case Pleroma.Config.get([Pleroma.Upload, :base_url]) do - nil -> "media" - _ -> "" - end - - base_url = - String.trim_trailing( - Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()), - "/" - ) - - file_path = String.trim_leading(href, "#{base_url}/#{prefix}") + href + |> String.trim_leading("#{base_url}/#{prefix}") + |> uploader.delete_file() - uploader.delete_file(file_path) + {id, href} + else + _ -> {id, nil} end - - id end) - from(o in Object, where: o.id in ^delete_ids) + object_ids = Enum.map(object_ids_and_hrefs, fn {id, _} -> id end) + + from(o in Object, where: o.id in ^object_ids) |> Repo.delete_all() + + object_ids_and_hrefs + |> Enum.filter(fn {_, href} -> not is_nil(href) end) + |> Enum.map(&elem(&1, 1)) + |> Pleroma.Web.MediaProxy.Invalidation.purge() + + {:ok, :success} end - def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: :ok + def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip} end diff --git a/lib/pleroma/workers/scheduled_activity_worker.ex b/lib/pleroma/workers/scheduled_activity_worker.ex index 8905f4ad0..97d1efbfb 100644 --- a/lib/pleroma/workers/scheduled_activity_worker.ex +++ b/lib/pleroma/workers/scheduled_activity_worker.ex @@ -30,6 +30,8 @@ defmodule Pleroma.Workers.ScheduledActivityWorker do end defp post_activity(%ScheduledActivity{user_id: user_id, params: params} = scheduled_activity) do + params = Map.new(params, fn {key, value} -> {String.to_existing_atom(key), value} end) + with {:delete, {:ok, _}} <- {:delete, ScheduledActivity.delete(scheduled_activity)}, {:user, %User{} = user} <- {:user, User.get_cached_by_id(user_id)}, {:post, {:ok, _}} <- {:post, CommonAPI.post(user, params)} do |