1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.CommonAPITest do
6 use Oban.Testing, repo: Pleroma.Repo
10 alias Pleroma.Conversation.Participation
11 alias Pleroma.Notification
15 alias Pleroma.Web.ActivityPub.Transmogrifier
16 alias Pleroma.Web.ActivityPub.Visibility
17 alias Pleroma.Web.AdminAPI.AccountView
18 alias Pleroma.Web.CommonAPI
19 alias Pleroma.Workers.PollWorker
21 import Pleroma.Factory
23 import Ecto.Query, only: [from: 2]
25 require Pleroma.Constants
28 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
29 clear_config([Pleroma.Uploaders.Local, :uploads], "uploads")
30 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
34 setup do: clear_config([:instance, :safe_dm_mentions])
35 setup do: clear_config([:instance, :limit])
36 setup do: clear_config([:instance, :max_pinned_statuses])
38 describe "posting polls" do
39 test "it posts a poll" do
43 CommonAPI.post(user, %{
44 status: "who is the best",
45 poll: %{expires_in: 600, options: ["reimu", "marisa"]}
48 object = Object.normalize(activity, fetch: false)
50 assert object.data["type"] == "Question"
51 assert object.data["oneOf"] |> length() == 2
55 args: %{op: "poll_end", activity_id: activity.id},
56 scheduled_at: NaiveDateTime.from_iso8601!(object.data["closed"])
61 describe "blocking" do
63 blocker = insert(:user)
64 blocked = insert(:user, local: false)
65 CommonAPI.follow(blocker, blocked)
66 CommonAPI.follow(blocked, blocker)
67 CommonAPI.accept_follow_request(blocker, blocked)
68 CommonAPI.accept_follow_request(blocked, blocked)
69 %{blocker: blocker, blocked: blocked}
72 test "it blocks and federates", %{blocker: blocker, blocked: blocked} do
73 clear_config([:instance, :federating], true)
75 with_mock Pleroma.Web.Federator,
76 publish: fn _ -> nil end do
77 assert User.get_follow_state(blocker, blocked) == :follow_accept
78 refute is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked))
80 assert {:ok, block} = CommonAPI.block(blocker, blocked)
83 assert User.blocks?(blocker, blocked)
84 refute User.following?(blocker, blocked)
85 refute User.following?(blocked, blocker)
87 refute User.get_follow_state(blocker, blocked)
89 assert %{data: %{"state" => "reject"}} =
90 Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked)
92 assert called(Pleroma.Web.Federator.publish(block))
96 test "it blocks and does not federate if outgoing blocks are disabled", %{
100 clear_config([:instance, :federating], true)
101 clear_config([:activitypub, :outgoing_blocks], false)
103 with_mock Pleroma.Web.Federator,
104 publish: fn _ -> nil end do
105 assert {:ok, block} = CommonAPI.block(blocker, blocked)
108 assert User.blocks?(blocker, blocked)
109 refute User.following?(blocker, blocked)
110 refute User.following?(blocked, blocker)
112 refute called(Pleroma.Web.Federator.publish(block))
117 describe "unblocking" do
118 test "it works even without an existing block activity" do
119 blocked = insert(:user)
120 blocker = insert(:user)
121 User.block(blocker, blocked)
123 assert User.blocks?(blocker, blocked)
124 assert {:ok, :no_activity} == CommonAPI.unblock(blocker, blocked)
125 refute User.blocks?(blocker, blocked)
129 describe "deletion" do
130 test "it works with pruned objects" do
133 {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
135 clear_config([:instance, :federating], true)
137 Object.normalize(post, fetch: false)
140 with_mock Pleroma.Web.Federator,
141 publish: fn _ -> nil end do
142 assert {:ok, delete} = CommonAPI.delete(post.id, user)
144 assert called(Pleroma.Web.Federator.publish(delete))
147 refute Activity.get_by_id(post.id)
150 test "it allows users to delete their posts" do
153 {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
155 clear_config([:instance, :federating], true)
157 with_mock Pleroma.Web.Federator,
158 publish: fn _ -> nil end do
159 assert {:ok, delete} = CommonAPI.delete(post.id, user)
161 assert called(Pleroma.Web.Federator.publish(delete))
164 refute Activity.get_by_id(post.id)
167 test "it does not allow a user to delete their posts" do
169 other_user = insert(:user)
171 {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
173 assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user)
174 assert Activity.get_by_id(post.id)
177 test "it allows moderators to delete other user's posts" do
179 moderator = insert(:user, is_moderator: true)
181 {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
183 assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
186 refute Activity.get_by_id(post.id)
189 test "it allows admins to delete other user's posts" do
191 moderator = insert(:user, is_admin: true)
193 {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
195 assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
198 refute Activity.get_by_id(post.id)
201 test "superusers deleting non-local posts won't federate the delete" do
202 # This is the user of the ingested activity
206 ap_id: "http://mastodon.example.org/users/admin",
207 last_refreshed_at: NaiveDateTime.utc_now()
210 moderator = insert(:user, is_admin: true)
213 File.read!("test/fixtures/mastodon-post-activity.json")
216 {:ok, post} = Transmogrifier.handle_incoming(data)
218 with_mock Pleroma.Web.Federator,
219 publish: fn _ -> nil end do
220 assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
222 refute called(Pleroma.Web.Federator.publish(:_))
225 refute Activity.get_by_id(post.id)
229 test "favoriting race condition" do
231 users_serial = insert_list(10, :user)
232 users = insert_list(10, :user)
234 {:ok, activity} = CommonAPI.post(user, %{status: "."})
237 |> Enum.map(fn user ->
238 CommonAPI.favorite(user, activity.id)
241 object = Object.get_by_ap_id(activity.data["object"])
242 assert object.data["like_count"] == 10
245 |> Enum.map(fn user ->
247 CommonAPI.favorite(user, activity.id)
250 |> Enum.map(&Task.await/1)
252 object = Object.get_by_ap_id(activity.data["object"])
253 assert object.data["like_count"] == 20
256 test "repeating race condition" do
258 users_serial = insert_list(10, :user)
259 users = insert_list(10, :user)
261 {:ok, activity} = CommonAPI.post(user, %{status: "."})
264 |> Enum.map(fn user ->
265 CommonAPI.repeat(activity.id, user)
268 object = Object.get_by_ap_id(activity.data["object"])
269 assert object.data["announcement_count"] == 10
272 |> Enum.map(fn user ->
274 CommonAPI.repeat(activity.id, user)
277 |> Enum.map(&Task.await/1)
279 object = Object.get_by_ap_id(activity.data["object"])
280 assert object.data["announcement_count"] == 20
283 test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
285 {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
287 [participation] = Participation.for_user(user)
290 CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id})
292 assert Visibility.is_direct?(convo_reply)
294 assert activity.data["context"] == convo_reply.data["context"]
297 test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do
299 jafnhar = insert(:user)
300 tridi = insert(:user)
303 CommonAPI.post(har, %{
304 status: "@#{jafnhar.nickname} hey",
308 assert har.ap_id in activity.recipients
309 assert jafnhar.ap_id in activity.recipients
311 [participation] = Participation.for_user(har)
314 CommonAPI.post(har, %{
315 status: "I don't really like @#{tridi.nickname}",
316 visibility: "direct",
317 in_reply_to_status_id: activity.id,
318 in_reply_to_conversation_id: participation.id
321 assert har.ap_id in activity.recipients
322 assert jafnhar.ap_id in activity.recipients
323 refute tridi.ap_id in activity.recipients
326 test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
328 jafnhar = insert(:user)
329 tridi = insert(:user)
331 clear_config([:instance, :safe_dm_mentions], true)
334 CommonAPI.post(har, %{
335 status: "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again",
339 refute tridi.ap_id in activity.recipients
340 assert jafnhar.ap_id in activity.recipients
343 test "it de-duplicates tags" do
345 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU"})
347 object = Object.normalize(activity, fetch: false)
349 assert Object.tags(object) == ["2hu"]
352 test "it adds emoji in the object" do
354 {:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"})
356 assert Object.normalize(activity, fetch: false).data["emoji"]["firefox"]
359 describe "posting" do
360 test "it adds an emoji on an external site" do
362 {:ok, activity} = CommonAPI.post(user, %{status: "hey :external_emoji:"})
364 assert %{"external_emoji" => url} = Object.normalize(activity).data["emoji"]
365 assert url == "https://example.com/emoji.png"
367 {:ok, activity} = CommonAPI.post(user, %{status: "hey :blank:"})
369 assert %{"blank" => url} = Object.normalize(activity).data["emoji"]
370 assert url == "#{Pleroma.Web.Endpoint.url()}/emoji/blank.png"
373 test "it copies emoji from the subject of the parent post" do
376 Object.normalize("https://patch.cx/objects/a399c28e-c821-4820-bc3e-4afeb044c16f",
380 activity = Activity.get_create_by_object_ap_id(object.data["id"])
383 {:ok, reply_activity} =
384 CommonAPI.post(user, %{
385 in_reply_to_id: activity.id,
386 status: ":joker_disapprove:",
387 spoiler_text: ":joker_smile:"
390 assert Object.normalize(reply_activity).data["emoji"]["joker_smile"]
391 refute Object.normalize(reply_activity).data["emoji"]["joker_disapprove"]
394 test "deactivated users can't post" do
395 user = insert(:user, is_active: false)
396 assert {:error, _} = CommonAPI.post(user, %{status: "ye"})
399 test "it supports explicit addressing" do
401 user_two = insert(:user)
402 user_three = insert(:user)
403 user_four = insert(:user)
406 CommonAPI.post(user, %{
408 "Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.",
409 to: [user_two.nickname, user_four.nickname, "nonexistent"]
412 assert user.ap_id in activity.recipients
413 assert user_two.ap_id in activity.recipients
414 assert user_four.ap_id in activity.recipients
415 refute user_three.ap_id in activity.recipients
418 test "it filters out obviously bad tags when accepting a post as HTML" do
421 post = "<p><b>2hu</b></p><script>alert('xss')</script>"
424 CommonAPI.post(user, %{
426 content_type: "text/html"
429 object = Object.normalize(activity, fetch: false)
431 assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
433 assert object.data["source"] == %{
434 "mediaType" => "text/html",
439 test "it filters out obviously bad tags when accepting a post as Markdown" do
442 post = "<p><b>2hu</b></p><script>alert('xss')</script>"
445 CommonAPI.post(user, %{
447 content_type: "text/markdown"
450 object = Object.normalize(activity, fetch: false)
452 assert object.data["content"] == "<p><b>2hu</b></p>"
454 assert object.data["source"] == %{
455 "mediaType" => "text/markdown",
460 test "it does not allow replies to direct messages that are not direct messages themselves" do
463 {:ok, activity} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
466 CommonAPI.post(user, %{
468 visibility: "direct",
469 in_reply_to_status_id: activity.id
472 Enum.each(["public", "private", "unlisted"], fn visibility ->
473 assert {:error, "The message visibility must be direct"} =
474 CommonAPI.post(user, %{
476 visibility: visibility,
477 in_reply_to_status_id: activity.id
482 test "replying with a direct message will NOT auto-add the author of the reply to the recipient list" do
484 other_user = insert(:user)
485 third_user = insert(:user)
487 {:ok, post} = CommonAPI.post(user, %{status: "I'm stupid"})
490 CommonAPI.post(other_user, %{status: "No ur smart", in_reply_to_status_id: post.id})
492 # The OP is implicitly added
493 assert user.ap_id in open_answer.recipients
495 {:ok, secret_answer} =
496 CommonAPI.post(other_user, %{
497 status: "lol, that guy really is stupid, right, @#{third_user.nickname}?",
498 in_reply_to_status_id: post.id,
502 assert third_user.ap_id in secret_answer.recipients
504 # The OP is not added
505 refute user.ap_id in secret_answer.recipients
508 test "it allows to address a list" do
510 {:ok, list} = Pleroma.List.create("foo", user)
512 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
514 assert activity.data["bcc"] == [list.ap_id]
515 assert activity.recipients == [list.ap_id, user.ap_id]
516 assert activity.data["listMessage"] == list.ap_id
519 test "it returns error when status is empty and no attachments" do
522 assert {:error, "Cannot post an empty status without attachments"} =
523 CommonAPI.post(user, %{status: ""})
526 test "it validates character limits are correctly enforced" do
527 clear_config([:instance, :limit], 5)
531 assert {:error, "The status is over the character limit"} =
532 CommonAPI.post(user, %{status: "foobar"})
534 assert {:ok, _activity} = CommonAPI.post(user, %{status: "12345"})
537 test "it can handle activities that expire" do
540 expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
542 assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000})
545 worker: Pleroma.Workers.PurgeExpiredActivity,
546 args: %{activity_id: activity.id},
547 scheduled_at: expires_at
552 describe "reactions" do
553 test "reacting to a status with an emoji" do
555 other_user = insert(:user)
557 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
559 {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
561 assert reaction.data["actor"] == user.ap_id
562 assert reaction.data["content"] == "👍"
564 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
566 {:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".")
569 test "unreacting to a status with an emoji" do
571 other_user = insert(:user)
573 clear_config([:instance, :federating], true)
575 with_mock Pleroma.Web.Federator,
576 publish: fn _ -> nil end do
577 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
578 {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
580 {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
582 assert unreaction.data["type"] == "Undo"
583 assert unreaction.data["object"] == reaction.data["id"]
584 assert unreaction.local
586 # On federation, it contains the undone (and deleted) object
587 unreaction_with_object = %{
589 | data: Map.put(unreaction.data, "object", reaction.data)
592 assert called(Pleroma.Web.Federator.publish(unreaction_with_object))
596 test "repeating a status" do
598 other_user = insert(:user)
600 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
602 {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user)
603 assert Visibility.is_public?(announce_activity)
606 test "can't repeat a repeat" do
608 other_user = insert(:user)
609 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
611 {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user)
613 refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user))
616 test "repeating a status privately" do
618 other_user = insert(:user)
620 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
622 {:ok, %Activity{} = announce_activity} =
623 CommonAPI.repeat(activity.id, user, %{visibility: "private"})
625 assert Visibility.is_private?(announce_activity)
626 refute Visibility.visible_for_user?(announce_activity, nil)
629 test "author can repeat own private statuses" do
630 author = insert(:user)
631 follower = insert(:user)
632 CommonAPI.follow(follower, author)
634 {:ok, activity} = CommonAPI.post(author, %{status: "cofe", visibility: "private"})
636 {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, author)
638 assert Visibility.is_private?(announce_activity)
639 refute Visibility.visible_for_user?(announce_activity, nil)
641 assert Visibility.visible_for_user?(activity, follower)
642 assert {:error, :not_found} = CommonAPI.repeat(activity.id, follower)
645 test "favoriting a status" do
647 other_user = insert(:user)
649 {:ok, post_activity} = CommonAPI.post(other_user, %{status: "cofe"})
651 {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id)
652 assert data["type"] == "Like"
653 assert data["actor"] == user.ap_id
654 assert data["object"] == post_activity.data["object"]
657 test "retweeting a status twice returns the status" do
659 other_user = insert(:user)
661 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
662 {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user)
663 {:ok, ^announce} = CommonAPI.repeat(activity.id, user)
666 test "favoriting a status twice returns ok, but without the like activity" do
668 other_user = insert(:user)
670 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
671 {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
672 assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id)
676 describe "pinned statuses" do
678 clear_config([:instance, :max_pinned_statuses], 1)
681 {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
683 [user: user, activity: activity]
686 test "activity not found error", %{user: user} do
687 assert {:error, :not_found} = CommonAPI.pin("id", user)
690 test "pin status", %{user: user, activity: activity} do
691 assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
693 %{data: %{"id" => object_id}} = Object.normalize(activity)
694 user = refresh_record(user)
696 assert user.pinned_objects |> Map.keys() == [object_id]
699 test "pin poll", %{user: user} do
701 CommonAPI.post(user, %{
702 status: "How is fediverse today?",
703 poll: %{options: ["Absolutely outstanding", "Not good"], expires_in: 20}
706 assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
708 %{data: %{"id" => object_id}} = Object.normalize(activity)
710 user = refresh_record(user)
712 assert user.pinned_objects |> Map.keys() == [object_id]
715 test "unlisted statuses can be pinned", %{user: user} do
716 {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!", visibility: "unlisted"})
717 assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
720 test "only self-authored can be pinned", %{activity: activity} do
723 assert {:error, :ownership_error} = CommonAPI.pin(activity.id, user)
726 test "max pinned statuses", %{user: user, activity: activity_one} do
727 {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
729 assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user)
731 user = refresh_record(user)
733 assert {:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity_two.id, user)
736 test "only public can be pinned", %{user: user} do
737 {:ok, activity} = CommonAPI.post(user, %{status: "private status", visibility: "private"})
738 {:error, :visibility_error} = CommonAPI.pin(activity.id, user)
741 test "unpin status", %{user: user, activity: activity} do
742 {:ok, activity} = CommonAPI.pin(activity.id, user)
744 user = refresh_record(user)
748 assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user))
750 user = refresh_record(user)
752 assert user.pinned_objects == %{}
755 test "should unpin when deleting a status", %{user: user, activity: activity} do
756 {:ok, activity} = CommonAPI.pin(activity.id, user)
758 user = refresh_record(user)
760 assert {:ok, _} = CommonAPI.delete(activity.id, user)
762 user = refresh_record(user)
764 assert user.pinned_objects == %{}
767 test "ephemeral activity won't be deleted if was pinned", %{user: user} do
768 {:ok, activity} = CommonAPI.post(user, %{status: "Hello!", expires_in: 601})
770 assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
772 {:ok, _activity} = CommonAPI.pin(activity.id, user)
773 refute Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
775 user = refresh_record(user)
776 {:ok, _} = CommonAPI.unpin(activity.id, user)
778 # recreates expiration job on unpin
779 assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
782 test "ephemeral activity deletion job won't be deleted on pinning error", %{
786 clear_config([:instance, :max_pinned_statuses], 1)
788 {:ok, _activity} = CommonAPI.pin(activity.id, user)
790 {:ok, activity2} = CommonAPI.post(user, %{status: "another status", expires_in: 601})
792 assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id)
794 user = refresh_record(user)
795 {:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity2.id, user)
797 assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id)
801 describe "mute tests" do
805 activity = insert(:note_activity)
807 [user: user, activity: activity]
810 test "marks notifications as read after mute" do
811 author = insert(:user)
812 activity = insert(:note_activity, user: author)
814 friend1 = insert(:user)
815 friend2 = insert(:user)
817 {:ok, reply_activity} =
821 status: "@#{author.nickname} @#{friend1.nickname} test reply",
822 in_reply_to_status_id: activity.id
826 {:ok, favorite_activity} = CommonAPI.favorite(friend2, activity.id)
827 {:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1)
829 assert Repo.aggregate(
830 from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id),
834 unread_notifications =
835 Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id))
837 assert Enum.any?(unread_notifications, fn n ->
838 n.type == "favourite" && n.activity_id == favorite_activity.id
841 assert Enum.any?(unread_notifications, fn n ->
842 n.type == "reblog" && n.activity_id == repeat_activity.id
845 assert Enum.any?(unread_notifications, fn n ->
846 n.type == "mention" && n.activity_id == reply_activity.id
849 {:ok, _} = CommonAPI.add_mute(author, activity)
850 assert CommonAPI.thread_muted?(author, activity)
852 assert Repo.aggregate(
853 from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id),
858 Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id))
860 assert Enum.any?(read_notifications, fn n ->
861 n.type == "favourite" && n.activity_id == favorite_activity.id
864 assert Enum.any?(read_notifications, fn n ->
865 n.type == "reblog" && n.activity_id == repeat_activity.id
868 assert Enum.any?(read_notifications, fn n ->
869 n.type == "mention" && n.activity_id == reply_activity.id
873 test "add mute", %{user: user, activity: activity} do
874 {:ok, _} = CommonAPI.add_mute(user, activity)
875 assert CommonAPI.thread_muted?(user, activity)
878 test "add expiring mute", %{user: user, activity: activity} do
879 {:ok, _} = CommonAPI.add_mute(user, activity, %{expires_in: 60})
880 assert CommonAPI.thread_muted?(user, activity)
882 worker = Pleroma.Workers.MuteExpireWorker
883 args = %{"op" => "unmute_conversation", "user_id" => user.id, "activity_id" => activity.id}
890 assert :ok = perform_job(worker, args)
891 refute CommonAPI.thread_muted?(user, activity)
894 test "remove mute", %{user: user, activity: activity} do
895 CommonAPI.add_mute(user, activity)
896 {:ok, _} = CommonAPI.remove_mute(user, activity)
897 refute CommonAPI.thread_muted?(user, activity)
900 test "remove mute by ids", %{user: user, activity: activity} do
901 CommonAPI.add_mute(user, activity)
902 {:ok, _} = CommonAPI.remove_mute(user.id, activity.id)
903 refute CommonAPI.thread_muted?(user, activity)
906 test "check that mutes can't be duplicate", %{user: user, activity: activity} do
907 CommonAPI.add_mute(user, activity)
908 {:error, _} = CommonAPI.add_mute(user, activity)
912 describe "reports" do
913 test "creates a report" do
914 reporter = insert(:user)
915 target_user = insert(:user)
917 {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"})
919 reporter_ap_id = reporter.ap_id
920 target_ap_id = target_user.ap_id
921 activity_ap_id = activity.data["id"]
925 account_id: target_user.id,
927 status_ids: [activity.id]
932 "id" => activity_ap_id,
933 "content" => "foobar",
934 "published" => activity.object.data["published"],
935 "actor" => AccountView.render("show.json", %{user: target_user})
938 assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
941 actor: ^reporter_ap_id,
944 "content" => ^comment,
945 "object" => [^target_ap_id, ^note_obj],
951 test "updates report state" do
952 [reporter, target_user] = insert_pair(:user)
953 activity = insert(:note_activity, user: target_user)
955 {:ok, %Activity{id: report_id}} =
956 CommonAPI.report(reporter, %{
957 account_id: target_user.id,
958 comment: "I feel offended",
959 status_ids: [activity.id]
962 {:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
964 assert report.data["state"] == "resolved"
966 [reported_user, activity_id] = report.data["object"]
968 assert reported_user == target_user.ap_id
969 assert activity_id == activity.data["id"]
972 test "does not update report state when state is unsupported" do
973 [reporter, target_user] = insert_pair(:user)
974 activity = insert(:note_activity, user: target_user)
976 {:ok, %Activity{id: report_id}} =
977 CommonAPI.report(reporter, %{
978 account_id: target_user.id,
979 comment: "I feel offended",
980 status_ids: [activity.id]
983 assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
986 test "updates state of multiple reports" do
987 [reporter, target_user] = insert_pair(:user)
988 activity = insert(:note_activity, user: target_user)
990 {:ok, %Activity{id: first_report_id}} =
991 CommonAPI.report(reporter, %{
992 account_id: target_user.id,
993 comment: "I feel offended",
994 status_ids: [activity.id]
997 {:ok, %Activity{id: second_report_id}} =
998 CommonAPI.report(reporter, %{
999 account_id: target_user.id,
1000 comment: "I feel very offended!",
1001 status_ids: [activity.id]
1005 CommonAPI.update_report_state([first_report_id, second_report_id], "resolved")
1007 first_report = Activity.get_by_id(first_report_id)
1008 second_report = Activity.get_by_id(second_report_id)
1010 assert report_ids -- [first_report_id, second_report_id] == []
1011 assert first_report.data["state"] == "resolved"
1012 assert second_report.data["state"] == "resolved"
1016 describe "reblog muting" do
1018 muter = insert(:user)
1020 muted = insert(:user)
1022 [muter: muter, muted: muted]
1025 test "add a reblog mute", %{muter: muter, muted: muted} do
1026 {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
1028 assert User.showing_reblogs?(muter, muted) == false
1031 test "remove a reblog mute", %{muter: muter, muted: muted} do
1032 {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
1033 {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted)
1035 assert User.showing_reblogs?(muter, muted) == true
1039 describe "follow/2" do
1040 test "directly follows a non-locked local user" do
1041 [follower, followed] = insert_pair(:user)
1042 {:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
1044 assert User.following?(follower, followed)
1048 describe "unfollow/2" do
1049 test "also unsubscribes a user" do
1050 [follower, followed] = insert_pair(:user)
1051 {:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
1052 {:ok, _subscription} = User.subscribe(follower, followed)
1054 assert User.subscribed_to?(follower, followed)
1056 {:ok, follower} = CommonAPI.unfollow(follower, followed)
1058 refute User.subscribed_to?(follower, followed)
1061 test "removes a pending follow for a local user" do
1062 follower = insert(:user)
1063 followed = insert(:user, is_locked: true)
1065 assert {:ok, follower, followed, %{id: _activity_id, data: %{"state" => "pending"}}} =
1066 CommonAPI.follow(follower, followed)
1068 assert User.get_follow_state(follower, followed) == :follow_pending
1069 assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
1070 assert User.get_follow_state(follower, followed) == nil
1072 assert is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed))
1077 "object" => %{"type" => "Follow"}
1079 } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower)
1082 test "cancels a pending follow for a remote user" do
1083 follower = insert(:user)
1084 followed = insert(:user, is_locked: true, local: false, ap_enabled: true)
1086 assert {:ok, follower, followed, %{id: _activity_id, data: %{"state" => "pending"}}} =
1087 CommonAPI.follow(follower, followed)
1089 assert User.get_follow_state(follower, followed) == :follow_pending
1090 assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
1091 assert User.get_follow_state(follower, followed) == nil
1093 assert is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed))
1098 "object" => %{"type" => "Follow"}
1100 } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower)
1104 describe "accept_follow_request/2" do
1105 test "after acceptance, it sets all existing pending follow request states to 'accept'" do
1106 user = insert(:user, is_locked: true)
1107 follower = insert(:user)
1108 follower_two = insert(:user)
1110 {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
1111 {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
1112 {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user)
1114 assert follow_activity.data["state"] == "pending"
1115 assert follow_activity_two.data["state"] == "pending"
1116 assert follow_activity_three.data["state"] == "pending"
1118 {:ok, _follower} = CommonAPI.accept_follow_request(follower, user)
1120 assert Repo.get(Activity, follow_activity.id).data["state"] == "accept"
1121 assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept"
1122 assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
1125 test "after rejection, it sets all existing pending follow request states to 'reject'" do
1126 user = insert(:user, is_locked: true)
1127 follower = insert(:user)
1128 follower_two = insert(:user)
1130 {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
1131 {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
1132 {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user)
1134 assert follow_activity.data["state"] == "pending"
1135 assert follow_activity_two.data["state"] == "pending"
1136 assert follow_activity_three.data["state"] == "pending"
1138 {:ok, _follower} = CommonAPI.reject_follow_request(follower, user)
1140 assert Repo.get(Activity, follow_activity.id).data["state"] == "reject"
1141 assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
1142 assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
1145 test "doesn't create a following relationship if the corresponding follow request doesn't exist" do
1146 user = insert(:user, is_locked: true)
1147 not_follower = insert(:user)
1148 CommonAPI.accept_follow_request(not_follower, user)
1150 assert Pleroma.FollowingRelationship.following?(not_follower, user) == false
1154 describe "vote/3" do
1155 test "does not allow to vote twice" do
1156 user = insert(:user)
1157 other_user = insert(:user)
1160 CommonAPI.post(user, %{
1161 status: "Am I cute?",
1162 poll: %{options: ["Yes", "No"], expires_in: 20}
1165 object = Object.normalize(activity, fetch: false)
1167 {:ok, _, object} = CommonAPI.vote(other_user, object, [0])
1169 assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1])
1173 describe "get_user/1" do
1174 test "gets user by ap_id" do
1175 user = insert(:user)
1176 assert CommonAPI.get_user(user.ap_id) == user
1179 test "gets user by guessed nickname" do
1180 user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom")
1181 assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user
1188 nickname: "erroruser@example.com"
1189 } = CommonAPI.get_user("")
1193 describe "with `local` visibility" do
1194 setup do: clear_config([:instance, :federating], true)
1197 user = insert(:user)
1199 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1200 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
1202 assert Visibility.is_local_public?(activity)
1203 assert_not_called(Pleroma.Web.Federator.publish(activity))
1208 user = insert(:user)
1210 {:ok, %Activity{id: activity_id}} =
1211 CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
1213 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1214 assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} =
1215 CommonAPI.delete(activity_id, user)
1217 assert Visibility.is_local_public?(activity)
1218 assert_not_called(Pleroma.Web.Federator.publish(activity))
1223 user = insert(:user)
1224 other_user = insert(:user)
1226 {:ok, %Activity{id: activity_id}} =
1227 CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1229 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1230 assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} =
1231 CommonAPI.repeat(activity_id, user)
1233 assert Visibility.is_local_public?(activity)
1234 refute called(Pleroma.Web.Federator.publish(activity))
1239 user = insert(:user)
1240 other_user = insert(:user)
1242 {:ok, %Activity{id: activity_id}} =
1243 CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1245 assert {:ok, _} = CommonAPI.repeat(activity_id, user)
1247 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1248 assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
1249 CommonAPI.unrepeat(activity_id, user)
1251 assert Visibility.is_local_public?(activity)
1252 refute called(Pleroma.Web.Federator.publish(activity))
1257 user = insert(:user)
1258 other_user = insert(:user)
1260 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1262 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1263 assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} =
1264 CommonAPI.favorite(user, activity.id)
1266 assert Visibility.is_local_public?(activity)
1267 refute called(Pleroma.Web.Federator.publish(activity))
1271 test "unfavorite" do
1272 user = insert(:user)
1273 other_user = insert(:user)
1275 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1277 {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
1279 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1280 assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user)
1281 assert Visibility.is_local_public?(activity)
1282 refute called(Pleroma.Web.Federator.publish(activity))
1286 test "react_with_emoji" do
1287 user = insert(:user)
1288 other_user = insert(:user)
1289 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1291 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1292 assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} =
1293 CommonAPI.react_with_emoji(activity.id, user, "👍")
1295 assert Visibility.is_local_public?(activity)
1296 refute called(Pleroma.Web.Federator.publish(activity))
1300 test "unreact_with_emoji" do
1301 user = insert(:user)
1302 other_user = insert(:user)
1303 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1305 {:ok, _reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
1307 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1308 assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
1309 CommonAPI.unreact_with_emoji(activity.id, user, "👍")
1311 assert Visibility.is_local_public?(activity)
1312 refute called(Pleroma.Web.Federator.publish(activity))
1317 describe "update/3" do
1318 test "updates a post" do
1319 user = insert(:user)
1320 {:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1"})
1322 {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
1324 updated_object = Object.normalize(updated)
1325 assert updated_object.data["content"] == "updated 2"
1326 assert Map.get(updated_object.data, "summary", "") == ""
1327 assert Map.has_key?(updated_object.data, "updated")
1330 test "does not change visibility" do
1331 user = insert(:user)
1334 CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1", visibility: "private"})
1336 {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
1338 updated_object = Object.normalize(updated)
1339 assert updated_object.data["content"] == "updated 2"
1340 assert Map.get(updated_object.data, "summary", "") == ""
1341 assert Visibility.get_visibility(updated_object) == "private"
1342 assert Visibility.get_visibility(updated) == "private"
1345 test "updates a post with emoji" do
1346 [{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all()
1348 user = insert(:user)
1351 CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"})
1353 {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2 :#{emoji2}:"})
1355 updated_object = Object.normalize(updated)
1356 assert updated_object.data["content"] == "updated 2 :#{emoji2}:"
1357 assert %{^emoji2 => _} = updated_object.data["emoji"]
1360 test "updates a post with emoji and federate properly" do
1361 [{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all()
1363 user = insert(:user)
1366 CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"})
1368 clear_config([:instance, :federating], true)
1370 with_mock Pleroma.Web.Federator,
1371 publish: fn _p -> nil end do
1372 {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2 :#{emoji2}:"})
1374 assert updated.data["object"]["content"] == "updated 2 :#{emoji2}:"
1375 assert %{^emoji2 => _} = updated.data["object"]["emoji"]
1377 assert called(Pleroma.Web.Federator.publish(updated))
1381 test "editing a post that copied a remote title with remote emoji should keep that emoji" do
1382 remote_emoji_uri = "https://remote.org/emoji.png"
1388 "summary" => ":remoteemoji:",
1390 "remoteemoji" => remote_emoji_uri
1395 "name" => "remoteemoji",
1396 "icon" => %{"url" => remote_emoji_uri}
1402 note_activity = insert(:note_activity, note: note)
1404 user = insert(:user)
1407 CommonAPI.post(user, %{
1409 spoiler_text: ":remoteemoji:",
1410 in_reply_to_id: note_activity.id
1413 assert reply.object.data["emoji"]["remoteemoji"] == remote_emoji_uri
1416 CommonAPI.update(user, reply, %{status: "reply mew mew", spoiler_text: ":remoteemoji:"})
1418 edited_note = Pleroma.Object.normalize(edit)
1420 assert edited_note.data["emoji"]["remoteemoji"] == remote_emoji_uri
1423 test "respects MRF" do
1424 user = insert(:user)
1426 clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
1427 clear_config([:mrf_keyword, :replace], [{"updated", "mewmew"}])
1429 {:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "updated 1"})
1430 assert Object.normalize(activity).data["summary"] == "mewmew 1"
1432 {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
1434 updated_object = Object.normalize(updated)
1435 assert updated_object.data["content"] == "mewmew 2"
1436 assert Map.get(updated_object.data, "summary", "") == ""
1437 assert Map.has_key?(updated_object.data, "updated")