aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/marker.ex45
-rw-r--r--lib/pleroma/notification.ex55
-rw-r--r--lib/pleroma/user.ex20
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex126
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex5
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex37
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex22
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_validations.ex3
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex81
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/undo_validator.ex62
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex57
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex117
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex49
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex5
-rw-r--r--lib/pleroma/web/api_spec/operations/search_operation.ex207
-rw-r--r--lib/pleroma/web/api_spec/schemas/status.ex12
-rw-r--r--lib/pleroma/web/api_spec/schemas/tag.ex27
-rw-r--r--lib/pleroma/web/common_api/common_api.ex38
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex3
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex24
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex29
-rw-r--r--lib/pleroma/web/mastodon_api/views/marker_view.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex5
-rw-r--r--lib/pleroma/web/push/impl.ex9
-rw-r--r--lib/pleroma/web/views/streamer_view.ex2
27 files changed, 696 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 a2c870ded..5df3927bd 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 75b7b1b59..b4bccedef 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -363,81 +363,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(
@@ -468,35 +393,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
@@ -569,28 +465,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 6e3a375e7..2a21a3811 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -11,6 +11,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)
@@ -76,6 +101,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.
@@ -97,7 +133,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 cc5ca1d9e..7f1e0171c 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -14,12 +14,24 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
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),
@@ -50,6 +62,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 validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
with {:ok, object_data} <- cast_and_apply(object),
meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
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 8bdc433ff..28b519432 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
collection, and so on.
"""
alias Pleroma.Chat
+ alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@@ -41,6 +42,25 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
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
@@ -109,4 +129,41 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
def handle_object_creation(object) do
{:ok, object}
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 55e0df283..29f668cad 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -666,7 +666,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
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
@@ -677,27 +677,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
@@ -757,25 +736,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
@@ -795,39 +755,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
@@ -850,43 +800,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 d7d934683..ad2096c16 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -55,6 +55,14 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ 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])
@@ -138,9 +146,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")}
@@ -197,9 +208,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")}
@@ -208,8 +222,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")}
@@ -217,8 +233,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 c46517e49..057f4f457 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: %{
@@ -254,8 +256,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
@@ -351,6 +362,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: