diff options
| author | Ivan Tashkinov <ivantashkinov@gmail.com> | 2020-05-13 12:42:36 +0300 | 
|---|---|---|
| committer | Ivan Tashkinov <ivantashkinov@gmail.com> | 2020-05-13 12:42:36 +0300 | 
| commit | fd2fb2bb2e2292997fbe6f70c4d12b50c56cfab9 (patch) | |
| tree | 12f2ac82ed24394fb52b5d0664583dc668814421 /lib | |
| parent | bfb48e3db6009c31e52cfe5ac4828a6143d7e549 (diff) | |
| parent | 156c8a508846bd6d4e55f666c4ecc6f0129ac5fc (diff) | |
| download | pleroma-fd2fb2bb2e2292997fbe6f70c4d12b50c56cfab9.tar.gz pleroma-fd2fb2bb2e2292997fbe6f70c4d12b50c56cfab9.zip | |
Merge remote-tracking branch 'remotes/origin/develop' into restricted-relations-embedding
# Conflicts:
#	lib/pleroma/web/mastodon_api/controllers/status_controller.ex
#	lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
#	test/web/mastodon_api/controllers/timeline_controller_test.exs
#	test/web/mastodon_api/views/status_view_test.exs
Diffstat (limited to 'lib')
29 files changed, 1154 insertions, 220 deletions
| 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..d4494b003 100644 --- a/lib/pleroma/bbs/authenticator.ex +++ b/lib/pleroma/bbs/authenticator.ex @@ -4,7 +4,6 @@  defmodule Pleroma.BBS.Authenticator do    use Sshd.PasswordAuthenticator -  alias Comeonin.Pbkdf2    alias Pleroma.User    def authenticate(username, password) do @@ -12,7 +11,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) +      Pbkdf2.verify_pass(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/mfa.ex b/lib/pleroma/mfa.ex index d353a4dad..2b77f5426 100644 --- a/lib/pleroma/mfa.ex +++ b/lib/pleroma/mfa.ex @@ -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/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex index 0061c69dc..ae4a235bd 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 @@ -18,7 +17,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do    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 @@ -37,7 +36,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do          } = conn,          _        ) do -    if Pbkdf2.checkpw(password, password_hash) do +    if Pbkdf2.verify_pass(password, password_hash) do        conn        |> assign(:user, auth_user)        |> OAuthScopesPlug.skip_plug() @@ -47,7 +46,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/user.ex b/lib/pleroma/user.ex index 2a6a23fec..cba391072 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 @@ -1554,10 +1553,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) @@ -1913,7 +1925,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/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/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 616ca52bd..80c4df0e2 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -845,15 +845,20 @@ 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 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..a6bb87560 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -0,0 +1,499 @@ +# 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: [ +        Operation.parameter(:with_relationships, :query, BooleanLike, "Include relationships") +        | pagination_params() +      ], +      security: [%{"oAuth" => ["read:bookmarks"]}], +      responses: %{ +        200 => Operation.response("Array of Statuses", "application/json", array_of_statuses()) +      } +    } +  end + +  defp 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, +          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{ +          type: :array, +          items: %Schema{type: :string}, +          description: "Array of Attachment ids to be attached as media." +        }, +        poll: %Schema{ +          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, +              description: +                "Duration the poll should be open, in seconds. Must be provided with `poll[options]`" +            }, +            multiple: %Schema{type: :boolean, description: "Allow multiple choices?"}, +            hide_totals: %Schema{ +              type: :boolean, +              description: "Hide vote counts until the poll ends?" +            } +          } +        }, +        in_reply_to_id: %Schema{ +          allOf: [FlakeID], +          description: "ID of the status being replied to, if status is a reply" +        }, +        sensitive: %Schema{ +          type: :boolean, +          description: "Mark status and attached media as sensitive?" +        }, +        spoiler_text: %Schema{ +          type: :string, +          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, description: "ISO 639 language code for this status."}, +        # Pleroma-specific properties: +        preview: %Schema{ +          type: :boolean, +          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, +          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, +          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{ +          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{ +          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{ +          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/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex new file mode 100644 index 000000000..1b89035d4 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex @@ -0,0 +1,199 @@ +# 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(), +        with_relationships_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: 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(), +        with_relationships_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(), +        with_relationships_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(), +        with_relationships_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 with_relationships_param do +    Operation.parameter(:with_relationships, :query, BooleanLike, "Include relationships") +  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/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/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex index 98aca9a51..04e489c83 100644 --- a/lib/pleroma/web/auth/totp_authenticator.ex +++ b/lib/pleroma/web/auth/totp_authenticator.ex @@ -3,7 +3,6 @@  # 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.User @@ -31,7 +30,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 -> Pbkdf2.verify_pass(code, hash) end)      if hash_code do        MFA.invalidate_backup_code(user, hash_code) 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..601caeb46 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -83,33 +83,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 +285,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 +295,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 @@ -339,7 +357,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 +466,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 +484,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/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 2b2e4a896..9dbf4f33c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -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 = @@ -124,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 @@ -150,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", @@ -168,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", @@ -187,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 @@ -197,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}) @@ -205,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}) @@ -213,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) @@ -221,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) @@ -229,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), @@ -253,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), @@ -263,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) @@ -271,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) @@ -280,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) @@ -291,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 @@ -311,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}} <- @@ -343,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"], %{ @@ -358,11 +370,12 @@ 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) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 61cc6ab49..e2922d830 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -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) @@ -67,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) @@ -88,7 +93,9 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do    # 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 @@ -154,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 @@ -168,10 +175,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do    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) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 6304d77ca..45fffaad2 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -260,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: "" @@ -333,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/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/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex index 1ed6ee521..0814b3bc3 100644 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex @@ -5,7 +5,6 @@  defmodule Pleroma.Web.MongooseIM.MongooseIMController do    use Pleroma.Web, :controller -  alias Comeonin.Pbkdf2    alias Pleroma.Plugs.RateLimiter    alias Pleroma.Repo    alias Pleroma.User @@ -28,7 +27,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 <- Pbkdf2.verify_pass(password, password_hash) do        conn        |> json(true)      else 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/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 | 
