test/pleroma/web/common_api_test.exs: Strip : around emoji key-name
[akkoma] / test / pleroma / web / common_api_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.CommonAPITest do
6 use Oban.Testing, repo: Pleroma.Repo
7 use Pleroma.DataCase
8
9 alias Pleroma.Activity
10 alias Pleroma.Chat
11 alias Pleroma.Conversation.Participation
12 alias Pleroma.Notification
13 alias Pleroma.Object
14 alias Pleroma.Repo
15 alias Pleroma.User
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Transmogrifier
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.AdminAPI.AccountView
20 alias Pleroma.Web.CommonAPI
21
22 import Pleroma.Factory
23 import Mock
24 import Ecto.Query, only: [from: 2]
25
26 require Pleroma.Constants
27
28 setup_all do
29 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
30 :ok
31 end
32
33 setup do: clear_config([:instance, :safe_dm_mentions])
34 setup do: clear_config([:instance, :limit])
35 setup do: clear_config([:instance, :max_pinned_statuses])
36
37 describe "posting polls" do
38 test "it posts a poll" do
39 user = insert(:user)
40
41 {:ok, activity} =
42 CommonAPI.post(user, %{
43 status: "who is the best",
44 poll: %{expires_in: 600, options: ["reimu", "marisa"]}
45 })
46
47 object = Object.normalize(activity, fetch: false)
48
49 assert object.data["type"] == "Question"
50 assert object.data["oneOf"] |> length() == 2
51 end
52 end
53
54 describe "blocking" do
55 setup do
56 blocker = insert(:user)
57 blocked = insert(:user)
58 User.follow(blocker, blocked)
59 User.follow(blocked, blocker)
60 %{blocker: blocker, blocked: blocked}
61 end
62
63 test "it blocks and federates", %{blocker: blocker, blocked: blocked} do
64 clear_config([:instance, :federating], true)
65
66 with_mock Pleroma.Web.Federator,
67 publish: fn _ -> nil end do
68 assert {:ok, block} = CommonAPI.block(blocker, blocked)
69
70 assert block.local
71 assert User.blocks?(blocker, blocked)
72 refute User.following?(blocker, blocked)
73 refute User.following?(blocked, blocker)
74
75 assert called(Pleroma.Web.Federator.publish(block))
76 end
77 end
78
79 test "it blocks and does not federate if outgoing blocks are disabled", %{
80 blocker: blocker,
81 blocked: blocked
82 } do
83 clear_config([:instance, :federating], true)
84 clear_config([:activitypub, :outgoing_blocks], false)
85
86 with_mock Pleroma.Web.Federator,
87 publish: fn _ -> nil end do
88 assert {:ok, block} = CommonAPI.block(blocker, blocked)
89
90 assert block.local
91 assert User.blocks?(blocker, blocked)
92 refute User.following?(blocker, blocked)
93 refute User.following?(blocked, blocker)
94
95 refute called(Pleroma.Web.Federator.publish(block))
96 end
97 end
98 end
99
100 describe "posting chat messages" do
101 setup do: clear_config([:instance, :chat_limit])
102
103 test "it posts a self-chat" do
104 author = insert(:user)
105 recipient = author
106
107 {:ok, activity} =
108 CommonAPI.post_chat_message(
109 author,
110 recipient,
111 "remember to buy milk when milk truk arive"
112 )
113
114 assert activity.data["type"] == "Create"
115 end
116
117 test "it posts a chat message without content but with an attachment" do
118 author = insert(:user)
119 recipient = insert(:user)
120
121 file = %Plug.Upload{
122 content_type: "image/jpeg",
123 path: Path.absname("test/fixtures/image.jpg"),
124 filename: "an_image.jpg"
125 }
126
127 {:ok, upload} = ActivityPub.upload(file, actor: author.ap_id)
128
129 with_mocks([
130 {
131 Pleroma.Web.Streamer,
132 [],
133 [
134 stream: fn _, _ ->
135 nil
136 end
137 ]
138 },
139 {
140 Pleroma.Web.Push,
141 [],
142 [
143 send: fn _ -> nil end
144 ]
145 }
146 ]) do
147 {:ok, activity} =
148 CommonAPI.post_chat_message(
149 author,
150 recipient,
151 nil,
152 media_id: upload.id
153 )
154
155 notification =
156 Notification.for_user_and_activity(recipient, activity)
157 |> Repo.preload(:activity)
158
159 assert called(Pleroma.Web.Push.send(notification))
160 assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
161 assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
162
163 assert activity
164 end
165 end
166
167 test "it adds html newlines" do
168 author = insert(:user)
169 recipient = insert(:user)
170
171 other_user = insert(:user)
172
173 {:ok, activity} =
174 CommonAPI.post_chat_message(
175 author,
176 recipient,
177 "uguu\nuguuu"
178 )
179
180 assert other_user.ap_id not in activity.recipients
181
182 object = Object.normalize(activity, fetch: false)
183
184 assert object.data["content"] == "uguu<br/>uguuu"
185 end
186
187 test "it linkifies" do
188 author = insert(:user)
189 recipient = insert(:user)
190
191 other_user = insert(:user)
192
193 {:ok, activity} =
194 CommonAPI.post_chat_message(
195 author,
196 recipient,
197 "https://example.org is the site of @#{other_user.nickname} #2hu"
198 )
199
200 assert other_user.ap_id not in activity.recipients
201
202 object = Object.normalize(activity, fetch: false)
203
204 assert object.data["content"] ==
205 "<a href=\"https://example.org\" rel=\"ugc\">https://example.org</a> is the site of <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{
206 other_user.id
207 }\" href=\"#{other_user.ap_id}\" rel=\"ugc\">@<span>#{other_user.nickname}</span></a></span> <a class=\"hashtag\" data-tag=\"2hu\" href=\"http://localhost:4001/tag/2hu\">#2hu</a>"
208 end
209
210 test "it posts a chat message" do
211 author = insert(:user)
212 recipient = insert(:user)
213
214 {:ok, activity} =
215 CommonAPI.post_chat_message(
216 author,
217 recipient,
218 "a test message <script>alert('uuu')</script> :firefox:"
219 )
220
221 assert activity.data["type"] == "Create"
222 assert activity.local
223 object = Object.normalize(activity, fetch: false)
224
225 assert object.data["type"] == "ChatMessage"
226 assert object.data["to"] == [recipient.ap_id]
227
228 assert object.data["content"] ==
229 "a test message &lt;script&gt;alert(&#39;uuu&#39;)&lt;/script&gt; :firefox:"
230
231 assert object.data["emoji"] == %{
232 "firefox" => "http://localhost:4001/emoji/Firefox.gif"
233 }
234
235 assert Chat.get(author.id, recipient.ap_id)
236 assert Chat.get(recipient.id, author.ap_id)
237
238 assert :ok == Pleroma.Web.Federator.perform(:publish, activity)
239 end
240
241 test "it reject messages over the local limit" do
242 clear_config([:instance, :chat_limit], 2)
243
244 author = insert(:user)
245 recipient = insert(:user)
246
247 {:error, message} =
248 CommonAPI.post_chat_message(
249 author,
250 recipient,
251 "123"
252 )
253
254 assert message == :content_too_long
255 end
256
257 test "it reject messages via MRF" do
258 clear_config([:mrf_keyword, :reject], ["GNO"])
259 clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
260
261 author = insert(:user)
262 recipient = insert(:user)
263
264 assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
265 CommonAPI.post_chat_message(author, recipient, "GNO/Linux")
266 end
267 end
268
269 describe "unblocking" do
270 test "it works even without an existing block activity" do
271 blocked = insert(:user)
272 blocker = insert(:user)
273 User.block(blocker, blocked)
274
275 assert User.blocks?(blocker, blocked)
276 assert {:ok, :no_activity} == CommonAPI.unblock(blocker, blocked)
277 refute User.blocks?(blocker, blocked)
278 end
279 end
280
281 describe "deletion" do
282 test "it works with pruned objects" do
283 user = insert(:user)
284
285 {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
286
287 clear_config([:instance, :federating], true)
288
289 Object.normalize(post, fetch: false)
290 |> Object.prune()
291
292 with_mock Pleroma.Web.Federator,
293 publish: fn _ -> nil end do
294 assert {:ok, delete} = CommonAPI.delete(post.id, user)
295 assert delete.local
296 assert called(Pleroma.Web.Federator.publish(delete))
297 end
298
299 refute Activity.get_by_id(post.id)
300 end
301
302 test "it allows users to delete their posts" do
303 user = insert(:user)
304
305 {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
306
307 clear_config([:instance, :federating], true)
308
309 with_mock Pleroma.Web.Federator,
310 publish: fn _ -> nil end do
311 assert {:ok, delete} = CommonAPI.delete(post.id, user)
312 assert delete.local
313 assert called(Pleroma.Web.Federator.publish(delete))
314 end
315
316 refute Activity.get_by_id(post.id)
317 end
318
319 test "it does not allow a user to delete their posts" do
320 user = insert(:user)
321 other_user = insert(:user)
322
323 {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
324
325 assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user)
326 assert Activity.get_by_id(post.id)
327 end
328
329 test "it allows moderators to delete other user's posts" do
330 user = insert(:user)
331 moderator = insert(:user, is_moderator: true)
332
333 {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
334
335 assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
336 assert delete.local
337
338 refute Activity.get_by_id(post.id)
339 end
340
341 test "it allows admins to delete other user's posts" do
342 user = insert(:user)
343 moderator = insert(:user, is_admin: true)
344
345 {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
346
347 assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
348 assert delete.local
349
350 refute Activity.get_by_id(post.id)
351 end
352
353 test "superusers deleting non-local posts won't federate the delete" do
354 # This is the user of the ingested activity
355 _user =
356 insert(:user,
357 local: false,
358 ap_id: "http://mastodon.example.org/users/admin",
359 last_refreshed_at: NaiveDateTime.utc_now()
360 )
361
362 moderator = insert(:user, is_admin: true)
363
364 data =
365 File.read!("test/fixtures/mastodon-post-activity.json")
366 |> Jason.decode!()
367
368 {:ok, post} = Transmogrifier.handle_incoming(data)
369
370 with_mock Pleroma.Web.Federator,
371 publish: fn _ -> nil end do
372 assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
373 assert delete.local
374 refute called(Pleroma.Web.Federator.publish(:_))
375 end
376
377 refute Activity.get_by_id(post.id)
378 end
379 end
380
381 test "favoriting race condition" do
382 user = insert(:user)
383 users_serial = insert_list(10, :user)
384 users = insert_list(10, :user)
385
386 {:ok, activity} = CommonAPI.post(user, %{status: "."})
387
388 users_serial
389 |> Enum.map(fn user ->
390 CommonAPI.favorite(user, activity.id)
391 end)
392
393 object = Object.get_by_ap_id(activity.data["object"])
394 assert object.data["like_count"] == 10
395
396 users
397 |> Enum.map(fn user ->
398 Task.async(fn ->
399 CommonAPI.favorite(user, activity.id)
400 end)
401 end)
402 |> Enum.map(&Task.await/1)
403
404 object = Object.get_by_ap_id(activity.data["object"])
405 assert object.data["like_count"] == 20
406 end
407
408 test "repeating race condition" do
409 user = insert(:user)
410 users_serial = insert_list(10, :user)
411 users = insert_list(10, :user)
412
413 {:ok, activity} = CommonAPI.post(user, %{status: "."})
414
415 users_serial
416 |> Enum.map(fn user ->
417 CommonAPI.repeat(activity.id, user)
418 end)
419
420 object = Object.get_by_ap_id(activity.data["object"])
421 assert object.data["announcement_count"] == 10
422
423 users
424 |> Enum.map(fn user ->
425 Task.async(fn ->
426 CommonAPI.repeat(activity.id, user)
427 end)
428 end)
429 |> Enum.map(&Task.await/1)
430
431 object = Object.get_by_ap_id(activity.data["object"])
432 assert object.data["announcement_count"] == 20
433 end
434
435 test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
436 user = insert(:user)
437 {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
438
439 [participation] = Participation.for_user(user)
440
441 {:ok, convo_reply} =
442 CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id})
443
444 assert Visibility.is_direct?(convo_reply)
445
446 assert activity.data["context"] == convo_reply.data["context"]
447 end
448
449 test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do
450 har = insert(:user)
451 jafnhar = insert(:user)
452 tridi = insert(:user)
453
454 {:ok, activity} =
455 CommonAPI.post(har, %{
456 status: "@#{jafnhar.nickname} hey",
457 visibility: "direct"
458 })
459
460 assert har.ap_id in activity.recipients
461 assert jafnhar.ap_id in activity.recipients
462
463 [participation] = Participation.for_user(har)
464
465 {:ok, activity} =
466 CommonAPI.post(har, %{
467 status: "I don't really like @#{tridi.nickname}",
468 visibility: "direct",
469 in_reply_to_status_id: activity.id,
470 in_reply_to_conversation_id: participation.id
471 })
472
473 assert har.ap_id in activity.recipients
474 assert jafnhar.ap_id in activity.recipients
475 refute tridi.ap_id in activity.recipients
476 end
477
478 test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
479 har = insert(:user)
480 jafnhar = insert(:user)
481 tridi = insert(:user)
482
483 clear_config([:instance, :safe_dm_mentions], true)
484
485 {:ok, activity} =
486 CommonAPI.post(har, %{
487 status: "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again",
488 visibility: "direct"
489 })
490
491 refute tridi.ap_id in activity.recipients
492 assert jafnhar.ap_id in activity.recipients
493 end
494
495 test "it de-duplicates tags" do
496 user = insert(:user)
497 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU"})
498
499 object = Object.normalize(activity, fetch: false)
500
501 assert Object.tags(object) == ["2hu"]
502 end
503
504 test "it adds emoji in the object" do
505 user = insert(:user)
506 {:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"})
507
508 assert Object.normalize(activity, fetch: false).data["emoji"]["firefox"]
509 end
510
511 describe "posting" do
512 test "it adds an emoji on an external site" do
513 user = insert(:user)
514 {:ok, activity} = CommonAPI.post(user, %{status: "hey :external_emoji:"})
515
516 assert %{"external_emoji" => url} = Object.normalize(activity).data["emoji"]
517 assert url == "https://example.com/emoji.png"
518
519 {:ok, activity} = CommonAPI.post(user, %{status: "hey :blank:"})
520
521 assert %{"blank" => url} = Object.normalize(activity).data["emoji"]
522 assert url == "#{Pleroma.Web.base_url()}/emoji/blank.png"
523 end
524
525 test "it copies emoji from the subject of the parent post" do
526 %Object{} =
527 object =
528 Object.normalize("https://patch.cx/objects/a399c28e-c821-4820-bc3e-4afeb044c16f",
529 fetch: true
530 )
531
532 activity = Activity.get_create_by_object_ap_id(object.data["id"])
533 user = insert(:user)
534
535 {:ok, reply_activity} =
536 CommonAPI.post(user, %{
537 in_reply_to_id: activity.id,
538 status: ":joker_disapprove:",
539 spoiler_text: ":joker_smile:"
540 })
541
542 assert Object.normalize(reply_activity).data["emoji"]["joker_smile"]
543 refute Object.normalize(reply_activity).data["emoji"]["joker_disapprove"]
544 end
545
546 test "deactivated users can't post" do
547 user = insert(:user, is_active: false)
548 assert {:error, _} = CommonAPI.post(user, %{status: "ye"})
549 end
550
551 test "it supports explicit addressing" do
552 user = insert(:user)
553 user_two = insert(:user)
554 user_three = insert(:user)
555 user_four = insert(:user)
556
557 {:ok, activity} =
558 CommonAPI.post(user, %{
559 status:
560 "Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.",
561 to: [user_two.nickname, user_four.nickname, "nonexistent"]
562 })
563
564 assert user.ap_id in activity.recipients
565 assert user_two.ap_id in activity.recipients
566 assert user_four.ap_id in activity.recipients
567 refute user_three.ap_id in activity.recipients
568 end
569
570 test "it filters out obviously bad tags when accepting a post as HTML" do
571 user = insert(:user)
572
573 post = "<p><b>2hu</b></p><script>alert('xss')</script>"
574
575 {:ok, activity} =
576 CommonAPI.post(user, %{
577 status: post,
578 content_type: "text/html"
579 })
580
581 object = Object.normalize(activity, fetch: false)
582
583 assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)"
584 assert object.data["source"] == post
585 end
586
587 test "it filters out obviously bad tags when accepting a post as Markdown" do
588 user = insert(:user)
589
590 post = "<p><b>2hu</b></p><script>alert('xss')</script>"
591
592 {:ok, activity} =
593 CommonAPI.post(user, %{
594 status: post,
595 content_type: "text/markdown"
596 })
597
598 object = Object.normalize(activity, fetch: false)
599
600 assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)"
601 assert object.data["source"] == post
602 end
603
604 test "it does not allow replies to direct messages that are not direct messages themselves" do
605 user = insert(:user)
606
607 {:ok, activity} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
608
609 assert {:ok, _} =
610 CommonAPI.post(user, %{
611 status: "suya..",
612 visibility: "direct",
613 in_reply_to_status_id: activity.id
614 })
615
616 Enum.each(["public", "private", "unlisted"], fn visibility ->
617 assert {:error, "The message visibility must be direct"} =
618 CommonAPI.post(user, %{
619 status: "suya..",
620 visibility: visibility,
621 in_reply_to_status_id: activity.id
622 })
623 end)
624 end
625
626 test "replying with a direct message will NOT auto-add the author of the reply to the recipient list" do
627 user = insert(:user)
628 other_user = insert(:user)
629 third_user = insert(:user)
630
631 {:ok, post} = CommonAPI.post(user, %{status: "I'm stupid"})
632
633 {:ok, open_answer} =
634 CommonAPI.post(other_user, %{status: "No ur smart", in_reply_to_status_id: post.id})
635
636 # The OP is implicitly added
637 assert user.ap_id in open_answer.recipients
638
639 {:ok, secret_answer} =
640 CommonAPI.post(other_user, %{
641 status: "lol, that guy really is stupid, right, @#{third_user.nickname}?",
642 in_reply_to_status_id: post.id,
643 visibility: "direct"
644 })
645
646 assert third_user.ap_id in secret_answer.recipients
647
648 # The OP is not added
649 refute user.ap_id in secret_answer.recipients
650 end
651
652 test "it allows to address a list" do
653 user = insert(:user)
654 {:ok, list} = Pleroma.List.create("foo", user)
655
656 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
657
658 assert activity.data["bcc"] == [list.ap_id]
659 assert activity.recipients == [list.ap_id, user.ap_id]
660 assert activity.data["listMessage"] == list.ap_id
661 end
662
663 test "it returns error when status is empty and no attachments" do
664 user = insert(:user)
665
666 assert {:error, "Cannot post an empty status without attachments"} =
667 CommonAPI.post(user, %{status: ""})
668 end
669
670 test "it validates character limits are correctly enforced" do
671 clear_config([:instance, :limit], 5)
672
673 user = insert(:user)
674
675 assert {:error, "The status is over the character limit"} =
676 CommonAPI.post(user, %{status: "foobar"})
677
678 assert {:ok, _activity} = CommonAPI.post(user, %{status: "12345"})
679 end
680
681 test "it can handle activities that expire" do
682 user = insert(:user)
683
684 expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
685
686 assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000})
687
688 assert_enqueued(
689 worker: Pleroma.Workers.PurgeExpiredActivity,
690 args: %{activity_id: activity.id},
691 scheduled_at: expires_at
692 )
693 end
694 end
695
696 describe "reactions" do
697 test "reacting to a status with an emoji" do
698 user = insert(:user)
699 other_user = insert(:user)
700
701 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
702
703 {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
704
705 assert reaction.data["actor"] == user.ap_id
706 assert reaction.data["content"] == "👍"
707
708 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
709
710 {:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".")
711 end
712
713 test "unreacting to a status with an emoji" do
714 user = insert(:user)
715 other_user = insert(:user)
716
717 clear_config([:instance, :federating], true)
718
719 with_mock Pleroma.Web.Federator,
720 publish: fn _ -> nil end do
721 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
722 {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
723
724 {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
725
726 assert unreaction.data["type"] == "Undo"
727 assert unreaction.data["object"] == reaction.data["id"]
728 assert unreaction.local
729
730 # On federation, it contains the undone (and deleted) object
731 unreaction_with_object = %{
732 unreaction
733 | data: Map.put(unreaction.data, "object", reaction.data)
734 }
735
736 assert called(Pleroma.Web.Federator.publish(unreaction_with_object))
737 end
738 end
739
740 test "repeating a status" do
741 user = insert(:user)
742 other_user = insert(:user)
743
744 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
745
746 {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user)
747 assert Visibility.is_public?(announce_activity)
748 end
749
750 test "can't repeat a repeat" do
751 user = insert(:user)
752 other_user = insert(:user)
753 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
754
755 {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user)
756
757 refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user))
758 end
759
760 test "repeating a status privately" do
761 user = insert(:user)
762 other_user = insert(:user)
763
764 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
765
766 {:ok, %Activity{} = announce_activity} =
767 CommonAPI.repeat(activity.id, user, %{visibility: "private"})
768
769 assert Visibility.is_private?(announce_activity)
770 refute Visibility.visible_for_user?(announce_activity, nil)
771 end
772
773 test "author can repeat own private statuses" do
774 author = insert(:user)
775 follower = insert(:user)
776 CommonAPI.follow(follower, author)
777
778 {:ok, activity} = CommonAPI.post(author, %{status: "cofe", visibility: "private"})
779
780 {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, author)
781
782 assert Visibility.is_private?(announce_activity)
783 refute Visibility.visible_for_user?(announce_activity, nil)
784
785 assert Visibility.visible_for_user?(activity, follower)
786 assert {:error, :not_found} = CommonAPI.repeat(activity.id, follower)
787 end
788
789 test "favoriting a status" do
790 user = insert(:user)
791 other_user = insert(:user)
792
793 {:ok, post_activity} = CommonAPI.post(other_user, %{status: "cofe"})
794
795 {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id)
796 assert data["type"] == "Like"
797 assert data["actor"] == user.ap_id
798 assert data["object"] == post_activity.data["object"]
799 end
800
801 test "retweeting a status twice returns the status" do
802 user = insert(:user)
803 other_user = insert(:user)
804
805 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
806 {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user)
807 {:ok, ^announce} = CommonAPI.repeat(activity.id, user)
808 end
809
810 test "favoriting a status twice returns ok, but without the like activity" do
811 user = insert(:user)
812 other_user = insert(:user)
813
814 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
815 {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
816 assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id)
817 end
818 end
819
820 describe "pinned statuses" do
821 setup do
822 clear_config([:instance, :max_pinned_statuses], 1)
823
824 user = insert(:user)
825 {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
826
827 [user: user, activity: activity]
828 end
829
830 test "pin status", %{user: user, activity: activity} do
831 assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
832
833 id = activity.id
834 user = refresh_record(user)
835
836 assert %User{pinned_activities: [^id]} = user
837 end
838
839 test "pin poll", %{user: user} do
840 {:ok, activity} =
841 CommonAPI.post(user, %{
842 status: "How is fediverse today?",
843 poll: %{options: ["Absolutely outstanding", "Not good"], expires_in: 20}
844 })
845
846 assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
847
848 id = activity.id
849 user = refresh_record(user)
850
851 assert %User{pinned_activities: [^id]} = user
852 end
853
854 test "unlisted statuses can be pinned", %{user: user} do
855 {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!", visibility: "unlisted"})
856 assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
857 end
858
859 test "only self-authored can be pinned", %{activity: activity} do
860 user = insert(:user)
861
862 assert {:error, "Could not pin"} = CommonAPI.pin(activity.id, user)
863 end
864
865 test "max pinned statuses", %{user: user, activity: activity_one} do
866 {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
867
868 assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user)
869
870 user = refresh_record(user)
871
872 assert {:error, "You have already pinned the maximum number of statuses"} =
873 CommonAPI.pin(activity_two.id, user)
874 end
875
876 test "unpin status", %{user: user, activity: activity} do
877 {:ok, activity} = CommonAPI.pin(activity.id, user)
878
879 user = refresh_record(user)
880
881 id = activity.id
882
883 assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user))
884
885 user = refresh_record(user)
886
887 assert %User{pinned_activities: []} = user
888 end
889
890 test "should unpin when deleting a status", %{user: user, activity: activity} do
891 {:ok, activity} = CommonAPI.pin(activity.id, user)
892
893 user = refresh_record(user)
894
895 assert {:ok, _} = CommonAPI.delete(activity.id, user)
896
897 user = refresh_record(user)
898
899 assert %User{pinned_activities: []} = user
900 end
901 end
902
903 describe "mute tests" do
904 setup do
905 user = insert(:user)
906
907 activity = insert(:note_activity)
908
909 [user: user, activity: activity]
910 end
911
912 test "marks notifications as read after mute" do
913 author = insert(:user)
914 activity = insert(:note_activity, user: author)
915
916 friend1 = insert(:user)
917 friend2 = insert(:user)
918
919 {:ok, reply_activity} =
920 CommonAPI.post(
921 friend2,
922 %{
923 status: "@#{author.nickname} @#{friend1.nickname} test reply",
924 in_reply_to_status_id: activity.id
925 }
926 )
927
928 {:ok, favorite_activity} = CommonAPI.favorite(friend2, activity.id)
929 {:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1)
930
931 assert Repo.aggregate(
932 from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id),
933 :count
934 ) == 1
935
936 unread_notifications =
937 Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id))
938
939 assert Enum.any?(unread_notifications, fn n ->
940 n.type == "favourite" && n.activity_id == favorite_activity.id
941 end)
942
943 assert Enum.any?(unread_notifications, fn n ->
944 n.type == "reblog" && n.activity_id == repeat_activity.id
945 end)
946
947 assert Enum.any?(unread_notifications, fn n ->
948 n.type == "mention" && n.activity_id == reply_activity.id
949 end)
950
951 {:ok, _} = CommonAPI.add_mute(author, activity)
952 assert CommonAPI.thread_muted?(author, activity)
953
954 assert Repo.aggregate(
955 from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id),
956 :count
957 ) == 1
958
959 read_notifications =
960 Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id))
961
962 assert Enum.any?(read_notifications, fn n ->
963 n.type == "favourite" && n.activity_id == favorite_activity.id
964 end)
965
966 assert Enum.any?(read_notifications, fn n ->
967 n.type == "reblog" && n.activity_id == repeat_activity.id
968 end)
969
970 assert Enum.any?(read_notifications, fn n ->
971 n.type == "mention" && n.activity_id == reply_activity.id
972 end)
973 end
974
975 test "add mute", %{user: user, activity: activity} do
976 {:ok, _} = CommonAPI.add_mute(user, activity)
977 assert CommonAPI.thread_muted?(user, activity)
978 end
979
980 test "add expiring mute", %{user: user, activity: activity} do
981 {:ok, _} = CommonAPI.add_mute(user, activity, %{expires_in: 60})
982 assert CommonAPI.thread_muted?(user, activity)
983
984 worker = Pleroma.Workers.MuteExpireWorker
985 args = %{"op" => "unmute_conversation", "user_id" => user.id, "activity_id" => activity.id}
986
987 assert_enqueued(
988 worker: worker,
989 args: args
990 )
991
992 assert :ok = perform_job(worker, args)
993 refute CommonAPI.thread_muted?(user, activity)
994 end
995
996 test "remove mute", %{user: user, activity: activity} do
997 CommonAPI.add_mute(user, activity)
998 {:ok, _} = CommonAPI.remove_mute(user, activity)
999 refute CommonAPI.thread_muted?(user, activity)
1000 end
1001
1002 test "remove mute by ids", %{user: user, activity: activity} do
1003 CommonAPI.add_mute(user, activity)
1004 {:ok, _} = CommonAPI.remove_mute(user.id, activity.id)
1005 refute CommonAPI.thread_muted?(user, activity)
1006 end
1007
1008 test "check that mutes can't be duplicate", %{user: user, activity: activity} do
1009 CommonAPI.add_mute(user, activity)
1010 {:error, _} = CommonAPI.add_mute(user, activity)
1011 end
1012 end
1013
1014 describe "reports" do
1015 test "creates a report" do
1016 reporter = insert(:user)
1017 target_user = insert(:user)
1018
1019 {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"})
1020
1021 reporter_ap_id = reporter.ap_id
1022 target_ap_id = target_user.ap_id
1023 activity_ap_id = activity.data["id"]
1024 comment = "foobar"
1025
1026 report_data = %{
1027 account_id: target_user.id,
1028 comment: comment,
1029 status_ids: [activity.id]
1030 }
1031
1032 note_obj = %{
1033 "type" => "Note",
1034 "id" => activity_ap_id,
1035 "content" => "foobar",
1036 "published" => activity.object.data["published"],
1037 "actor" => AccountView.render("show.json", %{user: target_user})
1038 }
1039
1040 assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
1041
1042 assert %Activity{
1043 actor: ^reporter_ap_id,
1044 data: %{
1045 "type" => "Flag",
1046 "content" => ^comment,
1047 "object" => [^target_ap_id, ^note_obj],
1048 "state" => "open"
1049 }
1050 } = flag_activity
1051 end
1052
1053 test "updates report state" do
1054 [reporter, target_user] = insert_pair(:user)
1055 activity = insert(:note_activity, user: target_user)
1056
1057 {:ok, %Activity{id: report_id}} =
1058 CommonAPI.report(reporter, %{
1059 account_id: target_user.id,
1060 comment: "I feel offended",
1061 status_ids: [activity.id]
1062 })
1063
1064 {:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
1065
1066 assert report.data["state"] == "resolved"
1067
1068 [reported_user, activity_id] = report.data["object"]
1069
1070 assert reported_user == target_user.ap_id
1071 assert activity_id == activity.data["id"]
1072 end
1073
1074 test "does not update report state when state is unsupported" do
1075 [reporter, target_user] = insert_pair(:user)
1076 activity = insert(:note_activity, user: target_user)
1077
1078 {:ok, %Activity{id: report_id}} =
1079 CommonAPI.report(reporter, %{
1080 account_id: target_user.id,
1081 comment: "I feel offended",
1082 status_ids: [activity.id]
1083 })
1084
1085 assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
1086 end
1087
1088 test "updates state of multiple reports" do
1089 [reporter, target_user] = insert_pair(:user)
1090 activity = insert(:note_activity, user: target_user)
1091
1092 {:ok, %Activity{id: first_report_id}} =
1093 CommonAPI.report(reporter, %{
1094 account_id: target_user.id,
1095 comment: "I feel offended",
1096 status_ids: [activity.id]
1097 })
1098
1099 {:ok, %Activity{id: second_report_id}} =
1100 CommonAPI.report(reporter, %{
1101 account_id: target_user.id,
1102 comment: "I feel very offended!",
1103 status_ids: [activity.id]
1104 })
1105
1106 {:ok, report_ids} =
1107 CommonAPI.update_report_state([first_report_id, second_report_id], "resolved")
1108
1109 first_report = Activity.get_by_id(first_report_id)
1110 second_report = Activity.get_by_id(second_report_id)
1111
1112 assert report_ids -- [first_report_id, second_report_id] == []
1113 assert first_report.data["state"] == "resolved"
1114 assert second_report.data["state"] == "resolved"
1115 end
1116 end
1117
1118 describe "reblog muting" do
1119 setup do
1120 muter = insert(:user)
1121
1122 muted = insert(:user)
1123
1124 [muter: muter, muted: muted]
1125 end
1126
1127 test "add a reblog mute", %{muter: muter, muted: muted} do
1128 {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
1129
1130 assert User.showing_reblogs?(muter, muted) == false
1131 end
1132
1133 test "remove a reblog mute", %{muter: muter, muted: muted} do
1134 {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
1135 {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted)
1136
1137 assert User.showing_reblogs?(muter, muted) == true
1138 end
1139 end
1140
1141 describe "follow/2" do
1142 test "directly follows a non-locked local user" do
1143 [follower, followed] = insert_pair(:user)
1144 {:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
1145
1146 assert User.following?(follower, followed)
1147 end
1148 end
1149
1150 describe "unfollow/2" do
1151 test "also unsubscribes a user" do
1152 [follower, followed] = insert_pair(:user)
1153 {:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
1154 {:ok, _subscription} = User.subscribe(follower, followed)
1155
1156 assert User.subscribed_to?(follower, followed)
1157
1158 {:ok, follower} = CommonAPI.unfollow(follower, followed)
1159
1160 refute User.subscribed_to?(follower, followed)
1161 end
1162
1163 test "cancels a pending follow for a local user" do
1164 follower = insert(:user)
1165 followed = insert(:user, is_locked: true)
1166
1167 assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} =
1168 CommonAPI.follow(follower, followed)
1169
1170 assert User.get_follow_state(follower, followed) == :follow_pending
1171 assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
1172 assert User.get_follow_state(follower, followed) == nil
1173
1174 assert %{id: ^activity_id, data: %{"state" => "cancelled"}} =
1175 Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed)
1176
1177 assert %{
1178 data: %{
1179 "type" => "Undo",
1180 "object" => %{"type" => "Follow", "state" => "cancelled"}
1181 }
1182 } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower)
1183 end
1184
1185 test "cancels a pending follow for a remote user" do
1186 follower = insert(:user)
1187 followed = insert(:user, is_locked: true, local: false, ap_enabled: true)
1188
1189 assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} =
1190 CommonAPI.follow(follower, followed)
1191
1192 assert User.get_follow_state(follower, followed) == :follow_pending
1193 assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
1194 assert User.get_follow_state(follower, followed) == nil
1195
1196 assert %{id: ^activity_id, data: %{"state" => "cancelled"}} =
1197 Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed)
1198
1199 assert %{
1200 data: %{
1201 "type" => "Undo",
1202 "object" => %{"type" => "Follow", "state" => "cancelled"}
1203 }
1204 } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower)
1205 end
1206 end
1207
1208 describe "accept_follow_request/2" do
1209 test "after acceptance, it sets all existing pending follow request states to 'accept'" do
1210 user = insert(:user, is_locked: true)
1211 follower = insert(:user)
1212 follower_two = insert(:user)
1213
1214 {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
1215 {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
1216 {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user)
1217
1218 assert follow_activity.data["state"] == "pending"
1219 assert follow_activity_two.data["state"] == "pending"
1220 assert follow_activity_three.data["state"] == "pending"
1221
1222 {:ok, _follower} = CommonAPI.accept_follow_request(follower, user)
1223
1224 assert Repo.get(Activity, follow_activity.id).data["state"] == "accept"
1225 assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept"
1226 assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
1227 end
1228
1229 test "after rejection, it sets all existing pending follow request states to 'reject'" do
1230 user = insert(:user, is_locked: true)
1231 follower = insert(:user)
1232 follower_two = insert(:user)
1233
1234 {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
1235 {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
1236 {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user)
1237
1238 assert follow_activity.data["state"] == "pending"
1239 assert follow_activity_two.data["state"] == "pending"
1240 assert follow_activity_three.data["state"] == "pending"
1241
1242 {:ok, _follower} = CommonAPI.reject_follow_request(follower, user)
1243
1244 assert Repo.get(Activity, follow_activity.id).data["state"] == "reject"
1245 assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
1246 assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
1247 end
1248
1249 test "doesn't create a following relationship if the corresponding follow request doesn't exist" do
1250 user = insert(:user, is_locked: true)
1251 not_follower = insert(:user)
1252 CommonAPI.accept_follow_request(not_follower, user)
1253
1254 assert Pleroma.FollowingRelationship.following?(not_follower, user) == false
1255 end
1256 end
1257
1258 describe "vote/3" do
1259 test "does not allow to vote twice" do
1260 user = insert(:user)
1261 other_user = insert(:user)
1262
1263 {:ok, activity} =
1264 CommonAPI.post(user, %{
1265 status: "Am I cute?",
1266 poll: %{options: ["Yes", "No"], expires_in: 20}
1267 })
1268
1269 object = Object.normalize(activity, fetch: false)
1270
1271 {:ok, _, object} = CommonAPI.vote(other_user, object, [0])
1272
1273 assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1])
1274 end
1275 end
1276
1277 describe "listen/2" do
1278 test "returns a valid activity" do
1279 user = insert(:user)
1280
1281 {:ok, activity} =
1282 CommonAPI.listen(user, %{
1283 title: "lain radio episode 1",
1284 album: "lain radio",
1285 artist: "lain",
1286 length: 180_000
1287 })
1288
1289 object = Object.normalize(activity, fetch: false)
1290
1291 assert object.data["title"] == "lain radio episode 1"
1292
1293 assert Visibility.get_visibility(activity) == "public"
1294 end
1295
1296 test "respects visibility=private" do
1297 user = insert(:user)
1298
1299 {:ok, activity} =
1300 CommonAPI.listen(user, %{
1301 title: "lain radio episode 1",
1302 album: "lain radio",
1303 artist: "lain",
1304 length: 180_000,
1305 visibility: "private"
1306 })
1307
1308 object = Object.normalize(activity, fetch: false)
1309
1310 assert object.data["title"] == "lain radio episode 1"
1311
1312 assert Visibility.get_visibility(activity) == "private"
1313 end
1314 end
1315
1316 describe "get_user/1" do
1317 test "gets user by ap_id" do
1318 user = insert(:user)
1319 assert CommonAPI.get_user(user.ap_id) == user
1320 end
1321
1322 test "gets user by guessed nickname" do
1323 user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom")
1324 assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user
1325 end
1326
1327 test "fallback" do
1328 assert %User{
1329 name: "",
1330 ap_id: "",
1331 nickname: "erroruser@example.com"
1332 } = CommonAPI.get_user("")
1333 end
1334 end
1335
1336 describe "with `local` visibility" do
1337 setup do: clear_config([:instance, :federating], true)
1338
1339 test "post" do
1340 user = insert(:user)
1341
1342 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1343 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
1344
1345 assert Visibility.is_local_public?(activity)
1346 assert_not_called(Pleroma.Web.Federator.publish(activity))
1347 end
1348 end
1349
1350 test "delete" do
1351 user = insert(:user)
1352
1353 {:ok, %Activity{id: activity_id}} =
1354 CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
1355
1356 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1357 assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} =
1358 CommonAPI.delete(activity_id, user)
1359
1360 assert Visibility.is_local_public?(activity)
1361 assert_not_called(Pleroma.Web.Federator.publish(activity))
1362 end
1363 end
1364
1365 test "repeat" do
1366 user = insert(:user)
1367 other_user = insert(:user)
1368
1369 {:ok, %Activity{id: activity_id}} =
1370 CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1371
1372 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1373 assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} =
1374 CommonAPI.repeat(activity_id, user)
1375
1376 assert Visibility.is_local_public?(activity)
1377 refute called(Pleroma.Web.Federator.publish(activity))
1378 end
1379 end
1380
1381 test "unrepeat" do
1382 user = insert(:user)
1383 other_user = insert(:user)
1384
1385 {:ok, %Activity{id: activity_id}} =
1386 CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1387
1388 assert {:ok, _} = CommonAPI.repeat(activity_id, user)
1389
1390 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1391 assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
1392 CommonAPI.unrepeat(activity_id, user)
1393
1394 assert Visibility.is_local_public?(activity)
1395 refute called(Pleroma.Web.Federator.publish(activity))
1396 end
1397 end
1398
1399 test "favorite" do
1400 user = insert(:user)
1401 other_user = insert(:user)
1402
1403 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1404
1405 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1406 assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} =
1407 CommonAPI.favorite(user, activity.id)
1408
1409 assert Visibility.is_local_public?(activity)
1410 refute called(Pleroma.Web.Federator.publish(activity))
1411 end
1412 end
1413
1414 test "unfavorite" do
1415 user = insert(:user)
1416 other_user = insert(:user)
1417
1418 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1419
1420 {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
1421
1422 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1423 assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user)
1424 assert Visibility.is_local_public?(activity)
1425 refute called(Pleroma.Web.Federator.publish(activity))
1426 end
1427 end
1428
1429 test "react_with_emoji" do
1430 user = insert(:user)
1431 other_user = insert(:user)
1432 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1433
1434 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1435 assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} =
1436 CommonAPI.react_with_emoji(activity.id, user, "👍")
1437
1438 assert Visibility.is_local_public?(activity)
1439 refute called(Pleroma.Web.Federator.publish(activity))
1440 end
1441 end
1442
1443 test "unreact_with_emoji" do
1444 user = insert(:user)
1445 other_user = insert(:user)
1446 {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
1447
1448 {:ok, _reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
1449
1450 with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
1451 assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
1452 CommonAPI.unreact_with_emoji(activity.id, user, "👍")
1453
1454 assert Visibility.is_local_public?(activity)
1455 refute called(Pleroma.Web.Federator.publish(activity))
1456 end
1457 end
1458 end
1459 end