DELETE /api/pleroma/admin/users now accepts nicknames array
[akkoma] / test / web / activity_pub / activity_pub_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
6 use Pleroma.DataCase
7 alias Pleroma.Activity
8 alias Pleroma.Builders.ActivityBuilder
9 alias Pleroma.Object
10 alias Pleroma.User
11 alias Pleroma.Web.ActivityPub.ActivityPub
12 alias Pleroma.Web.ActivityPub.Utils
13 alias Pleroma.Web.CommonAPI
14
15 import Pleroma.Factory
16 import Tesla.Mock
17 import Mock
18
19 setup do
20 mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
21 :ok
22 end
23
24 clear_config([:instance, :federating])
25
26 describe "streaming out participations" do
27 test "it streams them out" do
28 user = insert(:user)
29 {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
30
31 {:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity)
32
33 participations =
34 conversation.participations
35 |> Repo.preload(:user)
36
37 with_mock Pleroma.Web.Streamer,
38 stream: fn _, _ -> nil end do
39 ActivityPub.stream_out_participations(conversation.participations)
40
41 assert called(Pleroma.Web.Streamer.stream("participation", participations))
42 end
43 end
44 end
45
46 describe "fetching restricted by visibility" do
47 test "it restricts by the appropriate visibility" do
48 user = insert(:user)
49
50 {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
51
52 {:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
53
54 {:ok, unlisted_activity} =
55 CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
56
57 {:ok, private_activity} =
58 CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
59
60 activities =
61 ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id})
62
63 assert activities == [direct_activity]
64
65 activities =
66 ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id})
67
68 assert activities == [unlisted_activity]
69
70 activities =
71 ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id})
72
73 assert activities == [private_activity]
74
75 activities =
76 ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id})
77
78 assert activities == [public_activity]
79
80 activities =
81 ActivityPub.fetch_activities([], %{
82 :visibility => ~w[private public],
83 "actor_id" => user.ap_id
84 })
85
86 assert activities == [public_activity, private_activity]
87 end
88 end
89
90 describe "fetching excluded by visibility" do
91 test "it excludes by the appropriate visibility" do
92 user = insert(:user)
93
94 {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
95
96 {:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
97
98 {:ok, unlisted_activity} =
99 CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
100
101 {:ok, private_activity} =
102 CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
103
104 activities =
105 ActivityPub.fetch_activities([], %{
106 "exclude_visibilities" => "direct",
107 "actor_id" => user.ap_id
108 })
109
110 assert public_activity in activities
111 assert unlisted_activity in activities
112 assert private_activity in activities
113 refute direct_activity in activities
114
115 activities =
116 ActivityPub.fetch_activities([], %{
117 "exclude_visibilities" => "unlisted",
118 "actor_id" => user.ap_id
119 })
120
121 assert public_activity in activities
122 refute unlisted_activity in activities
123 assert private_activity in activities
124 assert direct_activity in activities
125
126 activities =
127 ActivityPub.fetch_activities([], %{
128 "exclude_visibilities" => "private",
129 "actor_id" => user.ap_id
130 })
131
132 assert public_activity in activities
133 assert unlisted_activity in activities
134 refute private_activity in activities
135 assert direct_activity in activities
136
137 activities =
138 ActivityPub.fetch_activities([], %{
139 "exclude_visibilities" => "public",
140 "actor_id" => user.ap_id
141 })
142
143 refute public_activity in activities
144 assert unlisted_activity in activities
145 assert private_activity in activities
146 assert direct_activity in activities
147 end
148 end
149
150 describe "building a user from his ap id" do
151 test "it returns a user" do
152 user_id = "http://mastodon.example.org/users/admin"
153 {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
154 assert user.ap_id == user_id
155 assert user.nickname == "admin@mastodon.example.org"
156 assert user.info.source_data
157 assert user.info.ap_enabled
158 assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
159 end
160
161 test "it fetches the appropriate tag-restricted posts" do
162 user = insert(:user)
163
164 {:ok, status_one} = CommonAPI.post(user, %{"status" => ". #test"})
165 {:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"})
166 {:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"})
167
168 fetch_one = ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => "test"})
169
170 fetch_two =
171 ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => ["test", "essais"]})
172
173 fetch_three =
174 ActivityPub.fetch_activities([], %{
175 "type" => "Create",
176 "tag" => ["test", "essais"],
177 "tag_reject" => ["reject"]
178 })
179
180 fetch_four =
181 ActivityPub.fetch_activities([], %{
182 "type" => "Create",
183 "tag" => ["test"],
184 "tag_all" => ["test", "reject"]
185 })
186
187 assert fetch_one == [status_one, status_three]
188 assert fetch_two == [status_one, status_two, status_three]
189 assert fetch_three == [status_one, status_two]
190 assert fetch_four == [status_three]
191 end
192 end
193
194 describe "insertion" do
195 test "drops activities beyond a certain limit" do
196 limit = Pleroma.Config.get([:instance, :remote_limit])
197
198 random_text =
199 :crypto.strong_rand_bytes(limit + 1)
200 |> Base.encode64()
201 |> binary_part(0, limit + 1)
202
203 data = %{
204 "ok" => true,
205 "object" => %{
206 "content" => random_text
207 }
208 }
209
210 assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data)
211 end
212
213 test "doesn't drop activities with content being null" do
214 user = insert(:user)
215
216 data = %{
217 "actor" => user.ap_id,
218 "to" => [],
219 "object" => %{
220 "actor" => user.ap_id,
221 "to" => [],
222 "type" => "Note",
223 "content" => nil
224 }
225 }
226
227 assert {:ok, _} = ActivityPub.insert(data)
228 end
229
230 test "returns the activity if one with the same id is already in" do
231 activity = insert(:note_activity)
232 {:ok, new_activity} = ActivityPub.insert(activity.data)
233
234 assert activity.id == new_activity.id
235 end
236
237 test "inserts a given map into the activity database, giving it an id if it has none." do
238 user = insert(:user)
239
240 data = %{
241 "actor" => user.ap_id,
242 "to" => [],
243 "object" => %{
244 "actor" => user.ap_id,
245 "to" => [],
246 "type" => "Note",
247 "content" => "hey"
248 }
249 }
250
251 {:ok, %Activity{} = activity} = ActivityPub.insert(data)
252 assert activity.data["ok"] == data["ok"]
253 assert is_binary(activity.data["id"])
254
255 given_id = "bla"
256
257 data = %{
258 "id" => given_id,
259 "actor" => user.ap_id,
260 "to" => [],
261 "context" => "blabla",
262 "object" => %{
263 "actor" => user.ap_id,
264 "to" => [],
265 "type" => "Note",
266 "content" => "hey"
267 }
268 }
269
270 {:ok, %Activity{} = activity} = ActivityPub.insert(data)
271 assert activity.data["ok"] == data["ok"]
272 assert activity.data["id"] == given_id
273 assert activity.data["context"] == "blabla"
274 assert activity.data["context_id"]
275 end
276
277 test "adds a context when none is there" do
278 user = insert(:user)
279
280 data = %{
281 "actor" => user.ap_id,
282 "to" => [],
283 "object" => %{
284 "actor" => user.ap_id,
285 "to" => [],
286 "type" => "Note",
287 "content" => "hey"
288 }
289 }
290
291 {:ok, %Activity{} = activity} = ActivityPub.insert(data)
292 object = Pleroma.Object.normalize(activity)
293
294 assert is_binary(activity.data["context"])
295 assert is_binary(object.data["context"])
296 assert activity.data["context_id"]
297 assert object.data["context_id"]
298 end
299
300 test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do
301 user = insert(:user)
302
303 data = %{
304 "actor" => user.ap_id,
305 "to" => [],
306 "object" => %{
307 "actor" => user.ap_id,
308 "to" => [],
309 "type" => "Note",
310 "content" => "hey"
311 }
312 }
313
314 {:ok, %Activity{} = activity} = ActivityPub.insert(data)
315 assert object = Object.normalize(activity)
316 assert is_binary(object.data["id"])
317 end
318 end
319
320 describe "listen activities" do
321 test "does not increase user note count" do
322 user = insert(:user)
323
324 {:ok, activity} =
325 ActivityPub.listen(%{
326 to: ["https://www.w3.org/ns/activitystreams#Public"],
327 actor: user,
328 context: "",
329 object: %{
330 "actor" => user.ap_id,
331 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
332 "artist" => "lain",
333 "title" => "lain radio episode 1",
334 "length" => 180_000,
335 "type" => "Audio"
336 }
337 })
338
339 assert activity.actor == user.ap_id
340
341 user = User.get_cached_by_id(user.id)
342 assert user.info.note_count == 0
343 end
344
345 test "can be fetched into a timeline" do
346 _listen_activity_1 = insert(:listen)
347 _listen_activity_2 = insert(:listen)
348 _listen_activity_3 = insert(:listen)
349
350 timeline = ActivityPub.fetch_activities([], %{"type" => ["Listen"]})
351
352 assert length(timeline) == 3
353 end
354 end
355
356 describe "create activities" do
357 test "removes doubled 'to' recipients" do
358 user = insert(:user)
359
360 {:ok, activity} =
361 ActivityPub.create(%{
362 to: ["user1", "user1", "user2"],
363 actor: user,
364 context: "",
365 object: %{
366 "to" => ["user1", "user1", "user2"],
367 "type" => "Note",
368 "content" => "testing"
369 }
370 })
371
372 assert activity.data["to"] == ["user1", "user2"]
373 assert activity.actor == user.ap_id
374 assert activity.recipients == ["user1", "user2", user.ap_id]
375 end
376
377 test "increases user note count only for public activities" do
378 user = insert(:user)
379
380 {:ok, _} =
381 CommonAPI.post(User.get_cached_by_id(user.id), %{
382 "status" => "1",
383 "visibility" => "public"
384 })
385
386 {:ok, _} =
387 CommonAPI.post(User.get_cached_by_id(user.id), %{
388 "status" => "2",
389 "visibility" => "unlisted"
390 })
391
392 {:ok, _} =
393 CommonAPI.post(User.get_cached_by_id(user.id), %{
394 "status" => "2",
395 "visibility" => "private"
396 })
397
398 {:ok, _} =
399 CommonAPI.post(User.get_cached_by_id(user.id), %{
400 "status" => "3",
401 "visibility" => "direct"
402 })
403
404 user = User.get_cached_by_id(user.id)
405 assert user.info.note_count == 2
406 end
407
408 test "increases replies count" do
409 user = insert(:user)
410 user2 = insert(:user)
411
412 {:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
413 ap_id = activity.data["id"]
414 reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
415
416 # public
417 {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
418 assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
419 assert object.data["repliesCount"] == 1
420
421 # unlisted
422 {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
423 assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
424 assert object.data["repliesCount"] == 2
425
426 # private
427 {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
428 assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
429 assert object.data["repliesCount"] == 2
430
431 # direct
432 {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
433 assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
434 assert object.data["repliesCount"] == 2
435 end
436 end
437
438 describe "fetch activities for recipients" do
439 test "retrieve the activities for certain recipients" do
440 {:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]})
441 {:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]})
442 {:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]})
443
444 activities = ActivityPub.fetch_activities(["someone", "someone_else"])
445 assert length(activities) == 2
446 assert activities == [activity_one, activity_two]
447 end
448 end
449
450 describe "fetch activities in context" do
451 test "retrieves activities that have a given context" do
452 {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
453 {:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
454 {:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
455 {:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"})
456 activity_five = insert(:note_activity)
457 user = insert(:user)
458
459 {:ok, user} = User.block(user, %{ap_id: activity_five.data["actor"]})
460
461 activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user})
462 assert activities == [activity_two, activity]
463 end
464 end
465
466 test "doesn't return blocked activities" do
467 activity_one = insert(:note_activity)
468 activity_two = insert(:note_activity)
469 activity_three = insert(:note_activity)
470 user = insert(:user)
471 booster = insert(:user)
472 {:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]})
473
474 activities =
475 ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
476
477 assert Enum.member?(activities, activity_two)
478 assert Enum.member?(activities, activity_three)
479 refute Enum.member?(activities, activity_one)
480
481 {:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
482
483 activities =
484 ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
485
486 assert Enum.member?(activities, activity_two)
487 assert Enum.member?(activities, activity_three)
488 assert Enum.member?(activities, activity_one)
489
490 {:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]})
491 {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
492 %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
493 activity_three = Activity.get_by_id(activity_three.id)
494
495 activities =
496 ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
497
498 assert Enum.member?(activities, activity_two)
499 refute Enum.member?(activities, activity_three)
500 refute Enum.member?(activities, boost_activity)
501 assert Enum.member?(activities, activity_one)
502
503 activities =
504 ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true})
505
506 assert Enum.member?(activities, activity_two)
507 assert Enum.member?(activities, activity_three)
508 assert Enum.member?(activities, boost_activity)
509 assert Enum.member?(activities, activity_one)
510 end
511
512 test "doesn't return transitive interactions concerning blocked users" do
513 blocker = insert(:user)
514 blockee = insert(:user)
515 friend = insert(:user)
516
517 {:ok, blocker} = User.block(blocker, blockee)
518
519 {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
520
521 {:ok, activity_two} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
522
523 {:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
524
525 {:ok, activity_four} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"})
526
527 activities = ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
528
529 assert Enum.member?(activities, activity_one)
530 refute Enum.member?(activities, activity_two)
531 refute Enum.member?(activities, activity_three)
532 refute Enum.member?(activities, activity_four)
533 end
534
535 test "doesn't return announce activities concerning blocked users" do
536 blocker = insert(:user)
537 blockee = insert(:user)
538 friend = insert(:user)
539
540 {:ok, blocker} = User.block(blocker, blockee)
541
542 {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
543
544 {:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
545
546 {:ok, activity_three, _} = CommonAPI.repeat(activity_two.id, friend)
547
548 activities =
549 ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
550 |> Enum.map(fn act -> act.id end)
551
552 assert Enum.member?(activities, activity_one.id)
553 refute Enum.member?(activities, activity_two.id)
554 refute Enum.member?(activities, activity_three.id)
555 end
556
557 test "doesn't return activities from blocked domains" do
558 domain = "dogwhistle.zone"
559 domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
560 note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
561 activity = insert(:note_activity, %{note: note})
562 user = insert(:user)
563 {:ok, user} = User.block_domain(user, domain)
564
565 activities =
566 ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
567
568 refute activity in activities
569
570 followed_user = insert(:user)
571 ActivityPub.follow(user, followed_user)
572 {:ok, repeat_activity, _} = CommonAPI.repeat(activity.id, followed_user)
573
574 activities =
575 ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
576
577 refute repeat_activity in activities
578 end
579
580 test "doesn't return muted activities" do
581 activity_one = insert(:note_activity)
582 activity_two = insert(:note_activity)
583 activity_three = insert(:note_activity)
584 user = insert(:user)
585 booster = insert(:user)
586 {:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]})
587
588 activities =
589 ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
590
591 assert Enum.member?(activities, activity_two)
592 assert Enum.member?(activities, activity_three)
593 refute Enum.member?(activities, activity_one)
594
595 # Calling with 'with_muted' will deliver muted activities, too.
596 activities =
597 ActivityPub.fetch_activities([], %{
598 "muting_user" => user,
599 "with_muted" => true,
600 "skip_preload" => true
601 })
602
603 assert Enum.member?(activities, activity_two)
604 assert Enum.member?(activities, activity_three)
605 assert Enum.member?(activities, activity_one)
606
607 {:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]})
608
609 activities =
610 ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
611
612 assert Enum.member?(activities, activity_two)
613 assert Enum.member?(activities, activity_three)
614 assert Enum.member?(activities, activity_one)
615
616 {:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]})
617 {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
618 %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
619 activity_three = Activity.get_by_id(activity_three.id)
620
621 activities =
622 ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
623
624 assert Enum.member?(activities, activity_two)
625 refute Enum.member?(activities, activity_three)
626 refute Enum.member?(activities, boost_activity)
627 assert Enum.member?(activities, activity_one)
628
629 activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true})
630
631 assert Enum.member?(activities, activity_two)
632 assert Enum.member?(activities, activity_three)
633 assert Enum.member?(activities, boost_activity)
634 assert Enum.member?(activities, activity_one)
635 end
636
637 test "doesn't return thread muted activities" do
638 user = insert(:user)
639 _activity_one = insert(:note_activity)
640 note_two = insert(:note, data: %{"context" => "suya.."})
641 activity_two = insert(:note_activity, note: note_two)
642
643 {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
644
645 assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
646 end
647
648 test "returns thread muted activities when with_muted is set" do
649 user = insert(:user)
650 _activity_one = insert(:note_activity)
651 note_two = insert(:note, data: %{"context" => "suya.."})
652 activity_two = insert(:note_activity, note: note_two)
653
654 {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
655
656 assert [_activity_two, _activity_one] =
657 ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
658 end
659
660 test "does include announces on request" do
661 activity_three = insert(:note_activity)
662 user = insert(:user)
663 booster = insert(:user)
664
665 {:ok, user} = User.follow(user, booster)
666
667 {:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster)
668
669 [announce_activity] = ActivityPub.fetch_activities([user.ap_id | user.following])
670
671 assert announce_activity.id == announce.id
672 end
673
674 test "excludes reblogs on request" do
675 user = insert(:user)
676 {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
677 {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
678
679 [activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"})
680
681 assert activity == expected_activity
682 end
683
684 describe "public fetch activities" do
685 test "doesn't retrieve unlisted activities" do
686 user = insert(:user)
687
688 {:ok, _unlisted_activity} =
689 CommonAPI.post(user, %{"status" => "yeah", "visibility" => "unlisted"})
690
691 {:ok, listed_activity} = CommonAPI.post(user, %{"status" => "yeah"})
692
693 [activity] = ActivityPub.fetch_public_activities()
694
695 assert activity == listed_activity
696 end
697
698 test "retrieves public activities" do
699 _activities = ActivityPub.fetch_public_activities()
700
701 %{public: public} = ActivityBuilder.public_and_non_public()
702
703 activities = ActivityPub.fetch_public_activities()
704 assert length(activities) == 1
705 assert Enum.at(activities, 0) == public
706 end
707
708 test "retrieves a maximum of 20 activities" do
709 activities = ActivityBuilder.insert_list(30)
710 last_expected = List.last(activities)
711
712 activities = ActivityPub.fetch_public_activities()
713 last = List.last(activities)
714
715 assert length(activities) == 20
716 assert last == last_expected
717 end
718
719 test "retrieves ids starting from a since_id" do
720 activities = ActivityBuilder.insert_list(30)
721 later_activities = ActivityBuilder.insert_list(10)
722 since_id = List.last(activities).id
723 last_expected = List.last(later_activities)
724
725 activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id})
726 last = List.last(activities)
727
728 assert length(activities) == 10
729 assert last == last_expected
730 end
731
732 test "retrieves ids up to max_id" do
733 _first_activities = ActivityBuilder.insert_list(10)
734 activities = ActivityBuilder.insert_list(20)
735 later_activities = ActivityBuilder.insert_list(10)
736 max_id = List.first(later_activities).id
737 last_expected = List.last(activities)
738
739 activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id})
740 last = List.last(activities)
741
742 assert length(activities) == 20
743 assert last == last_expected
744 end
745
746 test "paginates via offset/limit" do
747 _first_activities = ActivityBuilder.insert_list(10)
748 activities = ActivityBuilder.insert_list(10)
749 _later_activities = ActivityBuilder.insert_list(10)
750 first_expected = List.first(activities)
751
752 activities =
753 ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset)
754
755 first = List.first(activities)
756
757 assert length(activities) == 20
758 assert first == first_expected
759 end
760
761 test "doesn't return reblogs for users for whom reblogs have been muted" do
762 activity = insert(:note_activity)
763 user = insert(:user)
764 booster = insert(:user)
765 {:ok, user} = CommonAPI.hide_reblogs(user, booster)
766
767 {:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
768
769 activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
770
771 refute Enum.any?(activities, fn %{id: id} -> id == activity.id end)
772 end
773
774 test "returns reblogs for users for whom reblogs have not been muted" do
775 activity = insert(:note_activity)
776 user = insert(:user)
777 booster = insert(:user)
778 {:ok, user} = CommonAPI.hide_reblogs(user, booster)
779 {:ok, user} = CommonAPI.show_reblogs(user, booster)
780
781 {:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
782
783 activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
784
785 assert Enum.any?(activities, fn %{id: id} -> id == activity.id end)
786 end
787 end
788
789 describe "like an object" do
790 test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
791 Pleroma.Config.put([:instance, :federating], true)
792 note_activity = insert(:note_activity)
793 assert object_activity = Object.normalize(note_activity)
794
795 user = insert(:user)
796
797 {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
798 assert called(Pleroma.Web.Federator.publish(like_activity))
799 end
800
801 test "returns exist activity if object already liked" do
802 note_activity = insert(:note_activity)
803 assert object_activity = Object.normalize(note_activity)
804
805 user = insert(:user)
806
807 {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
808
809 {:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity)
810 assert like_activity == like_activity_exist
811 end
812
813 test "adds a like activity to the db" do
814 note_activity = insert(:note_activity)
815 assert object = Object.normalize(note_activity)
816
817 user = insert(:user)
818 user_two = insert(:user)
819
820 {:ok, like_activity, object} = ActivityPub.like(user, object)
821
822 assert like_activity.data["actor"] == user.ap_id
823 assert like_activity.data["type"] == "Like"
824 assert like_activity.data["object"] == object.data["id"]
825 assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
826 assert like_activity.data["context"] == object.data["context"]
827 assert object.data["like_count"] == 1
828 assert object.data["likes"] == [user.ap_id]
829
830 # Just return the original activity if the user already liked it.
831 {:ok, same_like_activity, object} = ActivityPub.like(user, object)
832
833 assert like_activity == same_like_activity
834 assert object.data["likes"] == [user.ap_id]
835 assert object.data["like_count"] == 1
836
837 {:ok, _like_activity, object} = ActivityPub.like(user_two, object)
838 assert object.data["like_count"] == 2
839 end
840 end
841
842 describe "unliking" do
843 test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
844 Pleroma.Config.put([:instance, :federating], true)
845
846 note_activity = insert(:note_activity)
847 object = Object.normalize(note_activity)
848 user = insert(:user)
849
850 {:ok, object} = ActivityPub.unlike(user, object)
851 refute called(Pleroma.Web.Federator.publish())
852
853 {:ok, _like_activity, object} = ActivityPub.like(user, object)
854 assert object.data["like_count"] == 1
855
856 {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
857 assert object.data["like_count"] == 0
858
859 assert called(Pleroma.Web.Federator.publish(unlike_activity))
860 end
861
862 test "unliking a previously liked object" do
863 note_activity = insert(:note_activity)
864 object = Object.normalize(note_activity)
865 user = insert(:user)
866
867 # Unliking something that hasn't been liked does nothing
868 {:ok, object} = ActivityPub.unlike(user, object)
869 assert object.data["like_count"] == 0
870
871 {:ok, like_activity, object} = ActivityPub.like(user, object)
872 assert object.data["like_count"] == 1
873
874 {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
875 assert object.data["like_count"] == 0
876
877 assert Activity.get_by_id(like_activity.id) == nil
878 assert note_activity.actor in unlike_activity.recipients
879 end
880 end
881
882 describe "announcing an object" do
883 test "adds an announce activity to the db" do
884 note_activity = insert(:note_activity)
885 object = Object.normalize(note_activity)
886 user = insert(:user)
887
888 {:ok, announce_activity, object} = ActivityPub.announce(user, object)
889 assert object.data["announcement_count"] == 1
890 assert object.data["announcements"] == [user.ap_id]
891
892 assert announce_activity.data["to"] == [
893 User.ap_followers(user),
894 note_activity.data["actor"]
895 ]
896
897 assert announce_activity.data["object"] == object.data["id"]
898 assert announce_activity.data["actor"] == user.ap_id
899 assert announce_activity.data["context"] == object.data["context"]
900 end
901 end
902
903 describe "announcing a private object" do
904 test "adds an announce activity to the db if the audience is not widened" do
905 user = insert(:user)
906 {:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
907 object = Object.normalize(note_activity)
908
909 {:ok, announce_activity, object} = ActivityPub.announce(user, object, nil, true, false)
910
911 assert announce_activity.data["to"] == [User.ap_followers(user)]
912
913 assert announce_activity.data["object"] == object.data["id"]
914 assert announce_activity.data["actor"] == user.ap_id
915 assert announce_activity.data["context"] == object.data["context"]
916 end
917
918 test "does not add an announce activity to the db if the audience is widened" do
919 user = insert(:user)
920 {:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
921 object = Object.normalize(note_activity)
922
923 assert {:error, _} = ActivityPub.announce(user, object, nil, true, true)
924 end
925
926 test "does not add an announce activity to the db if the announcer is not the author" do
927 user = insert(:user)
928 announcer = insert(:user)
929 {:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
930 object = Object.normalize(note_activity)
931
932 assert {:error, _} = ActivityPub.announce(announcer, object, nil, true, false)
933 end
934 end
935
936 describe "unannouncing an object" do
937 test "unannouncing a previously announced object" do
938 note_activity = insert(:note_activity)
939 object = Object.normalize(note_activity)
940 user = insert(:user)
941
942 # Unannouncing an object that is not announced does nothing
943 # {:ok, object} = ActivityPub.unannounce(user, object)
944 # assert object.data["announcement_count"] == 0
945
946 {:ok, announce_activity, object} = ActivityPub.announce(user, object)
947 assert object.data["announcement_count"] == 1
948
949 {:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object)
950 assert object.data["announcement_count"] == 0
951
952 assert unannounce_activity.data["to"] == [
953 User.ap_followers(user),
954 object.data["actor"]
955 ]
956
957 assert unannounce_activity.data["type"] == "Undo"
958 assert unannounce_activity.data["object"] == announce_activity.data
959 assert unannounce_activity.data["actor"] == user.ap_id
960 assert unannounce_activity.data["context"] == announce_activity.data["context"]
961
962 assert Activity.get_by_id(announce_activity.id) == nil
963 end
964 end
965
966 describe "uploading files" do
967 test "copies the file to the configured folder" do
968 file = %Plug.Upload{
969 content_type: "image/jpg",
970 path: Path.absname("test/fixtures/image.jpg"),
971 filename: "an_image.jpg"
972 }
973
974 {:ok, %Object{} = object} = ActivityPub.upload(file)
975 assert object.data["name"] == "an_image.jpg"
976 end
977
978 test "works with base64 encoded images" do
979 file = %{
980 "img" => data_uri()
981 }
982
983 {:ok, %Object{}} = ActivityPub.upload(file)
984 end
985 end
986
987 describe "fetch the latest Follow" do
988 test "fetches the latest Follow activity" do
989 %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
990 follower = Repo.get_by(User, ap_id: activity.data["actor"])
991 followed = Repo.get_by(User, ap_id: activity.data["object"])
992
993 assert activity == Utils.fetch_latest_follow(follower, followed)
994 end
995 end
996
997 describe "following / unfollowing" do
998 test "creates a follow activity" do
999 follower = insert(:user)
1000 followed = insert(:user)
1001
1002 {:ok, activity} = ActivityPub.follow(follower, followed)
1003 assert activity.data["type"] == "Follow"
1004 assert activity.data["actor"] == follower.ap_id
1005 assert activity.data["object"] == followed.ap_id
1006 end
1007
1008 test "creates an undo activity for the last follow" do
1009 follower = insert(:user)
1010 followed = insert(:user)
1011
1012 {:ok, follow_activity} = ActivityPub.follow(follower, followed)
1013 {:ok, activity} = ActivityPub.unfollow(follower, followed)
1014
1015 assert activity.data["type"] == "Undo"
1016 assert activity.data["actor"] == follower.ap_id
1017
1018 embedded_object = activity.data["object"]
1019 assert is_map(embedded_object)
1020 assert embedded_object["type"] == "Follow"
1021 assert embedded_object["object"] == followed.ap_id
1022 assert embedded_object["id"] == follow_activity.data["id"]
1023 end
1024 end
1025
1026 describe "blocking / unblocking" do
1027 test "creates a block activity" do
1028 blocker = insert(:user)
1029 blocked = insert(:user)
1030
1031 {:ok, activity} = ActivityPub.block(blocker, blocked)
1032
1033 assert activity.data["type"] == "Block"
1034 assert activity.data["actor"] == blocker.ap_id
1035 assert activity.data["object"] == blocked.ap_id
1036 end
1037
1038 test "creates an undo activity for the last block" do
1039 blocker = insert(:user)
1040 blocked = insert(:user)
1041
1042 {:ok, block_activity} = ActivityPub.block(blocker, blocked)
1043 {:ok, activity} = ActivityPub.unblock(blocker, blocked)
1044
1045 assert activity.data["type"] == "Undo"
1046 assert activity.data["actor"] == blocker.ap_id
1047
1048 embedded_object = activity.data["object"]
1049 assert is_map(embedded_object)
1050 assert embedded_object["type"] == "Block"
1051 assert embedded_object["object"] == blocked.ap_id
1052 assert embedded_object["id"] == block_activity.data["id"]
1053 end
1054 end
1055
1056 describe "deletion" do
1057 test "it creates a delete activity and deletes the original object" do
1058 note = insert(:note_activity)
1059 object = Object.normalize(note)
1060 {:ok, delete} = ActivityPub.delete(object)
1061
1062 assert delete.data["type"] == "Delete"
1063 assert delete.data["actor"] == note.data["actor"]
1064 assert delete.data["object"] == object.data["id"]
1065
1066 assert Activity.get_by_id(delete.id) != nil
1067
1068 assert Repo.get(Object, object.id).data["type"] == "Tombstone"
1069 end
1070
1071 test "decrements user note count only for public activities" do
1072 user = insert(:user, info: %{note_count: 10})
1073
1074 {:ok, a1} =
1075 CommonAPI.post(User.get_cached_by_id(user.id), %{
1076 "status" => "yeah",
1077 "visibility" => "public"
1078 })
1079
1080 {:ok, a2} =
1081 CommonAPI.post(User.get_cached_by_id(user.id), %{
1082 "status" => "yeah",
1083 "visibility" => "unlisted"
1084 })
1085
1086 {:ok, a3} =
1087 CommonAPI.post(User.get_cached_by_id(user.id), %{
1088 "status" => "yeah",
1089 "visibility" => "private"
1090 })
1091
1092 {:ok, a4} =
1093 CommonAPI.post(User.get_cached_by_id(user.id), %{
1094 "status" => "yeah",
1095 "visibility" => "direct"
1096 })
1097
1098 {:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
1099 {:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
1100 {:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
1101 {:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
1102
1103 user = User.get_cached_by_id(user.id)
1104 assert user.info.note_count == 10
1105 end
1106
1107 test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
1108 user = insert(:user)
1109 note = insert(:note_activity)
1110 object = Object.normalize(note)
1111
1112 {:ok, object} =
1113 object
1114 |> Object.change(%{
1115 data: %{
1116 "actor" => object.data["actor"],
1117 "id" => object.data["id"],
1118 "to" => [user.ap_id],
1119 "type" => "Note"
1120 }
1121 })
1122 |> Object.update_and_set_cache()
1123
1124 {:ok, delete} = ActivityPub.delete(object)
1125
1126 assert user.ap_id in delete.data["to"]
1127 end
1128
1129 test "decreases reply count" do
1130 user = insert(:user)
1131 user2 = insert(:user)
1132
1133 {:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
1134 reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
1135 ap_id = activity.data["id"]
1136
1137 {:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
1138 {:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
1139 {:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
1140 {:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
1141
1142 _ = CommonAPI.delete(direct_reply.id, user2)
1143 assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
1144 assert object.data["repliesCount"] == 2
1145
1146 _ = CommonAPI.delete(private_reply.id, user2)
1147 assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
1148 assert object.data["repliesCount"] == 2
1149
1150 _ = CommonAPI.delete(public_reply.id, user2)
1151 assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
1152 assert object.data["repliesCount"] == 1
1153
1154 _ = CommonAPI.delete(unlisted_reply.id, user2)
1155 assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
1156 assert object.data["repliesCount"] == 0
1157 end
1158 end
1159
1160 describe "timeline post-processing" do
1161 test "it filters broken threads" do
1162 user1 = insert(:user)
1163 user2 = insert(:user)
1164 user3 = insert(:user)
1165
1166 {:ok, user1} = User.follow(user1, user3)
1167 assert User.following?(user1, user3)
1168
1169 {:ok, user2} = User.follow(user2, user3)
1170 assert User.following?(user2, user3)
1171
1172 {:ok, user3} = User.follow(user3, user2)
1173 assert User.following?(user3, user2)
1174
1175 {:ok, public_activity} = CommonAPI.post(user3, %{"status" => "hi 1"})
1176
1177 {:ok, private_activity_1} =
1178 CommonAPI.post(user3, %{"status" => "hi 2", "visibility" => "private"})
1179
1180 {:ok, private_activity_2} =
1181 CommonAPI.post(user2, %{
1182 "status" => "hi 3",
1183 "visibility" => "private",
1184 "in_reply_to_status_id" => private_activity_1.id
1185 })
1186
1187 {:ok, private_activity_3} =
1188 CommonAPI.post(user3, %{
1189 "status" => "hi 4",
1190 "visibility" => "private",
1191 "in_reply_to_status_id" => private_activity_2.id
1192 })
1193
1194 activities =
1195 ActivityPub.fetch_activities([user1.ap_id | user1.following])
1196 |> Enum.map(fn a -> a.id end)
1197
1198 private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
1199
1200 assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities
1201
1202 assert length(activities) == 3
1203
1204 activities =
1205 ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"user" => user1})
1206 |> Enum.map(fn a -> a.id end)
1207
1208 assert [public_activity.id, private_activity_1.id] == activities
1209 assert length(activities) == 2
1210 end
1211 end
1212
1213 describe "update" do
1214 test "it creates an update activity with the new user data" do
1215 user = insert(:user)
1216 {:ok, user} = User.ensure_keys_present(user)
1217 user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
1218
1219 {:ok, update} =
1220 ActivityPub.update(%{
1221 actor: user_data["id"],
1222 to: [user.follower_address],
1223 cc: [],
1224 object: user_data
1225 })
1226
1227 assert update.data["actor"] == user.ap_id
1228 assert update.data["to"] == [user.follower_address]
1229 assert embedded_object = update.data["object"]
1230 assert embedded_object["id"] == user_data["id"]
1231 assert embedded_object["type"] == user_data["type"]
1232 end
1233 end
1234
1235 test "returned pinned statuses" do
1236 Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
1237 user = insert(:user)
1238
1239 {:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
1240 {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
1241 {:ok, activity_three} = CommonAPI.post(user, %{"status" => "HI!!!"})
1242
1243 CommonAPI.pin(activity_one.id, user)
1244 user = refresh_record(user)
1245
1246 CommonAPI.pin(activity_two.id, user)
1247 user = refresh_record(user)
1248
1249 CommonAPI.pin(activity_three.id, user)
1250 user = refresh_record(user)
1251
1252 activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"})
1253
1254 assert 3 = length(activities)
1255 end
1256
1257 test "it can create a Flag activity" do
1258 reporter = insert(:user)
1259 target_account = insert(:user)
1260 {:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"})
1261 context = Utils.generate_context_id()
1262 content = "foobar"
1263
1264 reporter_ap_id = reporter.ap_id
1265 target_ap_id = target_account.ap_id
1266 activity_ap_id = activity.data["id"]
1267
1268 assert {:ok, activity} =
1269 ActivityPub.flag(%{
1270 actor: reporter,
1271 context: context,
1272 account: target_account,
1273 statuses: [activity],
1274 content: content
1275 })
1276
1277 assert %Activity{
1278 actor: ^reporter_ap_id,
1279 data: %{
1280 "type" => "Flag",
1281 "content" => ^content,
1282 "context" => ^context,
1283 "object" => [^target_ap_id, ^activity_ap_id]
1284 }
1285 } = activity
1286 end
1287
1288 test "fetch_activities/2 returns activities addressed to a list " do
1289 user = insert(:user)
1290 member = insert(:user)
1291 {:ok, list} = Pleroma.List.create("foo", user)
1292 {:ok, list} = Pleroma.List.follow(list, member)
1293
1294 {:ok, activity} =
1295 CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
1296
1297 activity = Repo.preload(activity, :bookmark)
1298 activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
1299
1300 assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity]
1301 end
1302
1303 def data_uri do
1304 File.read!("test/fixtures/avatar_data_uri")
1305 end
1306
1307 describe "fetch_activities_bounded" do
1308 test "fetches private posts for followed users" do
1309 user = insert(:user)
1310
1311 {:ok, activity} =
1312 CommonAPI.post(user, %{
1313 "status" => "thought I looked cute might delete later :3",
1314 "visibility" => "private"
1315 })
1316
1317 [result] = ActivityPub.fetch_activities_bounded([user.follower_address], [])
1318 assert result.id == activity.id
1319 end
1320
1321 test "fetches only public posts for other users" do
1322 user = insert(:user)
1323 {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe", "visibility" => "public"})
1324
1325 {:ok, _private_activity} =
1326 CommonAPI.post(user, %{
1327 "status" => "why is tenshi eating a corndog so cute?",
1328 "visibility" => "private"
1329 })
1330
1331 [result] = ActivityPub.fetch_activities_bounded([], [user.follower_address])
1332 assert result.id == activity.id
1333 end
1334 end
1335
1336 describe "fetch_follow_information_for_user" do
1337 test "syncronizes following/followers counters" do
1338 user =
1339 insert(:user,
1340 local: false,
1341 follower_address: "http://localhost:4001/users/fuser2/followers",
1342 following_address: "http://localhost:4001/users/fuser2/following"
1343 )
1344
1345 {:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
1346 assert info.follower_count == 527
1347 assert info.following_count == 267
1348 end
1349
1350 test "detects hidden followers" do
1351 mock(fn env ->
1352 case env.url do
1353 "http://localhost:4001/users/masto_closed/followers?page=1" ->
1354 %Tesla.Env{status: 403, body: ""}
1355
1356 _ ->
1357 apply(HttpRequestMock, :request, [env])
1358 end
1359 end)
1360
1361 user =
1362 insert(:user,
1363 local: false,
1364 follower_address: "http://localhost:4001/users/masto_closed/followers",
1365 following_address: "http://localhost:4001/users/masto_closed/following"
1366 )
1367
1368 {:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
1369 assert info.hide_followers == true
1370 assert info.hide_follows == false
1371 end
1372
1373 test "detects hidden follows" do
1374 mock(fn env ->
1375 case env.url do
1376 "http://localhost:4001/users/masto_closed/following?page=1" ->
1377 %Tesla.Env{status: 403, body: ""}
1378
1379 _ ->
1380 apply(HttpRequestMock, :request, [env])
1381 end
1382 end)
1383
1384 user =
1385 insert(:user,
1386 local: false,
1387 follower_address: "http://localhost:4001/users/masto_closed/followers",
1388 following_address: "http://localhost:4001/users/masto_closed/following"
1389 )
1390
1391 {:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
1392 assert info.hide_followers == false
1393 assert info.hide_follows == true
1394 end
1395 end
1396 end