diff options
Diffstat (limited to 'lib')
27 files changed, 697 insertions, 363 deletions
| diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex index 443927392..4d82860f5 100644 --- a/lib/pleroma/marker.ex +++ b/lib/pleroma/marker.ex @@ -9,24 +9,34 @@ defmodule Pleroma.Marker do    import Ecto.Query    alias Ecto.Multi +  alias Pleroma.Notification    alias Pleroma.Repo    alias Pleroma.User +  alias __MODULE__    @timelines ["notifications"] +  @type t :: %__MODULE__{}    schema "markers" do      field(:last_read_id, :string, default: "")      field(:timeline, :string, default: "")      field(:lock_version, :integer, default: 0) +    field(:unread_count, :integer, default: 0, virtual: true)      belongs_to(:user, User, type: FlakeId.Ecto.CompatType)      timestamps()    end +  @doc "Gets markers by user and timeline." +  @spec get_markers(User.t(), list(String)) :: list(t())    def get_markers(user, timelines \\ []) do -    Repo.all(get_query(user, timelines)) +    user +    |> get_query(timelines) +    |> unread_count_query() +    |> Repo.all()    end +  @spec upsert(User.t(), map()) :: {:ok | :error, any()}    def upsert(%User{} = user, attrs) do      attrs      |> Map.take(@timelines) @@ -45,6 +55,27 @@ defmodule Pleroma.Marker do      |> Repo.transaction()    end +  @spec multi_set_last_read_id(Multi.t(), User.t(), String.t()) :: Multi.t() +  def multi_set_last_read_id(multi, %User{} = user, "notifications") do +    multi +    |> Multi.run(:counters, fn _repo, _changes -> +      {:ok, %{last_read_id: Repo.one(Notification.last_read_query(user))}} +    end) +    |> Multi.insert( +      :marker, +      fn %{counters: attrs} -> +        %Marker{timeline: "notifications", user_id: user.id} +        |> struct(attrs) +        |> Ecto.Changeset.change() +      end, +      returning: true, +      on_conflict: {:replace, [:last_read_id]}, +      conflict_target: [:user_id, :timeline] +    ) +  end + +  def multi_set_last_read_id(multi, _, _), do: multi +    defp get_marker(user, timeline) do      case Repo.find_resource(get_query(user, timeline)) do        {:ok, marker} -> %__MODULE__{marker | user: user} @@ -71,4 +102,16 @@ defmodule Pleroma.Marker do      |> by_user_id(user.id)      |> by_timeline(timelines)    end + +  defp unread_count_query(query) do +    from( +      q in query, +      left_join: n in "notifications", +      on: n.user_id == q.user_id and n.seen == false, +      group_by: [:id], +      select_merge: %{ +        unread_count: fragment("count(?)", n.id) +      } +    ) +  end  end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index af49fd713..8aa9ed2d4 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -5,8 +5,10 @@  defmodule Pleroma.Notification do    use Ecto.Schema +  alias Ecto.Multi    alias Pleroma.Activity    alias Pleroma.FollowingRelationship +  alias Pleroma.Marker    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Pagination @@ -34,11 +36,30 @@ defmodule Pleroma.Notification do      timestamps()    end +  @spec unread_notifications_count(User.t()) :: integer() +  def unread_notifications_count(%User{id: user_id}) do +    from(q in __MODULE__, +      where: q.user_id == ^user_id and q.seen == false +    ) +    |> Repo.aggregate(:count, :id) +  end +    def changeset(%Notification{} = notification, attrs) do      notification      |> cast(attrs, [:seen])    end +  @spec last_read_query(User.t()) :: Ecto.Queryable.t() +  def last_read_query(user) do +    from(q in Pleroma.Notification, +      where: q.user_id == ^user.id, +      where: q.seen == true, +      select: type(q.id, :string), +      limit: 1, +      order_by: [desc: :id] +    ) +  end +    defp for_user_query_ap_id_opts(user, opts) do      ap_id_relationships =        [:block] ++ @@ -185,25 +206,23 @@ defmodule Pleroma.Notification do      |> Repo.all()    end -  def set_read_up_to(%{id: user_id} = _user, id) do +  def set_read_up_to(%{id: user_id} = user, id) do      query =        from(          n in Notification,          where: n.user_id == ^user_id,          where: n.id <= ^id,          where: n.seen == false, -        update: [ -          set: [ -            seen: true, -            updated_at: ^NaiveDateTime.utc_now() -          ] -        ],          # Ideally we would preload object and activities here          # but Ecto does not support preloads in update_all          select: n.id        ) -    {_, notification_ids} = Repo.update_all(query, []) +    {:ok, %{ids: {_, notification_ids}}} = +      Multi.new() +      |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()]) +      |> Marker.multi_set_last_read_id(user, "notifications") +      |> Repo.transaction()      Notification      |> where([n], n.id in ^notification_ids) @@ -220,11 +239,18 @@ defmodule Pleroma.Notification do      |> Repo.all()    end +  @spec read_one(User.t(), String.t()) :: +          {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil    def read_one(%User{} = user, notification_id) do      with {:ok, %Notification{} = notification} <- get(user, notification_id) do -      notification -      |> changeset(%{seen: true}) -      |> Repo.update() +      Multi.new() +      |> Multi.update(:update, changeset(notification, %{seen: true})) +      |> Marker.multi_set_last_read_id(user, "notifications") +      |> Repo.transaction() +      |> case do +        {:ok, %{update: notification}} -> {:ok, notification} +        {:error, :update, changeset, _} -> {:error, changeset} +      end      end    end @@ -316,8 +342,11 @@ defmodule Pleroma.Notification do    # TODO move to sql, too.    def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do      unless skip?(activity, user) do -      notification = %Notification{user_id: user.id, activity: activity} -      {:ok, notification} = Repo.insert(notification) +      {:ok, %{notification: notification}} = +        Multi.new() +        |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity}) +        |> Marker.multi_set_last_read_id(user, "notifications") +        |> Repo.transaction()        if do_send do          Streamer.stream(["user", "user:notification"], notification) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index a6f51f0be..2a6a23fec 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1557,23 +1557,13 @@ defmodule Pleroma.User do    defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do      {:ok, delete_data, _} = Builder.delete(user, object) -    Pipeline.common_pipeline(delete_data, local: true) +    Pipeline.common_pipeline(delete_data, local: user.local)    end -  defp delete_activity(%{data: %{"type" => "Like"}} = activity, _user) do -    object = Object.normalize(activity) - -    activity.actor -    |> get_cached_by_ap_id() -    |> ActivityPub.unlike(object) -  end - -  defp delete_activity(%{data: %{"type" => "Announce"}} = activity, _user) do -    object = Object.normalize(activity) - -    activity.actor -    |> get_cached_by_ap_id() -    |> ActivityPub.unannounce(object) +  defp delete_activity(%{data: %{"type" => type}} = activity, user) +       when type in ["Like", "Announce"] do +    {:ok, undo, _} = Builder.undo(user, activity) +    Pipeline.common_pipeline(undo, local: user.local)    end    defp delete_activity(_activity, _user), do: "Doing nothing" diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 8baaf97ac..4955243ab 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -356,81 +356,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  @spec react_with_emoji(User.t(), Object.t(), String.t(), keyword()) :: -          {:ok, Activity.t(), Object.t()} | {:error, any()} -  def react_with_emoji(user, object, emoji, options \\ []) do -    with {:ok, result} <- -           Repo.transaction(fn -> do_react_with_emoji(user, object, emoji, options) end) do -      result -    end -  end - -  defp do_react_with_emoji(user, object, emoji, options) do -    with local <- Keyword.get(options, :local, true), -         activity_id <- Keyword.get(options, :activity_id, nil), -         true <- Pleroma.Emoji.is_unicode_emoji?(emoji), -         reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id), -         {:ok, activity} <- insert(reaction_data, local), -         {:ok, object} <- add_emoji_reaction_to_object(activity, object), -         _ <- notify_and_stream(activity), -         :ok <- maybe_federate(activity) do -      {:ok, activity, object} -    else -      false -> {:error, false} -      {:error, error} -> Repo.rollback(error) -    end -  end - -  @spec unreact_with_emoji(User.t(), String.t(), keyword()) :: -          {:ok, Activity.t(), Object.t()} | {:error, any()} -  def unreact_with_emoji(user, reaction_id, options \\ []) do -    with {:ok, result} <- -           Repo.transaction(fn -> do_unreact_with_emoji(user, reaction_id, options) end) do -      result -    end -  end - -  defp do_unreact_with_emoji(user, reaction_id, options) do -    with local <- Keyword.get(options, :local, true), -         activity_id <- Keyword.get(options, :activity_id, nil), -         user_ap_id <- user.ap_id, -         %Activity{actor: ^user_ap_id} = reaction_activity <- Activity.get_by_ap_id(reaction_id), -         object <- Object.normalize(reaction_activity), -         unreact_data <- make_undo_data(user, reaction_activity, activity_id), -         {:ok, activity} <- insert(unreact_data, local), -         {:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object), -         _ <- notify_and_stream(activity), -         :ok <- maybe_federate(activity) do -      {:ok, activity, object} -    else -      {:error, error} -> Repo.rollback(error) -    end -  end - -  @spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) :: -          {:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()} -  def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do -    with {:ok, result} <- -           Repo.transaction(fn -> do_unlike(actor, object, activity_id, local) end) do -      result -    end -  end - -  defp do_unlike(actor, object, activity_id, local) do -    with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object), -         unlike_data <- make_unlike_data(actor, like_activity, activity_id), -         {:ok, unlike_activity} <- insert(unlike_data, local), -         {:ok, _activity} <- Repo.delete(like_activity), -         {:ok, object} <- remove_like_from_object(like_activity, object), -         _ <- notify_and_stream(unlike_activity), -         :ok <- maybe_federate(unlike_activity) do -      {:ok, unlike_activity, like_activity, object} -    else -      nil -> {:ok, object} -      {:error, error} -> Repo.rollback(error) -    end -  end -    @spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::            {:ok, Activity.t(), Object.t()} | {:error, any()}    def announce( @@ -461,35 +386,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  @spec unannounce(User.t(), Object.t(), String.t() | nil, boolean()) :: -          {:ok, Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()} -  def unannounce( -        %User{} = actor, -        %Object{} = object, -        activity_id \\ nil, -        local \\ true -      ) do -    with {:ok, result} <- -           Repo.transaction(fn -> do_unannounce(actor, object, activity_id, local) end) do -      result -    end -  end - -  defp do_unannounce(actor, object, activity_id, local) do -    with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object), -         unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id), -         {:ok, unannounce_activity} <- insert(unannounce_data, local), -         _ <- notify_and_stream(unannounce_activity), -         :ok <- maybe_federate(unannounce_activity), -         {:ok, _activity} <- Repo.delete(announce_activity), -         {:ok, object} <- remove_announce_from_object(announce_activity, object) do -      {:ok, unannounce_activity, object} -    else -      nil -> {:ok, object} -      {:error, error} -> Repo.rollback(error) -    end -  end -    @spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::            {:ok, Activity.t()} | {:error, any()}    def follow(follower, followed, activity_id \\ nil, local \\ true) do @@ -562,28 +458,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  @spec unblock(User.t(), User.t(), String.t() | nil, boolean()) :: -          {:ok, Activity.t()} | {:error, any()} | nil -  def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do -    with {:ok, result} <- -           Repo.transaction(fn -> do_unblock(blocker, blocked, activity_id, local) end) do -      result -    end -  end - -  defp do_unblock(blocker, blocked, activity_id, local) do -    with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked), -         unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id), -         {:ok, activity} <- insert(unblock_data, local), -         _ <- notify_and_stream(activity), -         :ok <- maybe_federate(activity) do -      {:ok, activity} -    else -      nil -> nil -      {:error, error} -> Repo.rollback(error) -    end -  end -    @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}    def flag(          %{ diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 976ff243e..62ad15d85 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -396,7 +396,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do      |> json(err)    end -  defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do +  defp handle_user_activity( +         %User{} = user, +         %{"type" => "Create", "object" => %{"type" => "Note"}} = params +       ) do      object =        params["object"]        |> Map.merge(Map.take(params, ["to", "cc"])) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 1345a3a3e..922a444a9 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -10,6 +10,31 @@ defmodule Pleroma.Web.ActivityPub.Builder do    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.Visibility +  @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()} +  def emoji_react(actor, object, emoji) do +    with {:ok, data, meta} <- object_action(actor, object) do +      data = +        data +        |> Map.put("content", emoji) +        |> Map.put("type", "EmojiReact") + +      {:ok, data, meta} +    end +  end + +  @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()} +  def undo(actor, object) do +    {:ok, +     %{ +       "id" => Utils.generate_activity_id(), +       "actor" => actor.ap_id, +       "type" => "Undo", +       "object" => object.data["id"], +       "to" => object.data["to"] || [], +       "cc" => object.data["cc"] || [] +     }, []} +  end +    @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}    def delete(actor, object_id) do      object = Object.normalize(object_id, false) @@ -39,6 +64,17 @@ defmodule Pleroma.Web.ActivityPub.Builder do    @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}    def like(actor, object) do +    with {:ok, data, meta} <- object_action(actor, object) do +      data = +        data +        |> Map.put("type", "Like") + +      {:ok, data, meta} +    end +  end + +  @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()} +  defp object_action(actor, object) do      object_actor = User.get_cached_by_ap_id(object.data["actor"])      # Address the actor of the object, and our actor's follower collection if the post is public. @@ -60,7 +96,6 @@ defmodule Pleroma.Web.ActivityPub.Builder do       %{         "id" => Utils.generate_activity_id(),         "actor" => actor.ap_id, -       "type" => "Like",         "object" => object.data["id"],         "to" => to,         "cc" => cc, diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 479f922f5..549e5e761 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -12,12 +12,24 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    alias Pleroma.Object    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator +  alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.Types +  alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator    @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}    def validate(object, meta) +  def validate(%{"type" => "Undo"} = object, meta) do +    with {:ok, object} <- +           object +           |> UndoValidator.cast_and_validate() +           |> Ecto.Changeset.apply_action(:insert) do +      object = stringify_keys(object) +      {:ok, object, meta} +    end +  end +    def validate(%{"type" => "Delete"} = object, meta) do      with cng <- DeleteValidator.cast_and_validate(object),           do_not_federate <- DeleteValidator.do_not_federate?(cng), @@ -36,6 +48,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do      end    end +  def validate(%{"type" => "EmojiReact"} = object, meta) do +    with {:ok, object} <- +           object +           |> EmojiReactValidator.cast_and_validate() +           |> Ecto.Changeset.apply_action(:insert) do +      object = stringify_keys(object |> Map.from_struct()) +      {:ok, object, meta} +    end +  end +    def stringify_keys(%{__struct__: _} = object) do      object      |> Map.from_struct() diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex index 4e6ee2034..aeef31945 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -5,6 +5,7 @@  defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do    import Ecto.Changeset +  alias Pleroma.Activity    alias Pleroma.Object    alias Pleroma.User @@ -47,7 +48,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do      cng      |> validate_change(field_name, fn field_name, object_id -> -      object = Object.get_cached_by_ap_id(object_id) +      object = Object.get_cached_by_ap_id(object_id) || Activity.get_by_ap_id(object_id)        cond do          !object -> diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex new file mode 100644 index 000000000..e87519c59 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex @@ -0,0 +1,81 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do +  use Ecto.Schema + +  alias Pleroma.Object +  alias Pleroma.Web.ActivityPub.ObjectValidators.Types + +  import Ecto.Changeset +  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + +  @primary_key false + +  embedded_schema do +    field(:id, Types.ObjectID, primary_key: true) +    field(:type, :string) +    field(:object, Types.ObjectID) +    field(:actor, Types.ObjectID) +    field(:context, :string) +    field(:content, :string) +    field(:to, {:array, :string}, default: []) +    field(:cc, {:array, :string}, default: []) +  end + +  def cast_and_validate(data) do +    data +    |> cast_data() +    |> validate_data() +  end + +  def cast_data(data) do +    %__MODULE__{} +    |> changeset(data) +  end + +  def changeset(struct, data) do +    struct +    |> cast(data, __schema__(:fields)) +    |> fix_after_cast() +  end + +  def fix_after_cast(cng) do +    cng +    |> fix_context() +  end + +  def fix_context(cng) do +    object = get_field(cng, :object) + +    with nil <- get_field(cng, :context), +         %Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do +      cng +      |> put_change(:context, context) +    else +      _ -> +        cng +    end +  end + +  def validate_emoji(cng) do +    content = get_field(cng, :content) + +    if Pleroma.Emoji.is_unicode_emoji?(content) do +      cng +    else +      cng +      |> add_error(:content, "must be a single character emoji") +    end +  end + +  def validate_data(data_cng) do +    data_cng +    |> validate_inclusion(:type, ["EmojiReact"]) +    |> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content]) +    |> validate_actor_presence() +    |> validate_object_presence() +    |> validate_emoji() +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex new file mode 100644 index 000000000..d0ba418e8 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do +  use Ecto.Schema + +  alias Pleroma.Activity +  alias Pleroma.Web.ActivityPub.ObjectValidators.Types + +  import Ecto.Changeset +  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + +  @primary_key false + +  embedded_schema do +    field(:id, Types.ObjectID, primary_key: true) +    field(:type, :string) +    field(:object, Types.ObjectID) +    field(:actor, Types.ObjectID) +    field(:to, {:array, :string}, default: []) +    field(:cc, {:array, :string}, default: []) +  end + +  def cast_and_validate(data) do +    data +    |> cast_data() +    |> validate_data() +  end + +  def cast_data(data) do +    %__MODULE__{} +    |> changeset(data) +  end + +  def changeset(struct, data) do +    struct +    |> cast(data, __schema__(:fields)) +  end + +  def validate_data(data_cng) do +    data_cng +    |> validate_inclusion(:type, ["Undo"]) +    |> validate_required([:id, :type, :object, :actor, :to, :cc]) +    |> validate_actor_presence() +    |> validate_object_presence() +    |> validate_undo_rights() +  end + +  def validate_undo_rights(cng) do +    actor = get_field(cng, :actor) +    object = get_field(cng, :object) + +    with %Activity{data: %{"actor" => object_actor}} <- Activity.get_by_ap_id(object), +         true <- object_actor != actor do +      cng +      |> add_error(:actor, "not the same as object actor") +    else +      _ -> cng +    end +  end +end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 7b53abeaf..bfc2ab845 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -5,8 +5,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    liked object, a `Follow` activity will add the user to the follower    collection, and so on.    """ +  alias Pleroma.Activity    alias Pleroma.Notification    alias Pleroma.Object +  alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils @@ -25,6 +27,25 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      {:ok, object, meta}    end +  def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do +    with undone_object <- Activity.get_by_ap_id(undone_object), +         :ok <- handle_undoing(undone_object) do +      {:ok, object, meta} +    end +  end + +  # Tasks this handles: +  # - Add reaction to object +  # - Set up notification +  def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do +    reacted_object = Object.get_by_ap_id(object.data["object"]) +    Utils.add_emoji_reaction_to_object(object, reacted_object) + +    Notification.create_notifications(object) + +    {:ok, object, meta} +  end +    # Tasks this handles:    # - Delete and unpins the create activity    # - Replace object with Tombstone @@ -72,4 +93,41 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    def handle(object, meta) do      {:ok, object, meta}    end + +  def handle_undoing(%{data: %{"type" => "Like"}} = object) do +    with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]), +         {:ok, _} <- Utils.remove_like_from_object(object, liked_object), +         {:ok, _} <- Repo.delete(object) do +      :ok +    end +  end + +  def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do +    with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]), +         {:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object), +         {:ok, _} <- Repo.delete(object) do +      :ok +    end +  end + +  def handle_undoing(%{data: %{"type" => "Announce"}} = object) do +    with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]), +         {:ok, _} <- Utils.remove_announce_from_object(object, liked_object), +         {:ok, _} <- Repo.delete(object) do +      :ok +    end +  end + +  def handle_undoing( +        %{data: %{"type" => "Block", "actor" => blocker, "object" => blocked}} = object +      ) do +    with %User{} = blocker <- User.get_cached_by_ap_id(blocker), +         %User{} = blocked <- User.get_cached_by_ap_id(blocked), +         {:ok, _} <- User.unblock(blocker, blocked), +         {:ok, _} <- Repo.delete(object) do +      :ok +    end +  end + +  def handle_undoing(object), do: {:error, ["don't know how to handle", object]}  end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 0e4e7261b..be7b57f13 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -656,7 +656,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> handle_incoming(options)    end -  def handle_incoming(%{"type" => "Like"} = data, _options) do +  def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do      with :ok <- ObjectValidator.fetch_actor_and_object(data),           {:ok, activity, _meta} <-             Pipeline.common_pipeline(data, local: false) do @@ -667,27 +667,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( -        %{ -          "type" => "EmojiReact", -          "object" => object_id, -          "actor" => _actor, -          "id" => id, -          "content" => emoji -        } = data, -        _options -      ) do -    with actor <- Containment.get_actor(data), -         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), -         {:ok, object} <- get_obj_helper(object_id), -         {:ok, activity, _object} <- -           ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do -      {:ok, activity} -    else -      _e -> :error -    end -  end - -  def handle_incoming(          %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,          _options        ) do @@ -747,25 +726,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def handle_incoming(          %{            "type" => "Undo", -          "object" => %{"type" => "Announce", "object" => object_id}, -          "actor" => _actor, -          "id" => id -        } = data, -        _options -      ) do -    with actor <- Containment.get_actor(data), -         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), -         {:ok, object} <- get_obj_helper(object_id), -         {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do -      {:ok, activity} -    else -      _e -> :error -    end -  end - -  def handle_incoming( -        %{ -          "type" => "Undo",            "object" => %{"type" => "Follow", "object" => followed},            "actor" => follower,            "id" => id @@ -785,39 +745,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def handle_incoming(          %{            "type" => "Undo", -          "object" => %{"type" => "EmojiReact", "id" => reaction_activity_id}, -          "actor" => _actor, -          "id" => id +          "object" => %{"type" => type}          } = data,          _options -      ) do -    with actor <- Containment.get_actor(data), -         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), -         {:ok, activity, _} <- -           ActivityPub.unreact_with_emoji(actor, reaction_activity_id, -             activity_id: id, -             local: false -           ) do +      ) +      when type in ["Like", "EmojiReact", "Announce", "Block"] do +    with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do        {:ok, activity} -    else -      _e -> :error      end    end +  # For Undos that don't have the complete object attached, try to find it in our database.    def handle_incoming(          %{            "type" => "Undo", -          "object" => %{"type" => "Block", "object" => blocked}, -          "actor" => blocker, -          "id" => id -        } = _data, -        _options -      ) do -    with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked), -         {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker), -         {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do -      User.unblock(blocker, blocked) -      {:ok, activity} +          "object" => object +        } = activity, +        options +      ) +      when is_binary(object) do +    with %Activity{data: data} <- Activity.get_by_ap_id(object) do +      activity +      |> Map.put("object", data) +      |> handle_incoming(options)      else        _e -> :error      end @@ -840,43 +790,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def handle_incoming(          %{ -          "type" => "Undo", -          "object" => %{"type" => "Like", "object" => object_id}, -          "actor" => _actor, -          "id" => id -        } = data, -        _options -      ) do -    with actor <- Containment.get_actor(data), -         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), -         {:ok, object} <- get_obj_helper(object_id), -         {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do -      {:ok, activity} -    else -      _e -> :error -    end -  end - -  # For Undos that don't have the complete object attached, try to find it in our database. -  def handle_incoming( -        %{ -          "type" => "Undo", -          "object" => object -        } = activity, -        options -      ) -      when is_binary(object) do -    with %Activity{data: data} <- Activity.get_by_ap_id(object) do -      activity -      |> Map.put("object", data) -      |> handle_incoming(options) -    else -      _e -> :error -    end -  end - -  def handle_incoming( -        %{            "type" => "Move",            "actor" => origin_actor,            "object" => origin_actor, diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 1a3b0b3c1..09b80fa57 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -562,45 +562,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> maybe_put("id", activity_id)    end -  @doc """ -  Make unannounce activity data for the given actor and object -  """ -  def make_unannounce_data( -        %User{ap_id: ap_id} = user, -        %Activity{data: %{"context" => context, "object" => object}} = activity, -        activity_id -      ) do -    object = Object.normalize(object) - -    %{ -      "type" => "Undo", -      "actor" => ap_id, -      "object" => activity.data, -      "to" => [user.follower_address, object.data["actor"]], -      "cc" => [Pleroma.Constants.as_public()], -      "context" => context -    } -    |> maybe_put("id", activity_id) -  end - -  def make_unlike_data( -        %User{ap_id: ap_id} = user, -        %Activity{data: %{"context" => context, "object" => object}} = activity, -        activity_id -      ) do -    object = Object.normalize(object) - -    %{ -      "type" => "Undo", -      "actor" => ap_id, -      "object" => activity.data, -      "to" => [user.follower_address, object.data["actor"]], -      "cc" => [Pleroma.Constants.as_public()], -      "context" => context -    } -    |> maybe_put("id", activity_id) -  end -    def make_undo_data(          %User{ap_id: actor, follower_address: follower_address},          %Activity{ @@ -688,16 +649,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> maybe_put("id", activity_id)    end -  def make_unblock_data(blocker, blocked, block_activity, activity_id) do -    %{ -      "type" => "Undo", -      "actor" => blocker.ap_id, -      "to" => [blocked.ap_id], -      "object" => block_activity.data -    } -    |> maybe_put("id", activity_id) -  end -    #### Create-related helpers    def make_create_data(params, additional) do diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 470fc0215..70069d6f9 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -556,11 +556,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do      }    end -  defp array_of_accounts do +  def array_of_accounts do      %Schema{        title: "ArrayOfAccounts",        type: :array, -      items: Account +      items: Account, +      example: [Account.schema().example]      }    end diff --git a/lib/pleroma/web/api_spec/operations/search_operation.ex b/lib/pleroma/web/api_spec/operations/search_operation.ex new file mode 100644 index 000000000..6ea00a9a8 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/search_operation.ex @@ -0,0 +1,207 @@ +# 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.SearchOperation do +  alias OpenApiSpex.Operation +  alias OpenApiSpex.Schema +  alias Pleroma.Web.ApiSpec.AccountOperation +  alias Pleroma.Web.ApiSpec.Schemas.Account +  alias Pleroma.Web.ApiSpec.Schemas.BooleanLike +  alias Pleroma.Web.ApiSpec.Schemas.FlakeID +  alias Pleroma.Web.ApiSpec.Schemas.Status +  alias Pleroma.Web.ApiSpec.Schemas.Tag + +  import Pleroma.Web.ApiSpec.Helpers + +  def open_api_operation(action) do +    operation = String.to_existing_atom("#{action}_operation") +    apply(__MODULE__, operation, []) +  end + +  def account_search_operation do +    %Operation{ +      tags: ["Search"], +      summary: "Search for matching accounts by username or display name", +      operationId: "SearchController.account_search", +      parameters: [ +        Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for", +          required: true +        ), +        Operation.parameter( +          :limit, +          :query, +          %Schema{type: :integer, default: 40}, +          "Maximum number of results" +        ), +        Operation.parameter( +          :resolve, +          :query, +          %Schema{allOf: [BooleanLike], default: false}, +          "Attempt WebFinger lookup. Use this when `q` is an exact address." +        ), +        Operation.parameter( +          :following, +          :query, +          %Schema{allOf: [BooleanLike], default: false}, +          "Only include accounts that the user is following" +        ) +      ], +      responses: %{ +        200 => +          Operation.response( +            "Array of Account", +            "application/json", +            AccountOperation.array_of_accounts() +          ) +      } +    } +  end + +  def search_operation do +    %Operation{ +      tags: ["Search"], +      summary: "Search results", +      security: [%{"oAuth" => ["read:search"]}], +      operationId: "SearchController.search", +      deprecated: true, +      parameters: [ +        Operation.parameter( +          :account_id, +          :query, +          FlakeID, +          "If provided, statuses returned will be authored only by this account" +        ), +        Operation.parameter( +          :type, +          :query, +          %Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]}, +          "Search type" +        ), +        Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true), +        Operation.parameter( +          :resolve, +          :query, +          %Schema{allOf: [BooleanLike], default: false}, +          "Attempt WebFinger lookup" +        ), +        Operation.parameter( +          :following, +          :query, +          %Schema{allOf: [BooleanLike], default: false}, +          "Only include accounts that the user is following" +        ), +        Operation.parameter( +          :offset, +          :query, +          %Schema{type: :integer}, +          "Offset" +        ) +        | pagination_params() +      ], +      responses: %{ +        200 => Operation.response("Results", "application/json", results()) +      } +    } +  end + +  def search2_operation do +    %Operation{ +      tags: ["Search"], +      summary: "Search results", +      security: [%{"oAuth" => ["read:search"]}], +      operationId: "SearchController.search2", +      parameters: [ +        Operation.parameter( +          :account_id, +          :query, +          FlakeID, +          "If provided, statuses returned will be authored only by this account" +        ), +        Operation.parameter( +          :type, +          :query, +          %Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]}, +          "Search type" +        ), +        Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for", +          required: true +        ), +        Operation.parameter( +          :resolve, +          :query, +          %Schema{allOf: [BooleanLike], default: false}, +          "Attempt WebFinger lookup" +        ), +        Operation.parameter( +          :following, +          :query, +          %Schema{allOf: [BooleanLike], default: false}, +          "Only include accounts that the user is following" +        ) +        | pagination_params() +      ], +      responses: %{ +        200 => Operation.response("Results", "application/json", results2()) +      } +    } +  end + +  defp results2 do +    %Schema{ +      title: "SearchResults", +      type: :object, +      properties: %{ +        accounts: %Schema{ +          type: :array, +          items: Account, +          description: "Accounts which match the given query" +        }, +        statuses: %Schema{ +          type: :array, +          items: Status, +          description: "Statuses which match the given query" +        }, +        hashtags: %Schema{ +          type: :array, +          items: Tag, +          description: "Hashtags which match the given query" +        } +      }, +      example: %{ +        "accounts" => [Account.schema().example], +        "statuses" => [Status.schema().example], +        "hashtags" => [Tag.schema().example] +      } +    } +  end + +  defp results do +    %Schema{ +      title: "SearchResults", +      type: :object, +      properties: %{ +        accounts: %Schema{ +          type: :array, +          items: Account, +          description: "Accounts which match the given query" +        }, +        statuses: %Schema{ +          type: :array, +          items: Status, +          description: "Statuses which match the given query" +        }, +        hashtags: %Schema{ +          type: :array, +          items: %Schema{type: :string}, +          description: "Hashtags which match the given query" +        } +      }, +      example: %{ +        "accounts" => [Account.schema().example], +        "statuses" => [Status.schema().example], +        "hashtags" => ["cofe"] +      } +    } +  end +end diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 7a804461f..2572c9641 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do    alias Pleroma.Web.ApiSpec.Schemas.Emoji    alias Pleroma.Web.ApiSpec.Schemas.FlakeID    alias Pleroma.Web.ApiSpec.Schemas.Poll +  alias Pleroma.Web.ApiSpec.Schemas.Tag    alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope    require OpenApiSpex @@ -106,16 +107,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do        replies_count: %Schema{type: :integer},        sensitive: %Schema{type: :boolean},        spoiler_text: %Schema{type: :string}, -      tags: %Schema{ -        type: :array, -        items: %Schema{ -          type: :object, -          properties: %{ -            name: %Schema{type: :string}, -            url: %Schema{type: :string, format: :uri} -          } -        } -      }, +      tags: %Schema{type: :array, items: Tag},        uri: %Schema{type: :string, format: :uri},        url: %Schema{type: :string, nullable: true, format: :uri},        visibility: VisibilityScope diff --git a/lib/pleroma/web/api_spec/schemas/tag.ex b/lib/pleroma/web/api_spec/schemas/tag.ex new file mode 100644 index 000000000..e693fb83e --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/tag.ex @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.Tag do +  alias OpenApiSpex.Schema + +  require OpenApiSpex + +  OpenApiSpex.schema(%{ +    title: "Tag", +    description: "Represents a hashtag used within the content of a status", +    type: :object, +    properties: %{ +      name: %Schema{type: :string, description: "The value of the hashtag after the # sign"}, +      url: %Schema{ +        type: :string, +        format: :uri, +        description: "A link to the hashtag on the instance" +      } +    }, +    example: %{ +      name: "cofe", +      url: "https://lain.com/tag/cofe" +    } +  }) +end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 986e8d3f8..c538a634f 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -24,6 +24,14 @@ defmodule Pleroma.Web.CommonAPI do    require Pleroma.Constants    require Logger +  def unblock(blocker, blocked) do +    with %Activity{} = 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} +    end +  end +    def follow(follower, followed) do      timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) @@ -107,9 +115,12 @@ defmodule Pleroma.Web.CommonAPI do    def unrepeat(id, user) do      with {_, %Activity{data: %{"type" => "Create"}} = activity} <- -           {:find_activity, Activity.get_by_id(id)} do -      object = Object.normalize(activity) -      ActivityPub.unannounce(user, object) +           {:find_activity, Activity.get_by_id(id)}, +         %Object{} = note <- Object.normalize(activity, false), +         %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note), +         {:ok, undo, _} <- Builder.undo(user, announce), +         {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do +      {:ok, activity}      else        {:find_activity, _} -> {:error, :not_found}        _ -> {:error, dgettext("errors", "Could not unrepeat")} @@ -166,9 +177,12 @@ defmodule Pleroma.Web.CommonAPI do    def unfavorite(id, user) do      with {_, %Activity{data: %{"type" => "Create"}} = activity} <- -           {:find_activity, Activity.get_by_id(id)} do -      object = Object.normalize(activity) -      ActivityPub.unlike(user, object) +           {:find_activity, Activity.get_by_id(id)}, +         %Object{} = note <- Object.normalize(activity, false), +         %Activity{} = like <- Utils.get_existing_like(user.ap_id, note), +         {:ok, undo, _} <- Builder.undo(user, like), +         {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do +      {:ok, activity}      else        {:find_activity, _} -> {:error, :not_found}        _ -> {:error, dgettext("errors", "Could not unfavorite")} @@ -177,8 +191,10 @@ defmodule Pleroma.Web.CommonAPI do    def react_with_emoji(id, user, emoji) do      with %Activity{} = activity <- Activity.get_by_id(id), -         object <- Object.normalize(activity) do -      ActivityPub.react_with_emoji(user, object, emoji) +         object <- Object.normalize(activity), +         {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji), +         {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do +      {:ok, activity}      else        _ ->          {:error, dgettext("errors", "Could not add reaction emoji")} @@ -186,8 +202,10 @@ defmodule Pleroma.Web.CommonAPI do    end    def unreact_with_emoji(id, user, emoji) do -    with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do -      ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"]) +    with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji), +         {:ok, undo, _} <- Builder.undo(user, reaction_activity), +         {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do +      {:ok, activity}      else        _ ->          {:error, dgettext("errors", "Could not remove reaction emoji")} diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 8458cbdd5..b9ed2d7b2 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -356,8 +356,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    @doc "POST /api/v1/accounts/:id/unblock"    def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do -    with {:ok, _user_block} <- User.unblock(blocker, blocked), -         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do +    with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do        render(conn, "relationship.json", user: blocker, target: blocked)      else        {:error, message} -> json_response(conn, :forbidden, %{error: message}) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index cd49da6ad..0e0d54ba4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -5,7 +5,7 @@  defmodule Pleroma.Web.MastodonAPI.SearchController do    use Pleroma.Web, :controller -  import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 2, skip_relationships?: 1] +  import Pleroma.Web.ControllerHelper, only: [skip_relationships?: 1]    alias Pleroma.Activity    alias Pleroma.Plugs.OAuthScopesPlug @@ -18,6 +18,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    require Logger +  plug(Pleroma.Web.ApiSpec.CastAndValidate) +    # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)    plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated}) @@ -25,7 +27,9 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search]) -  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do +  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation + +  def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do      accounts = User.search(query, search_options(params, user))      conn @@ -36,7 +40,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    def search2(conn, params), do: do_search(:v2, conn, params)    def search(conn, params), do: do_search(:v1, conn, params) -  defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do +  defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do      options = search_options(params, user)      timeout = Keyword.get(Repo.config(), :timeout, 15_000)      default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} @@ -44,7 +48,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do      result =        default_values        |> Enum.map(fn {resource, default_value} -> -        if params["type"] in [nil, resource] do +        if params[:type] in [nil, resource] do            {resource, fn -> resource_search(version, resource, query, options) end}          else            {resource, fn -> default_value end} @@ -68,11 +72,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    defp search_options(params, user) do      [        skip_relationships: skip_relationships?(params), -      resolve: params["resolve"] == "true", -      following: params["following"] == "true", -      limit: fetch_integer_param(params, "limit"), -      offset: fetch_integer_param(params, "offset"), -      type: params["type"], +      resolve: params[:resolve], +      following: params[:following], +      limit: params[:limit], +      offset: params[:offset], +      type: params[:type],        author: get_author(params),        for_user: user      ] @@ -135,7 +139,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do      end    end -  defp get_author(%{"account_id" => account_id}) when is_binary(account_id), +  defp get_author(%{account_id: account_id}) when is_binary(account_id),      do: User.get_cached_by_id(account_id)    defp get_author(_params), do: nil diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 9eea2e9eb..12e3ba15e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -206,9 +206,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    end    @doc "POST /api/v1/statuses/:id/unreblog" -  def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do -    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), -         %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do +  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})      end    end @@ -222,9 +222,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    end    @doc "POST /api/v1/statuses/:id/unfavourite" -  def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do -    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user), -         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do +  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)      end    end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index b4b61e74c..420bd586f 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -36,9 +36,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    end    def render("show.json", %{user: user} = opts) do -    if User.visible_for?(user, opts[:for]), -      do: do_render("show.json", opts), -      else: %{} +    if User.visible_for?(user, opts[:for]) do +      do_render("show.json", opts) +    else +      %{} +    end    end    def render("mention.json", %{user: user}) do @@ -221,7 +223,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do        fields: user.fields,        bot: bot,        source: %{ -        note: (user.bio || "") |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags(), +        note: prepare_user_bio(user),          sensitive: false,          fields: user.raw_fields,          pleroma: %{ @@ -253,8 +255,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      |> maybe_put_follow_requests_count(user, opts[:for])      |> maybe_put_allow_following_move(user, opts[:for])      |> maybe_put_unread_conversation_count(user, opts[:for]) +    |> maybe_put_unread_notification_count(user, opts[:for])    end +  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() +  end + +  defp prepare_user_bio(_), do: "" +    defp username_from_nickname(string) when is_binary(string) do      hd(String.split(string, "@"))    end @@ -350,6 +361,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    defp maybe_put_unread_conversation_count(data, _, _), do: data +  defp maybe_put_unread_notification_count(data, %User{id: user_id}, %User{id: user_id} = user) do +    Kernel.put_in( +      data, +      [:pleroma, :unread_notifications_count], +      Pleroma.Notification.unread_notifications_count(user) +    ) +  end + +  defp maybe_put_unread_notification_count(data, _, _), do: data +    defp image_url(%{"url" => [%{"href" => href} | _]}), do: href    defp image_url(_), do: nil  end diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex index 9705b7a91..21d535d54 100644 --- a/lib/pleroma/web/mastodon_api/views/marker_view.ex +++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex @@ -11,7 +11,10 @@ defmodule Pleroma.Web.MastodonAPI.MarkerView do         %{           last_read_id: m.last_read_id,           version: m.lock_version, -         updated_at: NaiveDateTime.to_iso8601(m.updated_at) +         updated_at: NaiveDateTime.to_iso8601(m.updated_at), +         pleroma: %{ +           unread_count: m.unread_count +         }         }}      end)    end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 6ef3fe2dd..e2ffd02d0 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -78,7 +78,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do      user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)      unless Streamer.filtered_by_user?(user, item) do -      websocket_info({:text, view.render(template, user, item)}, %{state | user: user}) +      websocket_info({:text, view.render(template, item, user)}, %{state | user: user})      else        {:ok, state}      end 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 1bdb3aa4d..8bc77b75e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -86,7 +86,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do    end    def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do -    with {:ok, _activity, _object} <- CommonAPI.react_with_emoji(activity_id, user, emoji), +    with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji),           activity <- Activity.get_by_id(activity_id) do        conn        |> put_view(StatusView) @@ -98,7 +98,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do          "id" => activity_id,          "emoji" => emoji        }) do -    with {:ok, _activity, _object} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji), +    with {:ok, _activity} <- +           CommonAPI.unreact_with_emoji(activity_id, user, emoji),           activity <- Activity.get_by_id(activity_id) do        conn        |> put_view(StatusView) diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index a9f893f7b..691725702 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -106,14 +106,13 @@ defmodule Pleroma.Web.Push.Impl do    def build_content(          %{ -          activity: %{data: %{"directMessage" => true}},            user: %{notification_settings: %{privacy_option: true}} -        }, -        actor, +        } = notification, +        _actor,          _object, -        _mastodon_type +        mastodon_type        ) do -    %{title: "New Direct Message", body: "@#{actor.nickname}"} +    %{body: format_title(notification, mastodon_type)}    end    def build_content(notification, actor, object, mastodon_type) do diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 443868878..237b29ded 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -25,7 +25,7 @@ defmodule Pleroma.Web.StreamerView do      |> Jason.encode!()    end -  def render("notification.json", %User{} = user, %Notification{} = notify) do +  def render("notification.json", %Notification{} = notify, %User{} = user) do      %{        event: "notification",        payload: | 
