Merge remote-tracking branch 'tusooa/from/upstream-develop/tusooa/edits' into emr_develop
This commit is contained in:
commit
dea83d3cc1
|
@ -17,11 +17,11 @@ su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate"
|
||||||
## For from source installations (using git)
|
## For from source installations (using git)
|
||||||
|
|
||||||
1. Go to the working directory of Pleroma (default is `/opt/pleroma`)
|
1. Go to the working directory of Pleroma (default is `/opt/pleroma`)
|
||||||
2. Run `git pull`. This pulls the latest changes from upstream.
|
2. Run `git pull` [^1]. This pulls the latest changes from upstream.
|
||||||
3. Run `mix deps.get` [^1]. This pulls in any new dependencies.
|
3. Run `mix deps.get` [^1]. This pulls in any new dependencies.
|
||||||
4. Stop the Pleroma service.
|
4. Stop the Pleroma service.
|
||||||
5. Run `mix ecto.migrate` [^1] [^2]. This task performs database migrations, if there were any.
|
5. Run `mix ecto.migrate` [^1] [^2]. This task performs database migrations, if there were any.
|
||||||
6. Start the Pleroma service.
|
6. Start the Pleroma service.
|
||||||
|
|
||||||
[^1]: Depending on which install guide you followed (for example on Debian/Ubuntu), you want to run `mix` tasks as `pleroma` user by adding `sudo -Hu pleroma` before the command.
|
[^1]: Depending on which install guide you followed (for example on Debian/Ubuntu), you want to run `git` and `mix` tasks as `pleroma` user by adding `sudo -Hu pleroma` before the command.
|
||||||
[^2]: Prefix with `MIX_ENV=prod` to run it using the production config file.
|
[^2]: Prefix with `MIX_ENV=prod` to run it using the production config file.
|
||||||
|
|
|
@ -40,6 +40,10 @@ Has these additional fields under the `pleroma` object:
|
||||||
- `parent_visible`: If the parent of this post is visible to the user or not.
|
- `parent_visible`: If the parent of this post is visible to the user or not.
|
||||||
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
|
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
|
||||||
|
|
||||||
|
The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes:
|
||||||
|
|
||||||
|
- `content_type`: The content type of the status source.
|
||||||
|
|
||||||
## Scheduled statuses
|
## Scheduled statuses
|
||||||
|
|
||||||
Has these additional fields in `params`:
|
Has these additional fields in `params`:
|
||||||
|
|
|
@ -27,4 +27,28 @@ defmodule Pleroma.Constants do
|
||||||
do:
|
do:
|
||||||
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
|
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const(status_updatable_fields,
|
||||||
|
do: [
|
||||||
|
"source",
|
||||||
|
"tag",
|
||||||
|
"updated",
|
||||||
|
"emoji",
|
||||||
|
"content",
|
||||||
|
"summary",
|
||||||
|
"sensitive",
|
||||||
|
"attachment",
|
||||||
|
"generator"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const(actor_types,
|
||||||
|
do: [
|
||||||
|
"Application",
|
||||||
|
"Group",
|
||||||
|
"Organization",
|
||||||
|
"Person",
|
||||||
|
"Service"
|
||||||
|
]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -385,7 +385,7 @@ defmodule Pleroma.Notification do
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
|
def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
|
||||||
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag"] do
|
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
|
||||||
do_create_notifications(activity, options)
|
do_create_notifications(activity, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -439,6 +439,9 @@ defmodule Pleroma.Notification do
|
||||||
activity
|
activity
|
||||||
|> type_from_activity_object()
|
|> type_from_activity_object()
|
||||||
|
|
||||||
|
"Update" ->
|
||||||
|
"update"
|
||||||
|
|
||||||
t ->
|
t ->
|
||||||
raise "No notification type for activity type #{t}"
|
raise "No notification type for activity type #{t}"
|
||||||
end
|
end
|
||||||
|
@ -513,7 +516,16 @@ defmodule Pleroma.Notification do
|
||||||
def get_notified_from_activity(activity, local_only \\ true)
|
def get_notified_from_activity(activity, local_only \\ true)
|
||||||
|
|
||||||
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
|
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
|
||||||
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do
|
when type in [
|
||||||
|
"Create",
|
||||||
|
"Like",
|
||||||
|
"Announce",
|
||||||
|
"Follow",
|
||||||
|
"Move",
|
||||||
|
"EmojiReact",
|
||||||
|
"Flag",
|
||||||
|
"Update"
|
||||||
|
] do
|
||||||
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
|
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
|
||||||
|
|
||||||
potential_receivers =
|
potential_receivers =
|
||||||
|
@ -553,6 +565,21 @@ defmodule Pleroma.Notification do
|
||||||
(User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
|
(User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Update activity: notify all who repeated this
|
||||||
|
def get_potential_receiver_ap_ids(%{data: %{"type" => "Update", "actor" => actor}} = activity) do
|
||||||
|
with %Object{data: %{"id" => object_id}} <- Object.normalize(activity, fetch: false) do
|
||||||
|
repeaters =
|
||||||
|
Activity.Queries.by_type("Announce")
|
||||||
|
|> Activity.Queries.by_object_id(object_id)
|
||||||
|
|> Activity.with_joined_user_actor()
|
||||||
|
|> where([a, u], u.local)
|
||||||
|
|> select([a, u], u.ap_id)
|
||||||
|
|> Repo.all()
|
||||||
|
|
||||||
|
repeaters -- [actor]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def get_potential_receiver_ap_ids(activity) do
|
def get_potential_receiver_ap_ids(activity) do
|
||||||
[]
|
[]
|
||||||
|> Utils.maybe_notify_to_recipients(activity)
|
|> Utils.maybe_notify_to_recipients(activity)
|
||||||
|
|
|
@ -425,4 +425,46 @@ defmodule Pleroma.Object do
|
||||||
end
|
end
|
||||||
|
|
||||||
def object_data_hashtags(_), do: []
|
def object_data_hashtags(_), do: []
|
||||||
|
|
||||||
|
def history_for(object) do
|
||||||
|
with history <- Map.get(object, "formerRepresentations"),
|
||||||
|
true <- is_map(history),
|
||||||
|
"OrderedCollection" <- Map.get(history, "type"),
|
||||||
|
true <- is_list(Map.get(history, "orderedItems")),
|
||||||
|
true <- is_integer(Map.get(history, "totalItems")) do
|
||||||
|
history
|
||||||
|
else
|
||||||
|
_ -> history_skeleton()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp history_skeleton do
|
||||||
|
%{
|
||||||
|
"type" => "OrderedCollection",
|
||||||
|
"totalItems" => 0,
|
||||||
|
"orderedItems" => []
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_update_history(updated_object, orig_object_data, updated) do
|
||||||
|
if not updated do
|
||||||
|
updated_object
|
||||||
|
else
|
||||||
|
# Put edit history
|
||||||
|
# Note that we may have got the edit history by first fetching the object
|
||||||
|
history = Object.history_for(orig_object_data)
|
||||||
|
|
||||||
|
latest_history_item =
|
||||||
|
orig_object_data
|
||||||
|
|> Map.drop(["id", "formerRepresentations"])
|
||||||
|
|
||||||
|
new_history =
|
||||||
|
history
|
||||||
|
|> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
|
||||||
|
|> Map.put("totalItems", history["totalItems"] + 1)
|
||||||
|
|
||||||
|
updated_object
|
||||||
|
|> Map.put("formerRepresentations", new_history)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,8 +26,35 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
|
defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
|
||||||
|
has_history? = fn
|
||||||
|
%{"formerRepresentations" => %{"orderedItems" => list}} when is_list(list) -> true
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
|
||||||
internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
|
internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
|
||||||
|
|
||||||
|
remote_history_exists? = has_history?.(new_data)
|
||||||
|
|
||||||
|
# If the remote history exists, we treat that as the only source of truth.
|
||||||
|
new_data =
|
||||||
|
if has_history?.(old_data) and not remote_history_exists? do
|
||||||
|
Map.put(new_data, "formerRepresentations", old_data["formerRepresentations"])
|
||||||
|
else
|
||||||
|
new_data
|
||||||
|
end
|
||||||
|
|
||||||
|
# If the remote does not have history information, we need to manage it ourselves
|
||||||
|
new_data =
|
||||||
|
if not remote_history_exists? do
|
||||||
|
changed? =
|
||||||
|
Pleroma.Constants.status_updatable_fields()
|
||||||
|
|> Enum.any?(fn field -> Map.get(old_data, field) != Map.get(new_data, field) end)
|
||||||
|
|
||||||
|
new_data |> Object.maybe_update_history(old_data, changed?)
|
||||||
|
else
|
||||||
|
new_data
|
||||||
|
end
|
||||||
|
|
||||||
Map.merge(new_data, internal_fields)
|
Map.merge(new_data, internal_fields)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ defmodule Pleroma.Upload do
|
||||||
alias Ecto.UUID
|
alias Ecto.UUID
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
alias Pleroma.Maps
|
alias Pleroma.Maps
|
||||||
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@type source ::
|
@type source ::
|
||||||
|
@ -88,6 +89,7 @@ defmodule Pleroma.Upload do
|
||||||
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
|
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
|
"id" => Utils.generate_object_id(),
|
||||||
"type" => opts.activity_type,
|
"type" => opts.activity_type,
|
||||||
"mediaType" => upload.content_type,
|
"mediaType" => upload.content_type,
|
||||||
"url" => [
|
"url" => [
|
||||||
|
|
|
@ -190,7 +190,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
def notify_and_stream(activity) do
|
def notify_and_stream(activity) do
|
||||||
Notification.create_notifications(activity)
|
Notification.create_notifications(activity)
|
||||||
|
|
||||||
conversation = create_or_bump_conversation(activity, activity.actor)
|
original_activity =
|
||||||
|
case activity do
|
||||||
|
%{data: %{"type" => "Update"}, object: %{data: %{"id" => id}}} ->
|
||||||
|
Activity.get_create_by_object_ap_id_with_object(id)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
activity
|
||||||
|
end
|
||||||
|
|
||||||
|
conversation = create_or_bump_conversation(original_activity, original_activity.actor)
|
||||||
participations = get_participations(conversation)
|
participations = get_participations(conversation)
|
||||||
stream_out(activity)
|
stream_out(activity)
|
||||||
stream_out_participations(participations)
|
stream_out_participations(participations)
|
||||||
|
@ -256,7 +265,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def stream_out(%Activity{data: %{"type" => data_type}} = activity)
|
def stream_out(%Activity{data: %{"type" => data_type}} = activity)
|
||||||
when data_type in ["Create", "Announce", "Delete"] do
|
when data_type in ["Create", "Announce", "Delete", "Update"] do
|
||||||
activity
|
activity
|
||||||
|> Topics.get_activity_topics()
|
|> Topics.get_activity_topics()
|
||||||
|> Streamer.stream(activity)
|
|> Streamer.stream(activity)
|
||||||
|
|
|
@ -218,10 +218,16 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Retricted to user updates for now, always public
|
|
||||||
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
||||||
def update(actor, object) do
|
def update(actor, object) do
|
||||||
to = [Pleroma.Constants.as_public(), actor.follower_address]
|
{to, cc} =
|
||||||
|
if object["type"] in Pleroma.Constants.actor_types() do
|
||||||
|
# User updates, always public
|
||||||
|
{[Pleroma.Constants.as_public(), actor.follower_address], []}
|
||||||
|
else
|
||||||
|
# Status updates, follow the recipients in the object
|
||||||
|
{object["to"] || [], object["cc"] || []}
|
||||||
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
|
@ -229,7 +235,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
||||||
"type" => "Update",
|
"type" => "Update",
|
||||||
"actor" => actor.ap_id,
|
"actor" => actor.ap_id,
|
||||||
"object" => object,
|
"object" => object,
|
||||||
"to" => to
|
"to" => to,
|
||||||
|
"cc" => cc
|
||||||
}, []}
|
}, []}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
||||||
|
|
||||||
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
|
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
|
||||||
|
|
||||||
|
defp shortcode_matches?(shortcode, pattern) when is_binary(pattern) do
|
||||||
|
shortcode == pattern
|
||||||
|
end
|
||||||
|
|
||||||
|
defp shortcode_matches?(shortcode, pattern) do
|
||||||
|
String.match?(shortcode, pattern)
|
||||||
|
end
|
||||||
|
|
||||||
defp steal_emoji({shortcode, url}, emoji_dir_path) do
|
defp steal_emoji({shortcode, url}, emoji_dir_path) do
|
||||||
url = Pleroma.Web.MediaProxy.url(url)
|
url = Pleroma.Web.MediaProxy.url(url)
|
||||||
|
|
||||||
|
@ -72,7 +80,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
||||||
reject_emoji? =
|
reject_emoji? =
|
||||||
[:mrf_steal_emoji, :rejected_shortcodes]
|
[:mrf_steal_emoji, :rejected_shortcodes]
|
||||||
|> Config.get([])
|
|> Config.get([])
|
||||||
|> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
|
|> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end)
|
||||||
|
|
||||||
!reject_emoji?
|
!reject_emoji?
|
||||||
end)
|
end)
|
||||||
|
@ -122,8 +130,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
||||||
%{
|
%{
|
||||||
key: :rejected_shortcodes,
|
key: :rejected_shortcodes,
|
||||||
type: {:list, :string},
|
type: {:list, :string},
|
||||||
description: "Regex-list of shortcodes to reject",
|
description: """
|
||||||
suggestions: [""]
|
A list of patterns or matches to reject shortcodes with.
|
||||||
|
|
||||||
|
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||||
|
""",
|
||||||
|
suggestions: ["foo", ~r/foo/]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :size_limit,
|
key: :size_limit,
|
||||||
|
|
|
@ -51,7 +51,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
|
||||||
with actor = get_field(cng, :actor),
|
with actor = get_field(cng, :actor),
|
||||||
object = get_field(cng, :object),
|
object = get_field(cng, :object),
|
||||||
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
|
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
|
||||||
true <- actor == object_id do
|
actor_uri <- URI.parse(actor),
|
||||||
|
object_uri <- URI.parse(object_id),
|
||||||
|
true <- actor_uri.host == object_uri.host do
|
||||||
cng
|
cng
|
||||||
else
|
else
|
||||||
_e ->
|
_e ->
|
||||||
|
|
|
@ -25,6 +25,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
alias Pleroma.Web.Streamer
|
alias Pleroma.Web.Streamer
|
||||||
alias Pleroma.Workers.PollWorker
|
alias Pleroma.Workers.PollWorker
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||||
|
@ -153,24 +154,26 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
|
|
||||||
# Tasks this handles:
|
# Tasks this handles:
|
||||||
# - Update the user
|
# - Update the user
|
||||||
|
# - Update a non-user object (Note, Question, etc.)
|
||||||
#
|
#
|
||||||
# For a local user, we also get a changeset with the full information, so we
|
# For a local user, we also get a changeset with the full information, so we
|
||||||
# can update non-federating, non-activitypub settings as well.
|
# can update non-federating, non-activitypub settings as well.
|
||||||
@impl true
|
@impl true
|
||||||
def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
|
def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
|
||||||
if changeset = Keyword.get(meta, :user_update_changeset) do
|
updated_object_id = updated_object["id"]
|
||||||
changeset
|
|
||||||
|> User.update_and_set_cache()
|
with {_, true} <- {:has_id, is_binary(updated_object_id)},
|
||||||
|
{_, user} <- {:user, Pleroma.User.get_by_ap_id(updated_object_id)} do
|
||||||
|
if user do
|
||||||
|
handle_update_user(object, meta)
|
||||||
else
|
else
|
||||||
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object)
|
handle_update_object(object, meta)
|
||||||
|
|
||||||
User.get_by_ap_id(updated_object["id"])
|
|
||||||
|> User.remote_user_changeset(new_user_data)
|
|
||||||
|> User.update_and_set_cache()
|
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
{:ok, object, meta}
|
{:ok, object, meta}
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Tasks this handles:
|
# Tasks this handles:
|
||||||
# - Add like to object
|
# - Add like to object
|
||||||
|
@ -390,6 +393,96 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
{:ok, object, meta}
|
{:ok, object, meta}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp handle_update_user(
|
||||||
|
%{data: %{"type" => "Update", "object" => updated_object}} = object,
|
||||||
|
meta
|
||||||
|
) do
|
||||||
|
if changeset = Keyword.get(meta, :user_update_changeset) do
|
||||||
|
changeset
|
||||||
|
|> User.update_and_set_cache()
|
||||||
|
else
|
||||||
|
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object)
|
||||||
|
|
||||||
|
User.get_by_ap_id(updated_object["id"])
|
||||||
|
|> User.remote_user_changeset(new_user_data)
|
||||||
|
|> User.update_and_set_cache()
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
|
||||||
|
@updatable_object_types ["Note", "Question"]
|
||||||
|
defp update_content_fields(orig_object_data, updated_object) do
|
||||||
|
Pleroma.Constants.status_updatable_fields()
|
||||||
|
|> Enum.reduce(
|
||||||
|
%{data: orig_object_data, updated: false},
|
||||||
|
fn field, %{data: data, updated: updated} ->
|
||||||
|
updated = updated or Map.get(updated_object, field) != Map.get(orig_object_data, field)
|
||||||
|
|
||||||
|
data =
|
||||||
|
if Map.has_key?(updated_object, field) do
|
||||||
|
Map.put(data, field, updated_object[field])
|
||||||
|
else
|
||||||
|
Map.drop(data, [field])
|
||||||
|
end
|
||||||
|
|
||||||
|
%{data: data, updated: updated}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_update_poll(to_be_updated, updated_object) do
|
||||||
|
choice_key = fn data ->
|
||||||
|
if Map.has_key?(data, "anyOf"), do: "anyOf", else: "oneOf"
|
||||||
|
end
|
||||||
|
|
||||||
|
with true <- to_be_updated["type"] == "Question",
|
||||||
|
key <- choice_key.(updated_object),
|
||||||
|
true <- key == choice_key.(to_be_updated),
|
||||||
|
orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])),
|
||||||
|
new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])),
|
||||||
|
true <- orig_choices == new_choices do
|
||||||
|
# Choices are the same, but counts are different
|
||||||
|
to_be_updated
|
||||||
|
|> Map.put(key, updated_object[key])
|
||||||
|
else
|
||||||
|
# Choices (or vote type) have changed, do not allow this
|
||||||
|
_ -> to_be_updated
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_update_object(
|
||||||
|
%{data: %{"type" => "Update", "object" => updated_object}} = object,
|
||||||
|
meta
|
||||||
|
) do
|
||||||
|
orig_object = Object.get_by_ap_id(updated_object["id"])
|
||||||
|
orig_object_data = orig_object.data
|
||||||
|
|
||||||
|
if orig_object_data["type"] in @updatable_object_types do
|
||||||
|
%{data: updated_object_data, updated: updated} =
|
||||||
|
orig_object_data
|
||||||
|
|> update_content_fields(updated_object)
|
||||||
|
|
||||||
|
updated_object_data =
|
||||||
|
updated_object_data
|
||||||
|
|> Object.maybe_update_history(orig_object_data, updated)
|
||||||
|
|> maybe_update_poll(updated_object)
|
||||||
|
|
||||||
|
orig_object
|
||||||
|
|> Repo.preload(:hashtags)
|
||||||
|
|> Object.change(%{data: updated_object_data})
|
||||||
|
|> Object.update_and_set_cache()
|
||||||
|
|
||||||
|
if updated do
|
||||||
|
object
|
||||||
|
|> Activity.normalize()
|
||||||
|
|> ActivityPub.notify_and_stream()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do
|
def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do
|
||||||
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
|
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
|
||||||
actor = User.get_cached_by_ap_id(object.data["actor"])
|
actor = User.get_cached_by_ap_id(object.data["actor"])
|
||||||
|
|
|
@ -902,7 +902,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
|
|
||||||
def strip_internal_fields(object) do
|
def strip_internal_fields(object) do
|
||||||
Map.drop(object, Pleroma.Constants.object_internal_fields())
|
outer = Map.drop(object, Pleroma.Constants.object_internal_fields())
|
||||||
|
|
||||||
|
case outer do
|
||||||
|
%{"formerRepresentations" => %{"orderedItems" => list}} when is_list(list) ->
|
||||||
|
update_in(
|
||||||
|
outer["formerRepresentations"]["orderedItems"],
|
||||||
|
&Enum.map(
|
||||||
|
&1,
|
||||||
|
fn
|
||||||
|
item when is_map(item) -> Map.drop(item, Pleroma.Constants.object_internal_fields())
|
||||||
|
item -> item
|
||||||
|
end
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
outer
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp strip_internal_tags(%{"tag" => tags} = object) do
|
defp strip_internal_tags(%{"tag" => tags} = object) do
|
||||||
|
|
|
@ -6,9 +6,13 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
||||||
alias OpenApiSpex.Operation
|
alias OpenApiSpex.Operation
|
||||||
alias OpenApiSpex.Schema
|
alias OpenApiSpex.Schema
|
||||||
alias Pleroma.Web.ApiSpec.AccountOperation
|
alias Pleroma.Web.ApiSpec.AccountOperation
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Attachment
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
|
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||||
|
@ -434,6 +438,59 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show_history_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Retrieve status history"],
|
||||||
|
summary: "Status history",
|
||||||
|
description: "View history of a status",
|
||||||
|
operationId: "StatusController.show_history",
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
parameters: [
|
||||||
|
id_param()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => status_history_response(),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_source_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Retrieve status source"],
|
||||||
|
summary: "Status source",
|
||||||
|
description: "View source of a status",
|
||||||
|
operationId: "StatusController.show_source",
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
parameters: [
|
||||||
|
id_param()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => status_source_response(),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Update status"],
|
||||||
|
summary: "Update status",
|
||||||
|
description: "Change the content of a status",
|
||||||
|
operationId: "StatusController.update",
|
||||||
|
security: [%{"oAuth" => ["write:statuses"]}],
|
||||||
|
parameters: [
|
||||||
|
id_param()
|
||||||
|
],
|
||||||
|
requestBody: request_body("Parameters", update_request(), required: true),
|
||||||
|
responses: %{
|
||||||
|
200 => status_response(),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def array_of_statuses do
|
def array_of_statuses do
|
||||||
%Schema{type: :array, items: Status, example: [Status.schema().example]}
|
%Schema{type: :array, items: Status, example: [Status.schema().example]}
|
||||||
end
|
end
|
||||||
|
@ -537,6 +594,60 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp update_request do
|
||||||
|
%Schema{
|
||||||
|
title: "StatusUpdateRequest",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
status: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
|
||||||
|
},
|
||||||
|
media_ids: %Schema{
|
||||||
|
nullable: true,
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
description: "Array of Attachment ids to be attached as media."
|
||||||
|
},
|
||||||
|
poll: poll_params(),
|
||||||
|
sensitive: %Schema{
|
||||||
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
|
description: "Mark status and attached media as sensitive?"
|
||||||
|
},
|
||||||
|
spoiler_text: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
|
||||||
|
},
|
||||||
|
content_type: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
|
||||||
|
},
|
||||||
|
to: %Schema{
|
||||||
|
type: :array,
|
||||||
|
nullable: true,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
description:
|
||||||
|
"A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"status" => "What time is it?",
|
||||||
|
"sensitive" => "false",
|
||||||
|
"poll" => %{
|
||||||
|
"options" => ["Cofe", "Adventure"],
|
||||||
|
"expires_in" => 420
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def poll_params do
|
def poll_params do
|
||||||
%Schema{
|
%Schema{
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -579,6 +690,87 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
||||||
Operation.response("Status", "application/json", Status)
|
Operation.response("Status", "application/json", Status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp status_history_response do
|
||||||
|
Operation.response(
|
||||||
|
"Status History",
|
||||||
|
"application/json",
|
||||||
|
%Schema{
|
||||||
|
title: "Status history",
|
||||||
|
description: "Response schema for history of a status",
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
account: %Schema{
|
||||||
|
allOf: [Account],
|
||||||
|
description: "The account that authored this status"
|
||||||
|
},
|
||||||
|
content: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :html,
|
||||||
|
description: "HTML-encoded status content"
|
||||||
|
},
|
||||||
|
sensitive: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Is this status marked as sensitive content?"
|
||||||
|
},
|
||||||
|
spoiler_text: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"Subject or summary line, below which status content is collapsed until expanded"
|
||||||
|
},
|
||||||
|
created_at: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: "date-time",
|
||||||
|
description: "The date when this status was created"
|
||||||
|
},
|
||||||
|
media_attachments: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Attachment,
|
||||||
|
description: "Media that is attached to this status"
|
||||||
|
},
|
||||||
|
emojis: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Emoji,
|
||||||
|
description: "Custom emoji to be used when rendering status content"
|
||||||
|
},
|
||||||
|
poll: %Schema{
|
||||||
|
allOf: [Poll],
|
||||||
|
nullable: true,
|
||||||
|
description: "The poll attached to the status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_source_response do
|
||||||
|
Operation.response(
|
||||||
|
"Status Source",
|
||||||
|
"application/json",
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: FlakeID,
|
||||||
|
text: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Raw source of status content"
|
||||||
|
},
|
||||||
|
spoiler_text: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"Subject or summary line, below which status content is collapsed until expanded"
|
||||||
|
},
|
||||||
|
content_type: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "The content type of the source"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
defp context do
|
defp context do
|
||||||
%Schema{
|
%Schema{
|
||||||
title: "StatusContext",
|
title: "StatusContext",
|
||||||
|
|
|
@ -73,6 +73,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
||||||
format: "date-time",
|
format: "date-time",
|
||||||
description: "The date when this status was created"
|
description: "The date when this status was created"
|
||||||
},
|
},
|
||||||
|
edited_at: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: "date-time",
|
||||||
|
nullable: true,
|
||||||
|
description: "The date when this status was last edited"
|
||||||
|
},
|
||||||
emojis: %Schema{
|
emojis: %Schema{
|
||||||
type: :array,
|
type: :array,
|
||||||
items: Emoji,
|
items: Emoji,
|
||||||
|
|
|
@ -402,6 +402,42 @@ defmodule Pleroma.Web.CommonAPI do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update(user, orig_activity, changes) do
|
||||||
|
with orig_object <- Object.normalize(orig_activity),
|
||||||
|
{:ok, new_object} <- make_update_data(user, orig_object, changes),
|
||||||
|
{:ok, update_data, _} <- Builder.update(user, new_object),
|
||||||
|
{:ok, update, _} <- Pipeline.common_pipeline(update_data, local: true) do
|
||||||
|
{:ok, update}
|
||||||
|
else
|
||||||
|
_ -> {:error, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp make_update_data(user, orig_object, changes) do
|
||||||
|
kept_params = %{
|
||||||
|
visibility: Visibility.get_visibility(orig_object)
|
||||||
|
}
|
||||||
|
|
||||||
|
params = Map.merge(changes, kept_params)
|
||||||
|
|
||||||
|
with {:ok, draft} <- ActivityDraft.create(user, params) do
|
||||||
|
change =
|
||||||
|
Pleroma.Constants.status_updatable_fields()
|
||||||
|
|> Enum.reduce(orig_object.data, fn key, acc ->
|
||||||
|
if Map.has_key?(draft.object, key) do
|
||||||
|
acc |> Map.put(key, Map.get(draft.object, key))
|
||||||
|
else
|
||||||
|
acc |> Map.drop([key])
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Map.put("updated", Utils.make_date())
|
||||||
|
|
||||||
|
{:ok, change}
|
||||||
|
else
|
||||||
|
_ -> {:error, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
|
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
|
||||||
def pin(id, %User{} = user) do
|
def pin(id, %User{} = user) do
|
||||||
with %Activity{} = activity <- create_activity_by_id(id),
|
with %Activity{} = activity <- create_activity_by_id(id),
|
||||||
|
|
|
@ -224,7 +224,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
object =
|
object =
|
||||||
note_data
|
note_data
|
||||||
|> Map.put("emoji", emoji)
|
|> Map.put("emoji", emoji)
|
||||||
|> Map.put("source", draft.status)
|
|> Map.put("source", %{
|
||||||
|
"content" => draft.status,
|
||||||
|
"mediaType" => Utils.get_content_type(draft.params[:content_type])
|
||||||
|
})
|
||||||
|> Map.put("generator", draft.params[:generator])
|
|> Map.put("generator", draft.params[:generator])
|
||||||
|
|
||||||
%__MODULE__{draft | object: object}
|
%__MODULE__{draft | object: object}
|
||||||
|
|
|
@ -37,7 +37,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
||||||
|
|
||||||
def attachments_from_ids_no_descs(ids) do
|
def attachments_from_ids_no_descs(ids) do
|
||||||
Enum.map(ids, fn media_id ->
|
Enum.map(ids, fn media_id ->
|
||||||
case Repo.get(Object, media_id) do
|
case get_attachment(media_id) do
|
||||||
%Object{data: data} -> data
|
%Object{data: data} -> data
|
||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
|
@ -51,13 +51,17 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
||||||
{_, descs} = Jason.decode(descs_str)
|
{_, descs} = Jason.decode(descs_str)
|
||||||
|
|
||||||
Enum.map(ids, fn media_id ->
|
Enum.map(ids, fn media_id ->
|
||||||
with %Object{data: data} <- Repo.get(Object, media_id) do
|
with %Object{data: data} <- get_attachment(media_id) do
|
||||||
Map.put(data, "name", descs[media_id])
|
Map.put(data, "name", descs[media_id])
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_attachment(media_id) do
|
||||||
|
Repo.get(Object, media_id)
|
||||||
|
end
|
||||||
|
|
||||||
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
|
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
|
||||||
|
|
||||||
def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do
|
def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do
|
||||||
|
@ -219,7 +223,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
||||||
|> maybe_add_attachments(draft.attachments, attachment_links)
|
|> maybe_add_attachments(draft.attachments, attachment_links)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_content_type(content_type) do
|
def get_content_type(content_type) do
|
||||||
if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
|
if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
|
||||||
content_type
|
content_type
|
||||||
else
|
else
|
||||||
|
|
|
@ -51,6 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
||||||
move
|
move
|
||||||
pleroma:emoji_reaction
|
pleroma:emoji_reaction
|
||||||
poll
|
poll
|
||||||
|
update
|
||||||
}
|
}
|
||||||
def index(%{assigns: %{user: user}} = conn, params) do
|
def index(%{assigns: %{user: user}} = conn, params) do
|
||||||
params =
|
params =
|
||||||
|
|
|
@ -38,7 +38,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||||
:index,
|
:index,
|
||||||
:show,
|
:show,
|
||||||
:card,
|
:card,
|
||||||
:context
|
:context,
|
||||||
|
:show_history,
|
||||||
|
:show_source
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -49,7 +51,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||||
:create,
|
:create,
|
||||||
:delete,
|
:delete,
|
||||||
:reblog,
|
:reblog,
|
||||||
:unreblog
|
:unreblog,
|
||||||
|
:update
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -191,6 +194,57 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||||
create(%Plug.Conn{conn | body_params: params}, %{})
|
create(%Plug.Conn{conn | body_params: params}, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/statuses/:id/history"
|
||||||
|
def show_history(%{assigns: %{user: user}} = conn, %{id: id} = params) do
|
||||||
|
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
||||||
|
true <- Visibility.visible_for_user?(activity, user) do
|
||||||
|
try_render(conn, "history.json",
|
||||||
|
activity: activity,
|
||||||
|
for: user,
|
||||||
|
with_direct_conversation_id: true,
|
||||||
|
with_muted: Map.get(params, :with_muted, false)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
_ -> {:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "GET /api/v1/statuses/:id/source"
|
||||||
|
def show_source(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
|
||||||
|
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
||||||
|
true <- Visibility.visible_for_user?(activity, user) do
|
||||||
|
try_render(conn, "source.json",
|
||||||
|
activity: activity,
|
||||||
|
for: user
|
||||||
|
)
|
||||||
|
else
|
||||||
|
_ -> {:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "PUT /api/v1/statuses/:id"
|
||||||
|
def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
|
||||||
|
with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
|
||||||
|
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
|
||||||
|
{_, true} <- {:is_create, activity.data["type"] == "Create"},
|
||||||
|
actor <- Activity.user_actor(activity),
|
||||||
|
{_, true} <- {:own_status, actor.id == user.id},
|
||||||
|
changes <- body_params |> put_application(conn),
|
||||||
|
{_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
|
||||||
|
{_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
|
||||||
|
try_render(conn, "show.json",
|
||||||
|
activity: activity,
|
||||||
|
for: user,
|
||||||
|
with_direct_conversation_id: true,
|
||||||
|
with_muted: Map.get(params, :with_muted, false)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
{:own_status, _} -> {:error, :forbidden}
|
||||||
|
{:pipeline, _} -> {:error, :internal_server_error}
|
||||||
|
_ -> {:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc "GET /api/v1/statuses/:id"
|
@doc "GET /api/v1/statuses/:id"
|
||||||
def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
|
def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
|
||||||
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
||||||
|
|
|
@ -19,7 +19,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||||
|
|
||||||
@parent_types ~w{Like Announce EmojiReact}
|
defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id
|
||||||
|
|
||||||
|
defp object_id_for(%{data: %{"object" => id}}) when is_binary(id), do: id
|
||||||
|
|
||||||
|
@parent_types ~w{Like Announce EmojiReact Update}
|
||||||
|
|
||||||
def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
|
def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
|
||||||
activities = Enum.map(notifications, & &1.activity)
|
activities = Enum.map(notifications, & &1.activity)
|
||||||
|
@ -30,7 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
||||||
%{data: %{"type" => type}} ->
|
%{data: %{"type" => type}} ->
|
||||||
type in @parent_types
|
type in @parent_types
|
||||||
end)
|
end)
|
||||||
|> Enum.map(& &1.data["object"])
|
|> Enum.map(&object_id_for/1)
|
||||||
|> Activity.create_by_object_ap_id()
|
|> Activity.create_by_object_ap_id()
|
||||||
|> Activity.with_preloaded_object(:left)
|
|> Activity.with_preloaded_object(:left)
|
||||||
|> Pleroma.Repo.all()
|
|> Pleroma.Repo.all()
|
||||||
|
@ -78,9 +82,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
||||||
|
|
||||||
parent_activity_fn = fn ->
|
parent_activity_fn = fn ->
|
||||||
if opts[:parent_activities] do
|
if opts[:parent_activities] do
|
||||||
Activity.Queries.find_by_object_ap_id(opts[:parent_activities], activity.data["object"])
|
Activity.Queries.find_by_object_ap_id(opts[:parent_activities], object_id_for(activity))
|
||||||
else
|
else
|
||||||
Activity.get_create_by_object_ap_id(activity.data["object"])
|
Activity.get_create_by_object_ap_id(object_id_for(activity))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -109,6 +113,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
||||||
"reblog" ->
|
"reblog" ->
|
||||||
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
|
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
|
||||||
|
|
||||||
|
"update" ->
|
||||||
|
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
|
||||||
|
|
||||||
"move" ->
|
"move" ->
|
||||||
put_target(response, activity, reading_user, %{})
|
put_target(response, activity, reading_user, %{})
|
||||||
|
|
||||||
|
|
|
@ -258,6 +258,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||||
|
|
||||||
created_at = Utils.to_masto_date(object.data["published"])
|
created_at = Utils.to_masto_date(object.data["published"])
|
||||||
|
|
||||||
|
edited_at =
|
||||||
|
with %{"updated" => updated} <- object.data,
|
||||||
|
date <- Utils.to_masto_date(updated),
|
||||||
|
true <- date != "" do
|
||||||
|
date
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
reply_to = get_reply_to(activity, opts)
|
reply_to = get_reply_to(activity, opts)
|
||||||
|
|
||||||
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
|
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
|
||||||
|
@ -344,8 +354,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||||
reblog: nil,
|
reblog: nil,
|
||||||
card: card,
|
card: card,
|
||||||
content: content_html,
|
content: content_html,
|
||||||
text: opts[:with_source] && object.data["source"],
|
text: opts[:with_source] && get_source_text(object.data["source"]),
|
||||||
created_at: created_at,
|
created_at: created_at,
|
||||||
|
edited_at: edited_at,
|
||||||
reblogs_count: announcement_count,
|
reblogs_count: announcement_count,
|
||||||
replies_count: object.data["repliesCount"] || 0,
|
replies_count: object.data["repliesCount"] || 0,
|
||||||
favourites_count: like_count,
|
favourites_count: like_count,
|
||||||
|
@ -384,6 +395,82 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
|
||||||
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
|
||||||
|
hashtags = Object.hashtags(object)
|
||||||
|
|
||||||
|
user = CommonAPI.get_user(activity.data["actor"])
|
||||||
|
|
||||||
|
past_history =
|
||||||
|
Object.history_for(object.data)
|
||||||
|
|> Map.get("orderedItems")
|
||||||
|
|> Enum.map(&Map.put(&1, "id", object.data["id"]))
|
||||||
|
|> Enum.map(&%Object{data: &1, id: object.id})
|
||||||
|
|
||||||
|
history = [object | past_history]
|
||||||
|
|
||||||
|
individual_opts =
|
||||||
|
opts
|
||||||
|
|> Map.put(:as, :object)
|
||||||
|
|> Map.put(:user, user)
|
||||||
|
|> Map.put(:hashtags, hashtags)
|
||||||
|
|
||||||
|
render_many(history, StatusView, "history_item.json", individual_opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(
|
||||||
|
"history_item.json",
|
||||||
|
%{activity: activity, user: user, object: object, hashtags: hashtags} = opts
|
||||||
|
) do
|
||||||
|
sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
|
||||||
|
|
||||||
|
attachment_data = object.data["attachment"] || []
|
||||||
|
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
|
||||||
|
|
||||||
|
created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])
|
||||||
|
|
||||||
|
content =
|
||||||
|
object
|
||||||
|
|> render_content()
|
||||||
|
|
||||||
|
content_html =
|
||||||
|
content
|
||||||
|
|> Activity.HTML.get_cached_scrubbed_html_for_activity(
|
||||||
|
User.html_filter_policy(opts[:for]),
|
||||||
|
activity,
|
||||||
|
"mastoapi:content"
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = object.data["summary"] || ""
|
||||||
|
|
||||||
|
%{
|
||||||
|
account:
|
||||||
|
AccountView.render("show.json", %{
|
||||||
|
user: user,
|
||||||
|
for: opts[:for]
|
||||||
|
}),
|
||||||
|
content: content_html,
|
||||||
|
sensitive: sensitive,
|
||||||
|
spoiler_text: summary,
|
||||||
|
created_at: created_at,
|
||||||
|
media_attachments: attachments,
|
||||||
|
emojis: build_emojis(object.data["emoji"]),
|
||||||
|
poll: render(PollView, "show.json", object: object, for: opts[:for])
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
|
||||||
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
|
||||||
|
%{
|
||||||
|
id: activity.id,
|
||||||
|
text: get_source_text(Map.get(object.data, "source", "")),
|
||||||
|
spoiler_text: Map.get(object.data, "summary", ""),
|
||||||
|
content_type: get_source_content_type(object.data["source"])
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
|
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
|
||||||
page_url_data = URI.parse(page_url)
|
page_url_data = URI.parse(page_url)
|
||||||
|
|
||||||
|
@ -436,10 +523,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||||
true -> "unknown"
|
true -> "unknown"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
attachment_id =
|
||||||
|
with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
|
||||||
|
{_, %Object{data: _object_data, id: object_id}} <-
|
||||||
|
{:object, Object.get_by_ap_id(ap_id)} do
|
||||||
|
to_string(object_id)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
<<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
|
<<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
|
||||||
|
to_string(attachment["id"] || hash_id)
|
||||||
|
end
|
||||||
|
|
||||||
%{
|
%{
|
||||||
id: to_string(attachment["id"] || hash_id),
|
id: attachment_id,
|
||||||
url: href,
|
url: href,
|
||||||
remote_url: href,
|
remote_url: href,
|
||||||
preview_url: href_preview,
|
preview_url: href_preview,
|
||||||
|
@ -601,4 +697,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_image_url(_, _), do: nil
|
defp build_image_url(_, _), do: nil
|
||||||
|
|
||||||
|
defp get_source_text(%{"content" => content} = _source) do
|
||||||
|
content
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_source_text(source) when is_binary(source) do
|
||||||
|
source
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_source_text(_) do
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_source_content_type(%{"mediaType" => type} = _source) do
|
||||||
|
type
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_source_content_type(_source) do
|
||||||
|
Utils.get_content_type(nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -552,6 +552,9 @@ defmodule Pleroma.Web.Router do
|
||||||
get("/bookmarks", StatusController, :bookmarks)
|
get("/bookmarks", StatusController, :bookmarks)
|
||||||
|
|
||||||
post("/statuses", StatusController, :create)
|
post("/statuses", StatusController, :create)
|
||||||
|
get("/statuses/:id/history", StatusController, :show_history)
|
||||||
|
get("/statuses/:id/source", StatusController, :show_source)
|
||||||
|
put("/statuses/:id", StatusController, :update)
|
||||||
delete("/statuses/:id", StatusController, :delete)
|
delete("/statuses/:id", StatusController, :delete)
|
||||||
post("/statuses/:id/reblog", StatusController, :reblog)
|
post("/statuses/:id/reblog", StatusController, :reblog)
|
||||||
post("/statuses/:id/unreblog", StatusController, :unreblog)
|
post("/statuses/:id/unreblog", StatusController, :unreblog)
|
||||||
|
|
|
@ -296,6 +296,20 @@ defmodule Pleroma.Web.Streamer do
|
||||||
|
|
||||||
defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
|
defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
|
||||||
|
|
||||||
|
defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do
|
||||||
|
anon_render = StreamerView.render("status_update.json", item)
|
||||||
|
|
||||||
|
Registry.dispatch(@registry, topic, fn list ->
|
||||||
|
Enum.each(list, fn {pid, auth?} ->
|
||||||
|
if auth? do
|
||||||
|
send(pid, {:render_with_user, StreamerView, "status_update.json", item})
|
||||||
|
else
|
||||||
|
send(pid, {:text, anon_render})
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp push_to_socket(topic, item) do
|
defp push_to_socket(topic, item) do
|
||||||
anon_render = StreamerView.render("update.json", item)
|
anon_render = StreamerView.render("update.json", item)
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,22 @@ defmodule Pleroma.Web.StreamerView do
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render("status_update.json", %Activity{} = activity, %User{} = user) do
|
||||||
|
activity = Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"])
|
||||||
|
|
||||||
|
%{
|
||||||
|
event: "status.update",
|
||||||
|
payload:
|
||||||
|
Pleroma.Web.MastodonAPI.StatusView.render(
|
||||||
|
"show.json",
|
||||||
|
activity: activity,
|
||||||
|
for: user
|
||||||
|
)
|
||||||
|
|> Jason.encode!()
|
||||||
|
}
|
||||||
|
|> Jason.encode!()
|
||||||
|
end
|
||||||
|
|
||||||
def render("notification.json", %Notification{} = notify, %User{} = user) do
|
def render("notification.json", %Notification{} = notify, %User{} = user) do
|
||||||
%{
|
%{
|
||||||
event: "notification",
|
event: "notification",
|
||||||
|
@ -51,6 +67,21 @@ defmodule Pleroma.Web.StreamerView do
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render("status_update.json", %Activity{} = activity) do
|
||||||
|
activity = Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"])
|
||||||
|
|
||||||
|
%{
|
||||||
|
event: "status.update",
|
||||||
|
payload:
|
||||||
|
Pleroma.Web.MastodonAPI.StatusView.render(
|
||||||
|
"show.json",
|
||||||
|
activity: activity
|
||||||
|
)
|
||||||
|
|> Jason.encode!()
|
||||||
|
}
|
||||||
|
|> Jason.encode!()
|
||||||
|
end
|
||||||
|
|
||||||
def render("chat_update.json", %{chat_message_reference: cm_ref}) do
|
def render("chat_update.json", %{chat_message_reference: cm_ref}) do
|
||||||
# Explicitly giving the cmr for the object here, so we don't accidentally
|
# Explicitly giving the cmr for the object here, so we don't accidentally
|
||||||
# send a later 'last_message' that was inserted between inserting this and
|
# send a later 'last_message' that was inserted between inserting this and
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.AddUpdateToNotificationsEnum do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
@disable_ddl_transaction true
|
||||||
|
|
||||||
|
def up do
|
||||||
|
"""
|
||||||
|
alter type notification_type add value 'update'
|
||||||
|
"""
|
||||||
|
|> execute()
|
||||||
|
end
|
||||||
|
|
||||||
|
# 20210717000000_add_poll_to_notifications_enum.exs
|
||||||
|
def down do
|
||||||
|
alter table(:notifications) do
|
||||||
|
modify(:type, :string)
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
delete from notifications where type = 'update'
|
||||||
|
"""
|
||||||
|
|> execute()
|
||||||
|
|
||||||
|
"""
|
||||||
|
drop type if exists notification_type
|
||||||
|
"""
|
||||||
|
|> execute()
|
||||||
|
|
||||||
|
"""
|
||||||
|
create type notification_type as enum (
|
||||||
|
'follow',
|
||||||
|
'follow_request',
|
||||||
|
'mention',
|
||||||
|
'move',
|
||||||
|
'pleroma:emoji_reaction',
|
||||||
|
'pleroma:chat_mention',
|
||||||
|
'reblog',
|
||||||
|
'favourite',
|
||||||
|
'pleroma:report',
|
||||||
|
'poll'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|> execute()
|
||||||
|
|
||||||
|
"""
|
||||||
|
alter table notifications
|
||||||
|
alter column type type notification_type using (type::notification_type)
|
||||||
|
"""
|
||||||
|
|> execute()
|
||||||
|
end
|
||||||
|
end
|
|
@ -36,7 +36,8 @@
|
||||||
"@id": "as:alsoKnownAs",
|
"@id": "as:alsoKnownAs",
|
||||||
"@type": "@id"
|
"@type": "@id"
|
||||||
},
|
},
|
||||||
"vcard": "http://www.w3.org/2006/vcard/ns#"
|
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
||||||
|
"formerRepresentations": "litepub:formerRepresentations"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,28 @@ defmodule Pleroma.NotificationTest do
|
||||||
subscriber_notifications = Notification.for_user(subscriber)
|
subscriber_notifications = Notification.for_user(subscriber)
|
||||||
assert Enum.empty?(subscriber_notifications)
|
assert Enum.empty?(subscriber_notifications)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it sends edited notifications to those who repeated a status" do
|
||||||
|
user = insert(:user)
|
||||||
|
repeated_user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity_one} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
status: "hey @#{other_user.nickname}!"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, _activity_two} = CommonAPI.repeat(activity_one.id, repeated_user)
|
||||||
|
|
||||||
|
{:ok, _edit_activity} =
|
||||||
|
CommonAPI.update(user, activity_one, %{
|
||||||
|
status: "hey @#{other_user.nickname}! mew mew"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert [%{type: "reblog"}] = Notification.for_user(user)
|
||||||
|
assert [%{type: "update"}] = Notification.for_user(repeated_user)
|
||||||
|
assert [%{type: "mention"}] = Notification.for_user(other_user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_poll_notifications/1" do
|
test "create_poll_notifications/1" do
|
||||||
|
@ -839,6 +861,30 @@ defmodule Pleroma.NotificationTest do
|
||||||
assert [other_user] == enabled_receivers
|
assert [other_user] == enabled_receivers
|
||||||
assert [] == disabled_receivers
|
assert [] == disabled_receivers
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it sends edited notifications to those who repeated a status" do
|
||||||
|
user = insert(:user)
|
||||||
|
repeated_user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity_one} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
status: "hey @#{other_user.nickname}!"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, _activity_two} = CommonAPI.repeat(activity_one.id, repeated_user)
|
||||||
|
|
||||||
|
{:ok, edit_activity} =
|
||||||
|
CommonAPI.update(user, activity_one, %{
|
||||||
|
status: "hey @#{other_user.nickname}! mew mew"
|
||||||
|
})
|
||||||
|
|
||||||
|
{enabled_receivers, _disabled_receivers} =
|
||||||
|
Notification.get_notified_from_activity(edit_activity)
|
||||||
|
|
||||||
|
assert repeated_user in enabled_receivers
|
||||||
|
assert other_user not in enabled_receivers
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "notification lifecycle" do
|
describe "notification lifecycle" do
|
||||||
|
|
|
@ -269,4 +269,191 @@ defmodule Pleroma.Object.FetcherTest do
|
||||||
refute called(Pleroma.Signature.sign(:_, :_))
|
refute called(Pleroma.Signature.sign(:_, :_))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "refetching" do
|
||||||
|
setup do
|
||||||
|
object1 = %{
|
||||||
|
"id" => "https://mastodon.social/1",
|
||||||
|
"actor" => "https://mastodon.social/users/emelie",
|
||||||
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "test 1",
|
||||||
|
"bcc" => [],
|
||||||
|
"bto" => [],
|
||||||
|
"cc" => [],
|
||||||
|
"to" => [],
|
||||||
|
"summary" => ""
|
||||||
|
}
|
||||||
|
|
||||||
|
object2 = %{
|
||||||
|
"id" => "https://mastodon.social/2",
|
||||||
|
"actor" => "https://mastodon.social/users/emelie",
|
||||||
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "test 2",
|
||||||
|
"bcc" => [],
|
||||||
|
"bto" => [],
|
||||||
|
"cc" => [],
|
||||||
|
"to" => [],
|
||||||
|
"summary" => "",
|
||||||
|
"formerRepresentations" => %{
|
||||||
|
"type" => "OrderedCollection",
|
||||||
|
"orderedItems" => [
|
||||||
|
%{
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "orig 2",
|
||||||
|
"actor" => "https://mastodon.social/users/emelie",
|
||||||
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
||||||
|
"bcc" => [],
|
||||||
|
"bto" => [],
|
||||||
|
"cc" => [],
|
||||||
|
"to" => [],
|
||||||
|
"summary" => ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"totalItems" => 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mock(fn
|
||||||
|
%{
|
||||||
|
method: :get,
|
||||||
|
url: "https://mastodon.social/1"
|
||||||
|
} ->
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
headers: [{"content-type", "application/activity+json"}],
|
||||||
|
body: Jason.encode!(object1)
|
||||||
|
}
|
||||||
|
|
||||||
|
%{
|
||||||
|
method: :get,
|
||||||
|
url: "https://mastodon.social/2"
|
||||||
|
} ->
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
headers: [{"content-type", "application/activity+json"}],
|
||||||
|
body: Jason.encode!(object2)
|
||||||
|
}
|
||||||
|
|
||||||
|
%{
|
||||||
|
method: :get,
|
||||||
|
url: "https://mastodon.social/users/emelie/collections/featured"
|
||||||
|
} ->
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
headers: [{"content-type", "application/activity+json"}],
|
||||||
|
body:
|
||||||
|
Jason.encode!(%{
|
||||||
|
"id" => "https://mastodon.social/users/emelie/collections/featured",
|
||||||
|
"type" => "OrderedCollection",
|
||||||
|
"actor" => "https://mastodon.social/users/emelie",
|
||||||
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
||||||
|
"orderedItems" => [],
|
||||||
|
"totalItems" => 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
env ->
|
||||||
|
apply(HttpRequestMock, :request, [env])
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{object1: object1, object2: object2}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
|
||||||
|
full_object1 =
|
||||||
|
object1
|
||||||
|
|> Map.merge(%{
|
||||||
|
"formerRepresentations" => %{
|
||||||
|
"type" => "OrderedCollection",
|
||||||
|
"orderedItems" => [
|
||||||
|
%{
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "orig 2",
|
||||||
|
"actor" => "https://mastodon.social/users/emelie",
|
||||||
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
||||||
|
"bcc" => [],
|
||||||
|
"bto" => [],
|
||||||
|
"cc" => [],
|
||||||
|
"to" => [],
|
||||||
|
"summary" => ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"totalItems" => 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, o} = Object.create(full_object1)
|
||||||
|
|
||||||
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
||||||
|
|
||||||
|
assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
|
||||||
|
refetched.data
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it uses formerRepresentations from remote if possible", %{object2: object2} do
|
||||||
|
{:ok, o} = Object.create(object2)
|
||||||
|
|
||||||
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
||||||
|
|
||||||
|
assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
|
||||||
|
refetched.data
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it replaces formerRepresentations with the one from remote", %{object2: object2} do
|
||||||
|
full_object2 =
|
||||||
|
object2
|
||||||
|
|> Map.merge(%{
|
||||||
|
"content" => "mew mew #def",
|
||||||
|
"formerRepresentations" => %{
|
||||||
|
"type" => "OrderedCollection",
|
||||||
|
"orderedItems" => [
|
||||||
|
%{"type" => "Note", "content" => "mew mew 2"}
|
||||||
|
],
|
||||||
|
"totalItems" => 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, o} = Object.create(full_object2)
|
||||||
|
|
||||||
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"content" => "test 2",
|
||||||
|
"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}
|
||||||
|
} = refetched.data
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it adds to formerRepresentations if the remote does not have one and the object has changed",
|
||||||
|
%{object1: object1} do
|
||||||
|
full_object1 =
|
||||||
|
object1
|
||||||
|
|> Map.merge(%{
|
||||||
|
"content" => "mew mew #def",
|
||||||
|
"formerRepresentations" => %{
|
||||||
|
"type" => "OrderedCollection",
|
||||||
|
"orderedItems" => [
|
||||||
|
%{"type" => "Note", "content" => "mew mew 1"}
|
||||||
|
],
|
||||||
|
"totalItems" => 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, o} = Object.create(full_object1)
|
||||||
|
|
||||||
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"content" => "test 1",
|
||||||
|
"formerRepresentations" => %{
|
||||||
|
"orderedItems" => [
|
||||||
|
%{"content" => "mew mew #def"},
|
||||||
|
%{"content" => "mew mew 1"}
|
||||||
|
],
|
||||||
|
"totalItems" => 2
|
||||||
|
}
|
||||||
|
} = refetched.data
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -49,9 +49,11 @@ defmodule Pleroma.UploadTest do
|
||||||
test "it returns file" do
|
test "it returns file" do
|
||||||
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
|
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
|
||||||
|
|
||||||
assert Upload.store(@upload_file) ==
|
assert {:ok, result} = Upload.store(@upload_file)
|
||||||
{:ok,
|
|
||||||
|
assert result ==
|
||||||
%{
|
%{
|
||||||
|
"id" => result["id"],
|
||||||
"name" => "image.jpg",
|
"name" => "image.jpg",
|
||||||
"type" => "Document",
|
"type" => "Document",
|
||||||
"mediaType" => "image/jpeg",
|
"mediaType" => "image/jpeg",
|
||||||
|
@ -62,7 +64,7 @@ defmodule Pleroma.UploadTest do
|
||||||
"type" => "Link"
|
"type" => "Link"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}}
|
}
|
||||||
|
|
||||||
Task.await(Agent.get(TestUploaderSuccess, fn task_pid -> task_pid end))
|
Task.await(Agent.get(TestUploaderSuccess, fn task_pid -> task_pid end))
|
||||||
end
|
end
|
||||||
|
|
|
@ -60,7 +60,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
|
||||||
|> File.exists?()
|
|> File.exists?()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "reject shortcode", %{message: message} do
|
test "reject regex shortcode", %{message: message} do
|
||||||
refute "firedfox" in installed()
|
refute "firedfox" in installed()
|
||||||
|
|
||||||
clear_config(:mrf_steal_emoji,
|
clear_config(:mrf_steal_emoji,
|
||||||
|
@ -74,6 +74,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
|
||||||
refute "firedfox" in installed()
|
refute "firedfox" in installed()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "reject string shortcode", %{message: message} do
|
||||||
|
refute "firedfox" in installed()
|
||||||
|
|
||||||
|
clear_config(:mrf_steal_emoji,
|
||||||
|
hosts: ["example.org"],
|
||||||
|
size_limit: 284_468,
|
||||||
|
rejected_shortcodes: ["firedfox"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, _message} = StealEmojiPolicy.filter(message)
|
||||||
|
|
||||||
|
refute "firedfox" in installed()
|
||||||
|
end
|
||||||
|
|
||||||
test "reject if size is above the limit", %{message: message} do
|
test "reject if size is above the limit", %{message: message} do
|
||||||
refute "firedfox" in installed()
|
refute "firedfox" in installed()
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
|
||||||
test "returns an error if the object can't be updated by the actor", %{
|
test "returns an error if the object can't be updated by the actor", %{
|
||||||
valid_update: valid_update
|
valid_update: valid_update
|
||||||
} do
|
} do
|
||||||
other_user = insert(:user)
|
other_user = insert(:user, local: false)
|
||||||
|
|
||||||
update =
|
update =
|
||||||
valid_update
|
valid_update
|
||||||
|
@ -40,5 +40,27 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
|
||||||
|
|
||||||
assert {:error, _cng} = ObjectValidator.validate(update, [])
|
assert {:error, _cng} = ObjectValidator.validate(update, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "validates as long as the object is same-origin with the actor", %{
|
||||||
|
valid_update: valid_update
|
||||||
|
} do
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
update =
|
||||||
|
valid_update
|
||||||
|
|> Map.put("actor", other_user.ap_id)
|
||||||
|
|
||||||
|
assert {:ok, _update, []} = ObjectValidator.validate(update, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates if the object is not of an Actor type" do
|
||||||
|
note = insert(:note)
|
||||||
|
updated_note = note.data |> Map.put("content", "edited content")
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, update, _} = Builder.update(other_user, updated_note)
|
||||||
|
|
||||||
|
assert {:ok, _update, []} = ObjectValidator.validate(update, [])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -140,6 +140,127 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "update notes" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
note = insert(:note, user: user)
|
||||||
|
_note_activity = insert(:note_activity, note: note)
|
||||||
|
|
||||||
|
updated_note =
|
||||||
|
note.data
|
||||||
|
|> Map.put("summary", "edited summary")
|
||||||
|
|> Map.put("content", "edited content")
|
||||||
|
|
||||||
|
{:ok, update_data, []} = Builder.update(user, updated_note)
|
||||||
|
{:ok, update, _meta} = ActivityPub.persist(update_data, local: true)
|
||||||
|
|
||||||
|
%{user: user, note: note, object_id: note.id, update_data: update_data, update: update}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it updates the note", %{object_id: object_id, update: update} do
|
||||||
|
{:ok, _, _} = SideEffects.handle(update)
|
||||||
|
new_note = Pleroma.Object.get_by_id(object_id)
|
||||||
|
assert %{"summary" => "edited summary", "content" => "edited content"} = new_note.data
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it records the original note in formerRepresentations", %{
|
||||||
|
note: note,
|
||||||
|
object_id: object_id,
|
||||||
|
update: update
|
||||||
|
} do
|
||||||
|
{:ok, _, _} = SideEffects.handle(update)
|
||||||
|
%{data: new_note} = Pleroma.Object.get_by_id(object_id)
|
||||||
|
assert %{"summary" => "edited summary", "content" => "edited content"} = new_note
|
||||||
|
|
||||||
|
assert [Map.drop(note.data, ["id", "formerRepresentations"])] ==
|
||||||
|
new_note["formerRepresentations"]["orderedItems"]
|
||||||
|
|
||||||
|
assert new_note["formerRepresentations"]["totalItems"] == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it puts the original note at the front of formerRepresentations", %{
|
||||||
|
user: user,
|
||||||
|
note: note,
|
||||||
|
object_id: object_id,
|
||||||
|
update: update
|
||||||
|
} do
|
||||||
|
{:ok, _, _} = SideEffects.handle(update)
|
||||||
|
%{data: first_edit} = Pleroma.Object.get_by_id(object_id)
|
||||||
|
|
||||||
|
second_updated_note =
|
||||||
|
note.data
|
||||||
|
|> Map.put("summary", "edited summary 2")
|
||||||
|
|> Map.put("content", "edited content 2")
|
||||||
|
|
||||||
|
{:ok, second_update_data, []} = Builder.update(user, second_updated_note)
|
||||||
|
{:ok, update, _meta} = ActivityPub.persist(second_update_data, local: true)
|
||||||
|
{:ok, _, _} = SideEffects.handle(update)
|
||||||
|
%{data: new_note} = Pleroma.Object.get_by_id(object_id)
|
||||||
|
assert %{"summary" => "edited summary 2", "content" => "edited content 2"} = new_note
|
||||||
|
|
||||||
|
original_version = Map.drop(note.data, ["id", "formerRepresentations"])
|
||||||
|
first_edit = Map.drop(first_edit, ["id", "formerRepresentations"])
|
||||||
|
|
||||||
|
assert [first_edit, original_version] ==
|
||||||
|
new_note["formerRepresentations"]["orderedItems"]
|
||||||
|
|
||||||
|
assert new_note["formerRepresentations"]["totalItems"] == 2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it does not prepend to formerRepresentations if no actual changes are made", %{
|
||||||
|
note: note,
|
||||||
|
object_id: object_id,
|
||||||
|
update: update
|
||||||
|
} do
|
||||||
|
{:ok, _, _} = SideEffects.handle(update)
|
||||||
|
%{data: _first_edit} = Pleroma.Object.get_by_id(object_id)
|
||||||
|
|
||||||
|
{:ok, _, _} = SideEffects.handle(update)
|
||||||
|
%{data: new_note} = Pleroma.Object.get_by_id(object_id)
|
||||||
|
assert %{"summary" => "edited summary", "content" => "edited content"} = new_note
|
||||||
|
|
||||||
|
original_version = Map.drop(note.data, ["id", "formerRepresentations"])
|
||||||
|
|
||||||
|
assert [original_version] ==
|
||||||
|
new_note["formerRepresentations"]["orderedItems"]
|
||||||
|
|
||||||
|
assert new_note["formerRepresentations"]["totalItems"] == 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update questions" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
question = insert(:question, user: user)
|
||||||
|
|
||||||
|
%{user: user, data: question.data, id: question.id}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows updating choice count without generating edit history", %{
|
||||||
|
user: user,
|
||||||
|
data: data,
|
||||||
|
id: id
|
||||||
|
} do
|
||||||
|
new_choices =
|
||||||
|
data["oneOf"]
|
||||||
|
|> Enum.map(fn choice -> put_in(choice, ["replies", "totalItems"], 5) end)
|
||||||
|
|
||||||
|
updated_question = data |> Map.put("oneOf", new_choices)
|
||||||
|
|
||||||
|
{:ok, update_data, []} = Builder.update(user, updated_question)
|
||||||
|
{:ok, update, _meta} = ActivityPub.persist(update_data, local: true)
|
||||||
|
|
||||||
|
{:ok, _, _} = SideEffects.handle(update)
|
||||||
|
|
||||||
|
%{data: new_question} = Pleroma.Object.get_by_id(id)
|
||||||
|
|
||||||
|
assert [%{"replies" => %{"totalItems" => 5}}, %{"replies" => %{"totalItems" => 5}}] =
|
||||||
|
new_question["oneOf"]
|
||||||
|
|
||||||
|
refute Map.has_key?(new_question, "formerRepresentations")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "EmojiReact objects" do
|
describe "EmojiReact objects" do
|
||||||
setup do
|
setup do
|
||||||
poster = insert(:user)
|
poster = insert(:user)
|
||||||
|
|
|
@ -575,4 +575,58 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|
||||||
assert Transmogrifier.fix_attachments(object) == expected
|
assert Transmogrifier.fix_attachments(object) == expected
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "strip_internal_fields/1" do
|
||||||
|
test "it strips internal fields in formerRepresentations" do
|
||||||
|
original = %{
|
||||||
|
"formerRepresentations" => %{
|
||||||
|
"orderedItems" => [
|
||||||
|
%{"generator" => %{}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stripped = Transmogrifier.strip_internal_fields(original)
|
||||||
|
|
||||||
|
refute Map.has_key?(
|
||||||
|
Enum.at(stripped["formerRepresentations"]["orderedItems"], 0),
|
||||||
|
"generator"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it strips internal fields in maybe badly-formed formerRepresentations" do
|
||||||
|
original = %{
|
||||||
|
"formerRepresentations" => %{
|
||||||
|
"orderedItems" => [
|
||||||
|
%{"generator" => %{}},
|
||||||
|
"https://example.com/1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stripped = Transmogrifier.strip_internal_fields(original)
|
||||||
|
|
||||||
|
refute Map.has_key?(
|
||||||
|
Enum.at(stripped["formerRepresentations"]["orderedItems"], 0),
|
||||||
|
"generator"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Enum.at(stripped["formerRepresentations"]["orderedItems"], 1) ==
|
||||||
|
"https://example.com/1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it ignores if formerRepresentations does not look like an OrderedCollection" do
|
||||||
|
original = %{
|
||||||
|
"formerRepresentations" => %{
|
||||||
|
"items" => [
|
||||||
|
%{"generator" => %{}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stripped = Transmogrifier.strip_internal_fields(original)
|
||||||
|
|
||||||
|
assert Map.has_key?(Enum.at(stripped["formerRepresentations"]["items"], 0), "generator")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -586,7 +586,7 @@ defmodule Pleroma.Web.CommonAPITest do
|
||||||
object = Object.normalize(activity, fetch: false)
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
|
||||||
assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
|
assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
|
||||||
assert object.data["source"] == post
|
assert object.data["source"]["content"] == post
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it filters out obviously bad tags when accepting a post as Markdown" do
|
test "it filters out obviously bad tags when accepting a post as Markdown" do
|
||||||
|
@ -603,7 +603,7 @@ defmodule Pleroma.Web.CommonAPITest do
|
||||||
object = Object.normalize(activity, fetch: false)
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
|
||||||
assert object.data["content"] == "<p><b>2hu</b></p>"
|
assert object.data["content"] == "<p><b>2hu</b></p>"
|
||||||
assert object.data["source"] == post
|
assert object.data["source"]["content"] == post
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not allow replies to direct messages that are not direct messages themselves" do
|
test "it does not allow replies to direct messages that are not direct messages themselves" do
|
||||||
|
@ -1541,4 +1541,33 @@ defmodule Pleroma.Web.CommonAPITest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "update/3" do
|
||||||
|
test "updates a post" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1"})
|
||||||
|
|
||||||
|
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
|
||||||
|
|
||||||
|
updated_object = Object.normalize(updated)
|
||||||
|
assert updated_object.data["content"] == "updated 2"
|
||||||
|
assert Map.get(updated_object.data, "summary", "") == ""
|
||||||
|
assert Map.has_key?(updated_object.data, "updated")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not change visibility" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1", visibility: "private"})
|
||||||
|
|
||||||
|
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
|
||||||
|
|
||||||
|
updated_object = Object.normalize(updated)
|
||||||
|
assert updated_object.data["content"] == "updated 2"
|
||||||
|
assert Map.get(updated_object.data, "summary", "") == ""
|
||||||
|
assert Visibility.get_visibility(updated_object) == "private"
|
||||||
|
assert Visibility.get_visibility(updated) == "private"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1990,4 +1990,164 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
||||||
} = result
|
} = result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "get status history" do
|
||||||
|
setup do
|
||||||
|
oauth_access(["read:statuses"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "unedited post", %{conn: conn} do
|
||||||
|
activity = insert(:note_activity)
|
||||||
|
|
||||||
|
conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
|
||||||
|
|
||||||
|
assert [_] = json_response_and_validate_schema(conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "edited post", %{conn: conn} do
|
||||||
|
note =
|
||||||
|
insert(
|
||||||
|
:note,
|
||||||
|
data: %{
|
||||||
|
"formerRepresentations" => %{
|
||||||
|
"type" => "OrderedCollection",
|
||||||
|
"orderedItems" => [
|
||||||
|
%{
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "mew mew 2",
|
||||||
|
"summary" => "title 2"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "mew mew 1",
|
||||||
|
"summary" => "title 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"totalItems" => 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
activity = insert(:note_activity, note: note)
|
||||||
|
|
||||||
|
conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
|
||||||
|
|
||||||
|
assert [_, %{"spoiler_text" => "title 2"}, %{"spoiler_text" => "title 1"}] =
|
||||||
|
json_response_and_validate_schema(conn, 200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get status source" do
|
||||||
|
setup do
|
||||||
|
oauth_access(["read:statuses"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns the source", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
|
||||||
|
|
||||||
|
conn = get(conn, "/api/v1/statuses/#{activity.id}/source")
|
||||||
|
|
||||||
|
id = activity.id
|
||||||
|
|
||||||
|
assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} =
|
||||||
|
json_response_and_validate_schema(conn, 200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update status" do
|
||||||
|
setup do
|
||||||
|
oauth_access(["write:statuses"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it updates the status", %{conn: conn, user: user} do
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put("/api/v1/statuses/#{activity.id}", %{
|
||||||
|
"status" => "edited",
|
||||||
|
"spoiler_text" => "lol"
|
||||||
|
})
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert response["content"] == "edited"
|
||||||
|
assert response["spoiler_text"] == "lol"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it updates the attachments", %{conn: conn, user: user} do
|
||||||
|
attachment = insert(:attachment, user: user)
|
||||||
|
attachment_id = to_string(attachment.id)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put("/api/v1/statuses/#{activity.id}", %{
|
||||||
|
"status" => "mew mew #abc",
|
||||||
|
"spoiler_text" => "#def",
|
||||||
|
"media_ids" => [attachment_id]
|
||||||
|
})
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert [%{"id" => ^attachment_id}] = response["media_attachments"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it does not update visibility", %{conn: conn, user: user} do
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
status: "mew mew #abc",
|
||||||
|
spoiler_text: "#def",
|
||||||
|
visibility: "private"
|
||||||
|
})
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put("/api/v1/statuses/#{activity.id}", %{
|
||||||
|
"status" => "edited",
|
||||||
|
"spoiler_text" => "lol"
|
||||||
|
})
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert response["visibility"] == "private"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it refuses to update when original post is not by the user", %{conn: conn} do
|
||||||
|
another_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(another_user, %{status: "mew mew #abc", spoiler_text: "#def"})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put("/api/v1/statuses/#{activity.id}", %{
|
||||||
|
"status" => "edited",
|
||||||
|
"spoiler_text" => "lol"
|
||||||
|
})
|
||||||
|
|> json_response_and_validate_schema(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns 404 if the user cannot see the post", %{conn: conn} do
|
||||||
|
another_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(another_user, %{
|
||||||
|
status: "mew mew #abc",
|
||||||
|
spoiler_text: "#def",
|
||||||
|
visibility: "private"
|
||||||
|
})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put("/api/v1/statuses/#{activity.id}", %{
|
||||||
|
"status" => "edited",
|
||||||
|
"spoiler_text" => "lol"
|
||||||
|
})
|
||||||
|
|> json_response_and_validate_schema(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -237,6 +237,32 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
||||||
test_notifications_rendering([notification], moderator_user, [expected])
|
test_notifications_rendering([notification], moderator_user, [expected])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "Edit notification" do
|
||||||
|
user = insert(:user)
|
||||||
|
repeat_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "mew"})
|
||||||
|
{:ok, _} = CommonAPI.repeat(activity.id, repeat_user)
|
||||||
|
{:ok, update} = CommonAPI.update(user, activity, %{status: "mew mew"})
|
||||||
|
|
||||||
|
user = Pleroma.User.get_by_ap_id(user.ap_id)
|
||||||
|
activity = Pleroma.Activity.normalize(activity)
|
||||||
|
update = Pleroma.Activity.normalize(update)
|
||||||
|
|
||||||
|
{:ok, [notification]} = Notification.create_notifications(update)
|
||||||
|
|
||||||
|
expected = %{
|
||||||
|
id: to_string(notification.id),
|
||||||
|
pleroma: %{is_seen: false, is_muted: false},
|
||||||
|
type: "update",
|
||||||
|
account: AccountView.render("show.json", %{user: user, for: repeat_user}),
|
||||||
|
created_at: Utils.to_masto_date(notification.inserted_at),
|
||||||
|
status: StatusView.render("show.json", %{activity: activity, for: repeat_user})
|
||||||
|
}
|
||||||
|
|
||||||
|
test_notifications_rendering([notification], repeat_user, [expected])
|
||||||
|
end
|
||||||
|
|
||||||
test "muted notification" do
|
test "muted notification" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
another_user = insert(:user)
|
another_user = insert(:user)
|
||||||
|
|
|
@ -246,6 +246,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
||||||
content: HTML.filter_tags(object_data["content"]),
|
content: HTML.filter_tags(object_data["content"]),
|
||||||
text: nil,
|
text: nil,
|
||||||
created_at: created_at,
|
created_at: created_at,
|
||||||
|
edited_at: nil,
|
||||||
reblogs_count: 0,
|
reblogs_count: 0,
|
||||||
replies_count: 0,
|
replies_count: 0,
|
||||||
favourites_count: 0,
|
favourites_count: 0,
|
||||||
|
@ -708,4 +709,55 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
||||||
status = StatusView.render("show.json", activity: visible, for: poster)
|
status = StatusView.render("show.json", activity: visible, for: poster)
|
||||||
assert status.pleroma.parent_visible
|
assert status.pleroma.parent_visible
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it shows edited_at" do
|
||||||
|
poster = insert(:user)
|
||||||
|
|
||||||
|
{:ok, post} = CommonAPI.post(poster, %{status: "hey"})
|
||||||
|
|
||||||
|
status = StatusView.render("show.json", activity: post)
|
||||||
|
refute status.edited_at
|
||||||
|
|
||||||
|
{:ok, _} = CommonAPI.update(poster, post, %{status: "mew mew"})
|
||||||
|
edited = Pleroma.Activity.normalize(post)
|
||||||
|
|
||||||
|
status = StatusView.render("show.json", activity: edited)
|
||||||
|
assert status.edited_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "with a source object" do
|
||||||
|
note =
|
||||||
|
insert(:note,
|
||||||
|
data: %{"source" => %{"content" => "object source", "mediaType" => "text/markdown"}}
|
||||||
|
)
|
||||||
|
|
||||||
|
activity = insert(:note_activity, note: note)
|
||||||
|
|
||||||
|
status = StatusView.render("show.json", activity: activity, with_source: true)
|
||||||
|
assert status.text == "object source"
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "source.json" do
|
||||||
|
test "with a source object, renders both source and content type" do
|
||||||
|
note =
|
||||||
|
insert(:note,
|
||||||
|
data: %{"source" => %{"content" => "object source", "mediaType" => "text/markdown"}}
|
||||||
|
)
|
||||||
|
|
||||||
|
activity = insert(:note_activity, note: note)
|
||||||
|
|
||||||
|
status = StatusView.render("source.json", activity: activity)
|
||||||
|
assert status.text == "object source"
|
||||||
|
assert status.content_type == "text/markdown"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "with a source string, renders source and put text/plain as the content type" do
|
||||||
|
note = insert(:note, data: %{"source" => "string source"})
|
||||||
|
activity = insert(:note_activity, note: note)
|
||||||
|
|
||||||
|
status = StatusView.render("source.json", activity: activity)
|
||||||
|
assert status.text == "string source"
|
||||||
|
assert status.content_type == "text/plain"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -442,6 +442,31 @@ defmodule Pleroma.Web.StreamerTest do
|
||||||
"state" => "follow_accept"
|
"state" => "follow_accept"
|
||||||
} = Jason.decode!(payload)
|
} = Jason.decode!(payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it streams edits in the 'user' stream", %{user: user, token: oauth_token} do
|
||||||
|
sender = insert(:user)
|
||||||
|
{:ok, _, _, _} = CommonAPI.follow(user, sender)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(sender, %{status: "hey"})
|
||||||
|
|
||||||
|
Streamer.get_topic_and_add_socket("user", user, oauth_token)
|
||||||
|
{:ok, edited} = CommonAPI.update(sender, activity, %{status: "mew mew"})
|
||||||
|
edited = Pleroma.Activity.normalize(edited)
|
||||||
|
|
||||||
|
assert_receive {:render_with_user, _, "status_update.json", ^edited}
|
||||||
|
refute Streamer.filtered_by_user?(user, edited)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it streams own edits in the 'user' stream", %{user: user, token: oauth_token} do
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
|
||||||
|
|
||||||
|
Streamer.get_topic_and_add_socket("user", user, oauth_token)
|
||||||
|
{:ok, edited} = CommonAPI.update(user, activity, %{status: "mew mew"})
|
||||||
|
edited = Pleroma.Activity.normalize(edited)
|
||||||
|
|
||||||
|
assert_receive {:render_with_user, _, "status_update.json", ^edited}
|
||||||
|
refute Streamer.filtered_by_user?(user, edited)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "public streams" do
|
describe "public streams" do
|
||||||
|
@ -484,6 +509,25 @@ defmodule Pleroma.Web.StreamerTest do
|
||||||
assert_receive {:text, event}
|
assert_receive {:text, event}
|
||||||
assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event)
|
assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it streams edits in the 'public' stream" do
|
||||||
|
sender = insert(:user)
|
||||||
|
|
||||||
|
Streamer.get_topic_and_add_socket("public", nil, nil)
|
||||||
|
{:ok, activity} = CommonAPI.post(sender, %{status: "hey"})
|
||||||
|
assert_receive {:text, _}
|
||||||
|
|
||||||
|
{:ok, edited} = CommonAPI.update(sender, activity, %{status: "mew mew"})
|
||||||
|
|
||||||
|
edited = Pleroma.Activity.normalize(edited)
|
||||||
|
|
||||||
|
%{id: activity_id} = Pleroma.Activity.get_create_by_object_ap_id(edited.object.data["id"])
|
||||||
|
|
||||||
|
assert_receive {:text, event}
|
||||||
|
assert %{"event" => "status.update", "payload" => payload} = Jason.decode!(event)
|
||||||
|
assert %{"id" => ^activity_id} = Jason.decode!(payload)
|
||||||
|
refute Streamer.filtered_by_user?(sender, edited)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "thread_containment/2" do
|
describe "thread_containment/2" do
|
||||||
|
|
|
@ -111,6 +111,18 @@ defmodule Pleroma.Factory do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attachment_factory(attrs \\ %{}) do
|
||||||
|
user = attrs[:user] || insert(:user)
|
||||||
|
|
||||||
|
data =
|
||||||
|
attachment_data(user.ap_id, nil)
|
||||||
|
|> Map.put("id", Pleroma.Web.ActivityPub.Utils.generate_object_id())
|
||||||
|
|
||||||
|
%Pleroma.Object{
|
||||||
|
data: merge_attributes(data, Map.get(attrs, :data, %{}))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def attachment_note_factory(attrs \\ %{}) do
|
def attachment_note_factory(attrs \\ %{}) do
|
||||||
user = attrs[:user] || insert(:user)
|
user = attrs[:user] || insert(:user)
|
||||||
{length, attrs} = Map.pop(attrs, :length, 1)
|
{length, attrs} = Map.pop(attrs, :length, 1)
|
||||||
|
|
Loading…
Reference in New Issue