forked from AkkomaGang/akkoma
Announcements: Handle through common pipeline.
This commit is contained in:
parent
c7cdc553ff
commit
e42bc5f557
11 changed files with 135 additions and 79 deletions
|
@ -356,36 +356,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
|
|
||||||
{:ok, Activity.t(), Object.t()} | {:error, any()}
|
|
||||||
def announce(
|
|
||||||
%User{ap_id: _} = user,
|
|
||||||
%Object{data: %{"id" => _}} = object,
|
|
||||||
activity_id \\ nil,
|
|
||||||
local \\ true,
|
|
||||||
public \\ true
|
|
||||||
) do
|
|
||||||
with {:ok, result} <-
|
|
||||||
Repo.transaction(fn -> do_announce(user, object, activity_id, local, public) end) do
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_announce(user, object, activity_id, local, public) do
|
|
||||||
with true <- is_announceable?(object, user, public),
|
|
||||||
object <- Object.get_by_id(object.id),
|
|
||||||
announce_data <- make_announce_data(user, object, activity_id, public),
|
|
||||||
{:ok, activity} <- insert(announce_data, local),
|
|
||||||
{:ok, object} <- add_announce_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 follow(User.t(), User.t(), String.t() | nil, boolean()) ::
|
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
|
||||||
{:ok, Activity.t()} | {:error, any()}
|
{:ok, Activity.t()} | {:error, any()}
|
||||||
def follow(follower, followed, activity_id \\ nil, local \\ true) do
|
def follow(follower, followed, activity_id \\ nil, local \\ true) do
|
||||||
|
|
|
@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
||||||
def emoji_react(actor, object, emoji) do
|
def emoji_react(actor, object, emoji) do
|
||||||
with {:ok, data, meta} <- object_action(actor, object) do
|
with {:ok, data, meta} <- object_action(actor, object) do
|
||||||
|
@ -83,9 +85,17 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def announce(actor, object) do
|
def announce(actor, object, options \\ []) do
|
||||||
|
public? = Keyword.get(options, :public, false)
|
||||||
to = [actor.follower_address, object.data["actor"]]
|
to = [actor.follower_address, object.data["actor"]]
|
||||||
|
|
||||||
|
to =
|
||||||
|
if public? do
|
||||||
|
[Pleroma.Constants.as_public() | to]
|
||||||
|
else
|
||||||
|
to
|
||||||
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
"id" => Utils.generate_activity_id(),
|
"id" => Utils.generate_activity_id(),
|
||||||
|
@ -93,7 +103,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
||||||
"object" => object.data["id"],
|
"object" => object.data["id"],
|
||||||
"to" => to,
|
"to" => to,
|
||||||
"context" => object.data["context"],
|
"context" => object.data["context"],
|
||||||
"type" => "Announce"
|
"type" => "Announce",
|
||||||
|
"published" => Utils.make_date()
|
||||||
}, []}
|
}, []}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
||||||
|
|
||||||
def fetch_actor_and_object(object) do
|
def fetch_actor_and_object(object) do
|
||||||
fetch_actor(object)
|
fetch_actor(object)
|
||||||
Object.normalize(object["object"])
|
Object.normalize(object["object"], true)
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,9 +18,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
|
||||||
field(:type, :string)
|
field(:type, :string)
|
||||||
field(:object, Types.ObjectID)
|
field(:object, Types.ObjectID)
|
||||||
field(:actor, Types.ObjectID)
|
field(:actor, Types.ObjectID)
|
||||||
field(:context, :string)
|
field(:context, :string, autogenerate: {Utils, :generate_context_id, []})
|
||||||
field(:to, Types.Recipients, default: [])
|
field(:to, Types.Recipients, default: [])
|
||||||
field(:cc, Types.Recipients, default: [])
|
field(:cc, Types.Recipients, default: [])
|
||||||
|
field(:published, Types.DateTime)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cast_and_validate(data) do
|
def cast_and_validate(data) do
|
||||||
|
@ -47,7 +48,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
|
||||||
def validate_data(data_cng) do
|
def validate_data(data_cng) do
|
||||||
data_cng
|
data_cng
|
||||||
|> validate_inclusion(:type, ["Announce"])
|
|> validate_inclusion(:type, ["Announce"])
|
||||||
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc])
|
|> validate_required([:id, :type, :object, :actor, :to, :cc])
|
||||||
|> validate_actor_presence()
|
|> validate_actor_presence()
|
||||||
|> validate_object_presence()
|
|> validate_object_presence()
|
||||||
|> validate_existing_announce()
|
|> validate_existing_announce()
|
||||||
|
|
|
@ -27,6 +27,18 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
{:ok, object, meta}
|
{:ok, object, meta}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Tasks this handles:
|
||||||
|
# - Add announce to object
|
||||||
|
# - Set up notification
|
||||||
|
def handle(%{data: %{"type" => "Announce"}} = object, meta) do
|
||||||
|
announced_object = Object.get_by_ap_id(object.data["object"])
|
||||||
|
Utils.add_announce_to_object(object, announced_object)
|
||||||
|
|
||||||
|
Notification.create_notifications(object)
|
||||||
|
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
|
||||||
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
|
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
|
||||||
with undone_object <- Activity.get_by_ap_id(undone_object),
|
with undone_object <- Activity.get_by_ap_id(undone_object),
|
||||||
:ok <- handle_undoing(undone_object) do
|
:ok <- handle_undoing(undone_object) do
|
||||||
|
|
|
@ -662,7 +662,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
|> handle_incoming(options)
|
|> handle_incoming(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do
|
def handle_incoming(%{"type" => type} = data, _options)
|
||||||
|
when type in ["Like", "EmojiReact", "Announce"] do
|
||||||
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
||||||
{:ok, activity, _meta} <-
|
{:ok, activity, _meta} <-
|
||||||
Pipeline.common_pipeline(data, local: false) do
|
Pipeline.common_pipeline(data, local: false) do
|
||||||
|
@ -672,22 +673,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
|
||||||
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
|
|
||||||
_options
|
|
||||||
) do
|
|
||||||
with actor <- Containment.get_actor(data),
|
|
||||||
{_, {:ok, %User{} = actor}} <- {:fetch_user, User.get_or_fetch_by_ap_id(actor)},
|
|
||||||
{_, {:ok, object}} <- {:get_embedded, get_embedded_obj_helper(object_id, actor)},
|
|
||||||
public <- Visibility.is_public?(data),
|
|
||||||
{_, {:ok, activity, _object}} <-
|
|
||||||
{:announce, ActivityPub.announce(actor, object, id, false, public)} do
|
|
||||||
{:ok, activity}
|
|
||||||
else
|
|
||||||
e -> {:error, e}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
|
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -127,18 +127,19 @@ defmodule Pleroma.Web.CommonAPI do
|
||||||
end
|
end
|
||||||
|
|
||||||
def repeat(id, user, params \\ %{}) do
|
def repeat(id, user, params \\ %{}) do
|
||||||
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id) do
|
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
|
||||||
object = Object.normalize(activity)
|
object = %Object{} <- Object.normalize(activity, false),
|
||||||
announce_activity = Utils.get_existing_announce(user.ap_id, object)
|
{_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
|
||||||
public = public_announce?(object, params)
|
public = public_announce?(object, params),
|
||||||
|
{:ok, announce, _} <- Builder.announce(user, object, public: public),
|
||||||
|
{:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
|
||||||
|
{:ok, activity}
|
||||||
|
else
|
||||||
|
{:existing_announce, %Activity{} = announce} ->
|
||||||
|
{:ok, announce}
|
||||||
|
|
||||||
if announce_activity do
|
_ ->
|
||||||
{:ok, announce_activity, object}
|
{:error, :not_found}
|
||||||
else
|
|
||||||
ActivityPub.announce(user, object, nil, true, public)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
_ -> {:error, :not_found}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
46
test/fixtures/mastodon-note-object.json
vendored
46
test/fixtures/mastodon-note-object.json
vendored
|
@ -1,9 +1,45 @@
|
||||||
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"http://mastodon.example.org/users/admin/statuses/99541947525187367","type":"Note","summary":null,"content":"\u003cp\u003eyeah.\u003c/p\u003e","inReplyTo":null,"published":"2018-02-17T17:46:20Z","url":"http://mastodon.example.org/@admin/99541947525187367","attributedTo":"http://mastodon.example.org/users/admin","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["http://mastodon.example.org/users/admin/followers"],"sensitive":false,"atomUri":"http://mastodon.example.org/users/admin/statuses/99541947525187367","inReplyToAtomUri":null,"conversation":"tag:mastodon.example.org,2018-02-17:objectId=59:objectType=Conversation","tag":[],
|
{
|
||||||
|
"@context" : [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{
|
||||||
|
"Emoji" : "toot:Emoji",
|
||||||
|
"Hashtag" : "as:Hashtag",
|
||||||
|
"atomUri" : "ostatus:atomUri",
|
||||||
|
"conversation" : "ostatus:conversation",
|
||||||
|
"inReplyToAtomUri" : "ostatus:inReplyToAtomUri",
|
||||||
|
"manuallyApprovesFollowers" : "as:manuallyApprovesFollowers",
|
||||||
|
"movedTo" : "as:movedTo",
|
||||||
|
"ostatus" : "http://ostatus.org#",
|
||||||
|
"sensitive" : "as:sensitive",
|
||||||
|
"toot" : "http://joinmastodon.org/ns#"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"atomUri" : "http://mastodon.example.org/users/admin/statuses/99541947525187367",
|
||||||
"attachment" : [
|
"attachment" : [
|
||||||
{
|
{
|
||||||
"url": "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
|
"mediaType" : "image/jpeg",
|
||||||
"type": "Document",
|
|
||||||
"name" : null,
|
"name" : null,
|
||||||
"mediaType": "image/jpeg"
|
"type" : "Document",
|
||||||
|
"url" : "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributedTo" : "http://mastodon.example.org/users/admin",
|
||||||
|
"cc" : [
|
||||||
|
"http://mastodon.example.org/users/admin/followers"
|
||||||
|
],
|
||||||
|
"content" : "<p>yeah.</p>",
|
||||||
|
"conversation" : "tag:mastodon.example.org,2018-02-17:objectId=59:objectType=Conversation",
|
||||||
|
"id" : "http://mastodon.example.org/users/admin/statuses/99541947525187367",
|
||||||
|
"inReplyTo" : null,
|
||||||
|
"inReplyToAtomUri" : null,
|
||||||
|
"published" : "2018-02-17T17:46:20Z",
|
||||||
|
"sensitive" : false,
|
||||||
|
"summary" : null,
|
||||||
|
"tag" : [],
|
||||||
|
"to" : [
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
],
|
||||||
|
"type" : "Note",
|
||||||
|
"url" : "http://mastodon.example.org/@admin/99541947525187367"
|
||||||
}
|
}
|
||||||
]}
|
|
||||||
|
|
|
@ -289,4 +289,29 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
|
||||||
assert Repo.get_by(Notification, user_id: poster.id, activity_id: like.id)
|
assert Repo.get_by(Notification, user_id: poster.id, activity_id: like.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "announce objects" do
|
||||||
|
setup do
|
||||||
|
poster = insert(:user)
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, post} = CommonAPI.post(poster, %{status: "hey"})
|
||||||
|
|
||||||
|
{:ok, announce_data, _meta} = Builder.announce(user, post.object)
|
||||||
|
{:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true)
|
||||||
|
|
||||||
|
%{announce: announce, user: user, poster: poster}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "add the announce to the original object", %{announce: announce, user: user} do
|
||||||
|
{:ok, announce, _} = SideEffects.handle(announce)
|
||||||
|
object = Object.get_by_ap_id(announce.data["object"])
|
||||||
|
assert object.data["announcement_count"] == 1
|
||||||
|
assert user.ap_id in object.data["announcements"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "creates a notification", %{announce: announce, poster: poster} do
|
||||||
|
{:ok, announce, _} = SideEffects.handle(announce)
|
||||||
|
assert Repo.get_by(Notification, user_id: poster.id, activity_id: announce.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnnounceHandlingTest do
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
|
|
||||||
test "it works for incoming honk announces" do
|
test "it works for incoming honk announces" do
|
||||||
_user = insert(:user, ap_id: "https://honktest/u/test", local: false)
|
user = insert(:user, ap_id: "https://honktest/u/test", local: false)
|
||||||
other_user = insert(:user)
|
other_user = insert(:user)
|
||||||
{:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"})
|
{:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"})
|
||||||
|
|
||||||
|
@ -28,6 +28,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnnounceHandlingTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
{:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(announce)
|
{:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(announce)
|
||||||
|
|
||||||
|
object = Object.get_by_ap_id(post.data["object"])
|
||||||
|
|
||||||
|
assert length(object.data["announcements"]) == 1
|
||||||
|
assert user.ap_id in object.data["announcements"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works for incoming announces with actor being inlined (kroeg)" do
|
test "it works for incoming announces with actor being inlined (kroeg)" do
|
||||||
|
@ -48,8 +53,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnnounceHandlingTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works for incoming announces, fetching the announced object" do
|
test "it works for incoming announces, fetching the announced object" do
|
||||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
data =
|
||||||
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
|
File.read!("test/fixtures/mastodon-announce.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", "http://mastodon.example.org/users/admin/statuses/99541947525187367")
|
||||||
|
|
||||||
|
Tesla.Mock.mock(fn
|
||||||
|
%{method: :get} ->
|
||||||
|
%Tesla.Env{status: 200, body: File.read!("test/fixtures/mastodon-note-object.json")}
|
||||||
|
end)
|
||||||
|
|
||||||
_user = insert(:user, local: false, ap_id: data["actor"])
|
_user = insert(:user, local: false, ap_id: data["actor"])
|
||||||
|
|
||||||
|
@ -92,6 +104,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnnounceHandlingTest do
|
||||||
assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id
|
assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Ignore inlined activities for now
|
||||||
|
@tag skip: true
|
||||||
test "it works for incoming announces with an inlined activity" do
|
test "it works for incoming announces with an inlined activity" do
|
||||||
data =
|
data =
|
||||||
File.read!("test/fixtures/mastodon-announce-private.json")
|
File.read!("test/fixtures/mastodon-announce-private.json")
|
||||||
|
|
|
@ -416,7 +416,8 @@ defmodule Pleroma.Web.CommonAPITest do
|
||||||
|
|
||||||
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
|
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
|
||||||
|
|
||||||
{:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user)
|
{:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user)
|
||||||
|
assert Visibility.is_public?(announce_activity)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "can't repeat a repeat" do
|
test "can't repeat a repeat" do
|
||||||
|
@ -424,9 +425,9 @@ defmodule Pleroma.Web.CommonAPITest do
|
||||||
other_user = insert(:user)
|
other_user = insert(:user)
|
||||||
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
|
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
|
||||||
|
|
||||||
{:ok, %Activity{} = announce, _} = CommonAPI.repeat(activity.id, other_user)
|
{:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user)
|
||||||
|
|
||||||
refute match?({:ok, %Activity{}, _}, CommonAPI.repeat(announce.id, user))
|
refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "repeating a status privately" do
|
test "repeating a status privately" do
|
||||||
|
@ -435,7 +436,7 @@ defmodule Pleroma.Web.CommonAPITest do
|
||||||
|
|
||||||
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
|
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
|
||||||
|
|
||||||
{:ok, %Activity{} = announce_activity, _} =
|
{:ok, %Activity{} = announce_activity} =
|
||||||
CommonAPI.repeat(activity.id, user, %{visibility: "private"})
|
CommonAPI.repeat(activity.id, user, %{visibility: "private"})
|
||||||
|
|
||||||
assert Visibility.is_private?(announce_activity)
|
assert Visibility.is_private?(announce_activity)
|
||||||
|
@ -458,8 +459,8 @@ defmodule Pleroma.Web.CommonAPITest do
|
||||||
other_user = insert(:user)
|
other_user = insert(:user)
|
||||||
|
|
||||||
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
|
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
|
||||||
{:ok, %Activity{} = announce, object} = CommonAPI.repeat(activity.id, user)
|
{:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user)
|
||||||
{:ok, ^announce, ^object} = CommonAPI.repeat(activity.id, user)
|
{:ok, ^announce} = CommonAPI.repeat(activity.id, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "favoriting a status twice returns ok, but without the like activity" do
|
test "favoriting a status twice returns ok, but without the like activity" do
|
||||||
|
|
Loading…
Reference in a new issue