Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms
This commit is contained in:
commit
7637ef4203
50 changed files with 1415 additions and 896 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -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"]))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -676,27 +676,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
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
|
||||
|
|
@ -754,25 +733,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
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",
|
||||
|
|
@ -795,75 +755,13 @@ 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
|
||||
|
||||
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}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"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.block(blocker, blocked, id, false) do
|
||||
User.unfollow(blocker, blocked)
|
||||
User.block(blocker, blocked)
|
||||
{:ok, activity}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -885,6 +783,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"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.block(blocker, blocked, id, false) do
|
||||
User.unfollow(blocker, blocked)
|
||||
User.block(blocker, blocked)
|
||||
{:ok, activity}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Move",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
207
lib/pleroma/web/api_spec/operations/search_operation.ex
Normal file
207
lib/pleroma/web/api_spec/operations/search_operation.ex
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
27
lib/pleroma/web/api_spec/schemas/tag.ex
Normal file
27
lib/pleroma/web/api_spec/schemas/tag.ex
Normal file
|
|
@ -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
|
||||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue