logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://anongit.hacktivis.me/git/pleroma.git/

activity_pub_test.exs (88010B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
  5. use Pleroma.DataCase
  6. use Oban.Testing, repo: Pleroma.Repo
  7. alias Pleroma.Activity
  8. alias Pleroma.Builders.ActivityBuilder
  9. alias Pleroma.Config
  10. alias Pleroma.Notification
  11. alias Pleroma.Object
  12. alias Pleroma.UnstubbedConfigMock, as: ConfigMock
  13. alias Pleroma.User
  14. alias Pleroma.Web.ActivityPub.ActivityPub
  15. alias Pleroma.Web.ActivityPub.Utils
  16. alias Pleroma.Web.AdminAPI.AccountView
  17. alias Pleroma.Web.CommonAPI
  18. import ExUnit.CaptureLog
  19. import Mock
  20. import Mox
  21. import Pleroma.Factory
  22. import Tesla.Mock
  23. setup do
  24. mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
  25. ConfigMock
  26. |> stub_with(Pleroma.Test.StaticConfig)
  27. :ok
  28. end
  29. setup do: clear_config([:instance, :federating])
  30. describe "streaming out participations" do
  31. test "it streams them out" do
  32. user = insert(:user)
  33. {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
  34. {:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity)
  35. participations =
  36. conversation.participations
  37. |> Repo.preload(:user)
  38. with_mock Pleroma.Web.Streamer,
  39. stream: fn _, _ -> nil end do
  40. ActivityPub.stream_out_participations(conversation.participations)
  41. assert called(Pleroma.Web.Streamer.stream("participation", participations))
  42. end
  43. end
  44. test "streams them out on activity creation" do
  45. user_one = insert(:user)
  46. user_two = insert(:user)
  47. with_mock Pleroma.Web.Streamer,
  48. stream: fn _, _ -> nil end do
  49. {:ok, activity} =
  50. CommonAPI.post(user_one, %{
  51. status: "@#{user_two.nickname}",
  52. visibility: "direct"
  53. })
  54. conversation =
  55. activity.data["context"]
  56. |> Pleroma.Conversation.get_for_ap_id()
  57. |> Repo.preload(participations: :user)
  58. assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations))
  59. end
  60. end
  61. end
  62. describe "fetching restricted by visibility" do
  63. test "it restricts by the appropriate visibility" do
  64. user = insert(:user)
  65. {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"})
  66. {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
  67. {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
  68. {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"})
  69. activities = ActivityPub.fetch_activities([], %{visibility: "direct", actor_id: user.ap_id})
  70. assert activities == [direct_activity]
  71. activities =
  72. ActivityPub.fetch_activities([], %{visibility: "unlisted", actor_id: user.ap_id})
  73. assert activities == [unlisted_activity]
  74. activities =
  75. ActivityPub.fetch_activities([], %{visibility: "private", actor_id: user.ap_id})
  76. assert activities == [private_activity]
  77. activities = ActivityPub.fetch_activities([], %{visibility: "public", actor_id: user.ap_id})
  78. assert activities == [public_activity]
  79. activities =
  80. ActivityPub.fetch_activities([], %{
  81. visibility: ~w[private public],
  82. actor_id: user.ap_id
  83. })
  84. assert activities == [public_activity, private_activity]
  85. end
  86. end
  87. describe "fetching excluded by visibility" do
  88. test "it excludes by the appropriate visibility" do
  89. user = insert(:user)
  90. {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"})
  91. {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
  92. {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
  93. {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"})
  94. activities =
  95. ActivityPub.fetch_activities([], %{
  96. exclude_visibilities: "direct",
  97. actor_id: user.ap_id
  98. })
  99. assert public_activity in activities
  100. assert unlisted_activity in activities
  101. assert private_activity in activities
  102. refute direct_activity in activities
  103. activities =
  104. ActivityPub.fetch_activities([], %{
  105. exclude_visibilities: "unlisted",
  106. actor_id: user.ap_id
  107. })
  108. assert public_activity in activities
  109. refute unlisted_activity in activities
  110. assert private_activity in activities
  111. assert direct_activity in activities
  112. activities =
  113. ActivityPub.fetch_activities([], %{
  114. exclude_visibilities: "private",
  115. actor_id: user.ap_id
  116. })
  117. assert public_activity in activities
  118. assert unlisted_activity in activities
  119. refute private_activity in activities
  120. assert direct_activity in activities
  121. activities =
  122. ActivityPub.fetch_activities([], %{
  123. exclude_visibilities: "public",
  124. actor_id: user.ap_id
  125. })
  126. refute public_activity in activities
  127. assert unlisted_activity in activities
  128. assert private_activity in activities
  129. assert direct_activity in activities
  130. end
  131. end
  132. describe "building a user from his ap id" do
  133. test "it returns a user" do
  134. user_id = "http://mastodon.example.org/users/admin"
  135. {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
  136. assert user.ap_id == user_id
  137. assert user.nickname == "admin@mastodon.example.org"
  138. assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
  139. end
  140. test "it returns a user that is invisible" do
  141. user_id = "http://mastodon.example.org/users/relay"
  142. {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
  143. assert User.invisible?(user)
  144. end
  145. test "it returns a user that accepts chat messages" do
  146. user_id = "http://mastodon.example.org/users/admin"
  147. {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
  148. assert user.accepts_chat_messages
  149. end
  150. test "works for guppe actors" do
  151. user_id = "https://gup.pe/u/bernie2020"
  152. Tesla.Mock.mock(fn
  153. %{method: :get, url: ^user_id} ->
  154. %Tesla.Env{
  155. status: 200,
  156. body: File.read!("test/fixtures/guppe-actor.json"),
  157. headers: [{"content-type", "application/activity+json"}]
  158. }
  159. end)
  160. {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
  161. assert user.name == "Bernie2020 group"
  162. assert user.actor_type == "Group"
  163. end
  164. test "works for bridgy actors" do
  165. user_id = "https://fed.brid.gy/jk.nipponalba.scot"
  166. Tesla.Mock.mock(fn
  167. %{method: :get, url: ^user_id} ->
  168. %Tesla.Env{
  169. status: 200,
  170. body: File.read!("test/fixtures/bridgy/actor.json"),
  171. headers: [{"content-type", "application/activity+json"}]
  172. }
  173. end)
  174. {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
  175. assert user.actor_type == "Person"
  176. assert user.avatar == %{
  177. "type" => "Image",
  178. "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}],
  179. "name" => "profile picture"
  180. }
  181. assert user.banner == %{
  182. "type" => "Image",
  183. "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}],
  184. "name" => "profile picture"
  185. }
  186. end
  187. test "fetches user featured collection" do
  188. ap_id = "https://example.com/users/lain"
  189. featured_url = "https://example.com/users/lain/collections/featured"
  190. user_data =
  191. "test/fixtures/users_mock/user.json"
  192. |> File.read!()
  193. |> String.replace("{{nickname}}", "lain")
  194. |> Jason.decode!()
  195. |> Map.put("featured", featured_url)
  196. |> Jason.encode!()
  197. object_id = Ecto.UUID.generate()
  198. featured_data =
  199. "test/fixtures/mastodon/collections/featured.json"
  200. |> File.read!()
  201. |> String.replace("{{domain}}", "example.com")
  202. |> String.replace("{{nickname}}", "lain")
  203. |> String.replace("{{object_id}}", object_id)
  204. object_url = "https://example.com/objects/#{object_id}"
  205. object_data =
  206. "test/fixtures/statuses/note.json"
  207. |> File.read!()
  208. |> String.replace("{{object_id}}", object_id)
  209. |> String.replace("{{nickname}}", "lain")
  210. Tesla.Mock.mock(fn
  211. %{
  212. method: :get,
  213. url: ^ap_id
  214. } ->
  215. %Tesla.Env{
  216. status: 200,
  217. body: user_data,
  218. headers: [{"content-type", "application/activity+json"}]
  219. }
  220. %{
  221. method: :get,
  222. url: ^featured_url
  223. } ->
  224. %Tesla.Env{
  225. status: 200,
  226. body: featured_data,
  227. headers: [{"content-type", "application/activity+json"}]
  228. }
  229. %{
  230. method: :get,
  231. url: ^object_url
  232. } ->
  233. %Tesla.Env{
  234. status: 200,
  235. body: object_data,
  236. headers: [{"content-type", "application/activity+json"}]
  237. }
  238. end)
  239. {:ok, user} = ActivityPub.make_user_from_ap_id(ap_id)
  240. assert_enqueued(
  241. worker: Pleroma.Workers.RemoteFetcherWorker,
  242. args: %{
  243. "op" => "fetch_remote",
  244. "id" => object_url,
  245. "depth" => 1
  246. }
  247. )
  248. # wait for oban
  249. Pleroma.Tests.ObanHelpers.perform_all()
  250. assert user.featured_address == featured_url
  251. assert Map.has_key?(user.pinned_objects, object_url)
  252. in_db = Pleroma.User.get_by_ap_id(ap_id)
  253. assert in_db.featured_address == featured_url
  254. assert Map.has_key?(user.pinned_objects, object_url)
  255. assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url)
  256. end
  257. test "fetches user featured collection without embedded object" do
  258. ap_id = "https://example.com/users/lain"
  259. featured_url = "https://example.com/users/lain/collections/featured"
  260. user_data =
  261. "test/fixtures/users_mock/user.json"
  262. |> File.read!()
  263. |> String.replace("{{nickname}}", "lain")
  264. |> Jason.decode!()
  265. |> Map.put("featured", featured_url)
  266. |> Jason.encode!()
  267. object_id = Ecto.UUID.generate()
  268. featured_data =
  269. "test/fixtures/mastodon/collections/external_featured.json"
  270. |> File.read!()
  271. |> String.replace("{{domain}}", "example.com")
  272. |> String.replace("{{nickname}}", "lain")
  273. |> String.replace("{{object_id}}", object_id)
  274. object_url = "https://example.com/objects/#{object_id}"
  275. object_data =
  276. "test/fixtures/statuses/note.json"
  277. |> File.read!()
  278. |> String.replace("{{object_id}}", object_id)
  279. |> String.replace("{{nickname}}", "lain")
  280. Tesla.Mock.mock(fn
  281. %{
  282. method: :get,
  283. url: ^ap_id
  284. } ->
  285. %Tesla.Env{
  286. status: 200,
  287. body: user_data,
  288. headers: [{"content-type", "application/activity+json"}]
  289. }
  290. %{
  291. method: :get,
  292. url: ^featured_url
  293. } ->
  294. %Tesla.Env{
  295. status: 200,
  296. body: featured_data,
  297. headers: [{"content-type", "application/activity+json"}]
  298. }
  299. %{
  300. method: :get,
  301. url: ^object_url
  302. } ->
  303. %Tesla.Env{
  304. status: 200,
  305. body: object_data,
  306. headers: [{"content-type", "application/activity+json"}]
  307. }
  308. end)
  309. {:ok, user} = ActivityPub.make_user_from_ap_id(ap_id)
  310. assert_enqueued(
  311. worker: Pleroma.Workers.RemoteFetcherWorker,
  312. args: %{
  313. "op" => "fetch_remote",
  314. "id" => object_url,
  315. "depth" => 1
  316. }
  317. )
  318. # wait for oban
  319. Pleroma.Tests.ObanHelpers.perform_all()
  320. assert user.featured_address == featured_url
  321. assert Map.has_key?(user.pinned_objects, object_url)
  322. in_db = Pleroma.User.get_by_ap_id(ap_id)
  323. assert in_db.featured_address == featured_url
  324. assert Map.has_key?(user.pinned_objects, object_url)
  325. assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url)
  326. end
  327. test "fetches user birthday information from misskey" do
  328. user_id = "https://misskey.io/@mkljczk"
  329. Tesla.Mock.mock(fn
  330. %{
  331. method: :get,
  332. url: ^user_id
  333. } ->
  334. %Tesla.Env{
  335. status: 200,
  336. body: File.read!("test/fixtures/birthdays/misskey-user.json"),
  337. headers: [{"content-type", "application/activity+json"}]
  338. }
  339. end)
  340. {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
  341. assert user.birthday == ~D[2001-02-12]
  342. end
  343. test "fetches avatar description" do
  344. user_id = "https://example.com/users/marcin"
  345. user_data =
  346. "test/fixtures/users_mock/user.json"
  347. |> File.read!()
  348. |> String.replace("{{nickname}}", "marcin")
  349. |> Jason.decode!()
  350. |> Map.delete("featured")
  351. |> Map.update("icon", %{}, fn image -> Map.put(image, "name", "image description") end)
  352. |> Jason.encode!()
  353. Tesla.Mock.mock(fn
  354. %{
  355. method: :get,
  356. url: ^user_id
  357. } ->
  358. %Tesla.Env{
  359. status: 200,
  360. body: user_data,
  361. headers: [{"content-type", "application/activity+json"}]
  362. }
  363. end)
  364. {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
  365. assert user.avatar["name"] == "image description"
  366. end
  367. end
  368. test "it fetches the appropriate tag-restricted posts" do
  369. user = insert(:user)
  370. {:ok, status_one} = CommonAPI.post(user, %{status: ". #TEST"})
  371. {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"})
  372. {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #Reject"})
  373. {:ok, status_four} = CommonAPI.post(user, %{status: ". #Any1 #any2"})
  374. {:ok, status_five} = CommonAPI.post(user, %{status: ". #Any2 #any1"})
  375. for hashtag_timeline_strategy <- [:enabled, :disabled] do
  376. clear_config([:features, :improved_hashtag_timeline], hashtag_timeline_strategy)
  377. fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"})
  378. fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["TEST", "essais"]})
  379. fetch_three =
  380. ActivityPub.fetch_activities([], %{
  381. type: "Create",
  382. tag: ["test", "Essais"],
  383. tag_reject: ["reject"]
  384. })
  385. fetch_four =
  386. ActivityPub.fetch_activities([], %{
  387. type: "Create",
  388. tag: ["test"],
  389. tag_all: ["test", "REJECT"]
  390. })
  391. # Testing that deduplication (if needed) is done on DB (not Ecto) level; :limit is important
  392. fetch_five =
  393. ActivityPub.fetch_activities([], %{
  394. type: "Create",
  395. tag: ["ANY1", "any2"],
  396. limit: 2
  397. })
  398. fetch_six =
  399. ActivityPub.fetch_activities([], %{
  400. type: "Create",
  401. tag: ["any1", "Any2"],
  402. tag_all: [],
  403. tag_reject: []
  404. })
  405. # Regression test: passing empty lists as filter options shouldn't affect the results
  406. assert fetch_five == fetch_six
  407. [fetch_one, fetch_two, fetch_three, fetch_four, fetch_five] =
  408. Enum.map([fetch_one, fetch_two, fetch_three, fetch_four, fetch_five], fn statuses ->
  409. Enum.map(statuses, fn s -> Repo.preload(s, object: :hashtags) end)
  410. end)
  411. assert fetch_one == [status_one, status_three]
  412. assert fetch_two == [status_one, status_two, status_three]
  413. assert fetch_three == [status_one, status_two]
  414. assert fetch_four == [status_three]
  415. assert fetch_five == [status_four, status_five]
  416. end
  417. end
  418. describe "insertion" do
  419. test "drops activities beyond a certain limit" do
  420. limit = Config.get([:instance, :remote_limit])
  421. random_text =
  422. :crypto.strong_rand_bytes(limit + 1)
  423. |> Base.encode64()
  424. |> binary_part(0, limit + 1)
  425. data = %{
  426. "ok" => true,
  427. "object" => %{
  428. "content" => random_text
  429. }
  430. }
  431. assert {:error, :remote_limit} = ActivityPub.insert(data)
  432. end
  433. test "doesn't drop activities with content being null" do
  434. user = insert(:user)
  435. data = %{
  436. "actor" => user.ap_id,
  437. "to" => [],
  438. "object" => %{
  439. "actor" => user.ap_id,
  440. "to" => [],
  441. "type" => "Note",
  442. "content" => nil
  443. }
  444. }
  445. assert {:ok, _} = ActivityPub.insert(data)
  446. end
  447. test "returns the activity if one with the same id is already in" do
  448. activity = insert(:note_activity)
  449. {:ok, new_activity} = ActivityPub.insert(activity.data)
  450. assert activity.id == new_activity.id
  451. end
  452. test "inserts a given map into the activity database, giving it an id if it has none." do
  453. user = insert(:user)
  454. data = %{
  455. "actor" => user.ap_id,
  456. "to" => [],
  457. "object" => %{
  458. "actor" => user.ap_id,
  459. "to" => [],
  460. "type" => "Note",
  461. "content" => "hey"
  462. }
  463. }
  464. {:ok, %Activity{} = activity} = ActivityPub.insert(data)
  465. assert activity.data["ok"] == data["ok"]
  466. assert is_binary(activity.data["id"])
  467. given_id = "bla"
  468. data = %{
  469. "id" => given_id,
  470. "actor" => user.ap_id,
  471. "to" => [],
  472. "context" => "blabla",
  473. "object" => %{
  474. "actor" => user.ap_id,
  475. "to" => [],
  476. "type" => "Note",
  477. "content" => "hey"
  478. }
  479. }
  480. {:ok, %Activity{} = activity} = ActivityPub.insert(data)
  481. assert activity.data["ok"] == data["ok"]
  482. assert activity.data["id"] == given_id
  483. assert activity.data["context"] == "blabla"
  484. end
  485. test "adds a context when none is there" do
  486. user = insert(:user)
  487. data = %{
  488. "actor" => user.ap_id,
  489. "to" => [],
  490. "object" => %{
  491. "actor" => user.ap_id,
  492. "to" => [],
  493. "type" => "Note",
  494. "content" => "hey"
  495. }
  496. }
  497. {:ok, %Activity{} = activity} = ActivityPub.insert(data)
  498. object = Pleroma.Object.normalize(activity, fetch: false)
  499. assert is_binary(activity.data["context"])
  500. assert is_binary(object.data["context"])
  501. end
  502. test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do
  503. user = insert(:user)
  504. data = %{
  505. "actor" => user.ap_id,
  506. "to" => [],
  507. "object" => %{
  508. "actor" => user.ap_id,
  509. "to" => [],
  510. "type" => "Note",
  511. "content" => "hey"
  512. }
  513. }
  514. {:ok, %Activity{} = activity} = ActivityPub.insert(data)
  515. assert object = Object.normalize(activity, fetch: false)
  516. assert is_binary(object.data["id"])
  517. end
  518. end
  519. describe "listen activities" do
  520. test "does not increase user note count" do
  521. user = insert(:user)
  522. {:ok, activity} =
  523. ActivityPub.listen(%{
  524. to: ["https://www.w3.org/ns/activitystreams#Public"],
  525. actor: user,
  526. context: "",
  527. object: %{
  528. "actor" => user.ap_id,
  529. "to" => ["https://www.w3.org/ns/activitystreams#Public"],
  530. "artist" => "lain",
  531. "title" => "lain radio episode 1",
  532. "length" => 180_000,
  533. "type" => "Audio"
  534. }
  535. })
  536. assert activity.actor == user.ap_id
  537. user = User.get_cached_by_id(user.id)
  538. assert user.note_count == 0
  539. end
  540. test "can be fetched into a timeline" do
  541. _listen_activity_1 = insert(:listen)
  542. _listen_activity_2 = insert(:listen)
  543. _listen_activity_3 = insert(:listen)
  544. timeline = ActivityPub.fetch_activities([], %{type: ["Listen"]})
  545. assert length(timeline) == 3
  546. end
  547. end
  548. describe "create activities" do
  549. setup do
  550. [user: insert(:user)]
  551. end
  552. test "it reverts create", %{user: user} do
  553. with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
  554. assert {:error, :reverted} =
  555. ActivityPub.create(%{
  556. to: ["user1", "user2"],
  557. actor: user,
  558. context: "",
  559. object: %{
  560. "to" => ["user1", "user2"],
  561. "type" => "Note",
  562. "content" => "testing"
  563. }
  564. })
  565. end
  566. assert Repo.aggregate(Activity, :count, :id) == 0
  567. assert Repo.aggregate(Object, :count, :id) == 0
  568. end
  569. test "creates activity if expiration is not configured and expires_at is not passed", %{
  570. user: user
  571. } do
  572. clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false)
  573. assert {:ok, _} =
  574. ActivityPub.create(%{
  575. to: ["user1", "user2"],
  576. actor: user,
  577. context: "",
  578. object: %{
  579. "to" => ["user1", "user2"],
  580. "type" => "Note",
  581. "content" => "testing"
  582. }
  583. })
  584. end
  585. test "rejects activity if expires_at present but expiration is not configured", %{user: user} do
  586. clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false)
  587. assert {:error, :expired_activities_disabled} =
  588. ActivityPub.create(%{
  589. to: ["user1", "user2"],
  590. actor: user,
  591. context: "",
  592. object: %{
  593. "to" => ["user1", "user2"],
  594. "type" => "Note",
  595. "content" => "testing"
  596. },
  597. additional: %{
  598. "expires_at" => DateTime.utc_now()
  599. }
  600. })
  601. assert Repo.aggregate(Activity, :count, :id) == 0
  602. assert Repo.aggregate(Object, :count, :id) == 0
  603. end
  604. test "removes doubled 'to' recipients", %{user: user} do
  605. {:ok, activity} =
  606. ActivityPub.create(%{
  607. to: ["user1", "user1", "user2"],
  608. actor: user,
  609. context: "",
  610. object: %{
  611. "to" => ["user1", "user1", "user2"],
  612. "type" => "Note",
  613. "content" => "testing"
  614. }
  615. })
  616. assert activity.data["to"] == ["user1", "user2"]
  617. assert activity.actor == user.ap_id
  618. assert activity.recipients == ["user1", "user2", user.ap_id]
  619. end
  620. test "increases user note count only for public activities", %{user: user} do
  621. {:ok, _} =
  622. CommonAPI.post(User.get_cached_by_id(user.id), %{
  623. status: "1",
  624. visibility: "public"
  625. })
  626. {:ok, _} =
  627. CommonAPI.post(User.get_cached_by_id(user.id), %{
  628. status: "2",
  629. visibility: "unlisted"
  630. })
  631. {:ok, _} =
  632. CommonAPI.post(User.get_cached_by_id(user.id), %{
  633. status: "2",
  634. visibility: "private"
  635. })
  636. {:ok, _} =
  637. CommonAPI.post(User.get_cached_by_id(user.id), %{
  638. status: "3",
  639. visibility: "direct"
  640. })
  641. user = User.get_cached_by_id(user.id)
  642. assert user.note_count == 2
  643. end
  644. test "increases replies count", %{user: user} do
  645. user2 = insert(:user)
  646. {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"})
  647. ap_id = activity.data["id"]
  648. reply_data = %{status: "1", in_reply_to_status_id: activity.id}
  649. # public
  650. {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "public"))
  651. assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
  652. assert object.data["repliesCount"] == 1
  653. # unlisted
  654. {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "unlisted"))
  655. assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
  656. assert object.data["repliesCount"] == 2
  657. # private
  658. {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "private"))
  659. assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
  660. assert object.data["repliesCount"] == 2
  661. # direct
  662. {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "direct"))
  663. assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
  664. assert object.data["repliesCount"] == 2
  665. end
  666. test "increates quotes count", %{user: user} do
  667. user2 = insert(:user)
  668. {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"})
  669. ap_id = activity.data["id"]
  670. quote_data = %{status: "1", quote_id: activity.id}
  671. # public
  672. {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "public"))
  673. assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
  674. assert object.data["quotesCount"] == 1
  675. # unlisted
  676. {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "unlisted"))
  677. assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
  678. assert object.data["quotesCount"] == 2
  679. # private
  680. {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "private"))
  681. assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
  682. assert object.data["quotesCount"] == 2
  683. # direct
  684. {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "direct"))
  685. assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
  686. assert object.data["quotesCount"] == 2
  687. end
  688. end
  689. describe "fetch activities for recipients" do
  690. test "retrieve the activities for certain recipients" do
  691. {:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]})
  692. {:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]})
  693. {:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]})
  694. activities = ActivityPub.fetch_activities(["someone", "someone_else"])
  695. assert length(activities) == 2
  696. assert activities == [activity_one, activity_two]
  697. end
  698. end
  699. describe "fetch activities in context" do
  700. test "retrieves activities that have a given context" do
  701. {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
  702. {:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
  703. {:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
  704. {:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"})
  705. activity_five = insert(:note_activity)
  706. user = insert(:user)
  707. {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]})
  708. activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user})
  709. assert activities == [activity_two, activity]
  710. end
  711. test "doesn't return activities with filtered words" do
  712. user = insert(:user)
  713. user_two = insert(:user)
  714. insert(:filter, user: user, phrase: "test", hide: true)
  715. {:ok, %{id: id1, data: %{"context" => context}}} = CommonAPI.post(user, %{status: "1"})
  716. {:ok, %{id: id2}} = CommonAPI.post(user_two, %{status: "2", in_reply_to_status_id: id1})
  717. {:ok, %{id: id3} = user_activity} =
  718. CommonAPI.post(user, %{status: "3 test?", in_reply_to_status_id: id2})
  719. {:ok, %{id: id4} = filtered_activity} =
  720. CommonAPI.post(user_two, %{status: "4 test!", in_reply_to_status_id: id3})
  721. {:ok, _} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
  722. activities =
  723. context
  724. |> ActivityPub.fetch_activities_for_context(%{user: user})
  725. |> Enum.map(& &1.id)
  726. assert length(activities) == 4
  727. assert user_activity.id in activities
  728. refute filtered_activity.id in activities
  729. end
  730. end
  731. test "doesn't return blocked activities" do
  732. activity_one = insert(:note_activity)
  733. activity_two = insert(:note_activity)
  734. activity_three = insert(:note_activity)
  735. user = insert(:user)
  736. booster = insert(:user)
  737. {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]})
  738. activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
  739. assert Enum.member?(activities, activity_two)
  740. assert Enum.member?(activities, activity_three)
  741. refute Enum.member?(activities, activity_one)
  742. {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
  743. activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
  744. assert Enum.member?(activities, activity_two)
  745. assert Enum.member?(activities, activity_three)
  746. assert Enum.member?(activities, activity_one)
  747. {:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]})
  748. {:ok, %{data: %{"object" => id}}} = CommonAPI.repeat(activity_three.id, booster)
  749. %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
  750. activity_three = Activity.get_by_id(activity_three.id)
  751. activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
  752. assert Enum.member?(activities, activity_two)
  753. refute Enum.member?(activities, activity_three)
  754. refute Enum.member?(activities, boost_activity)
  755. assert Enum.member?(activities, activity_one)
  756. activities = ActivityPub.fetch_activities([], %{blocking_user: nil, skip_preload: true})
  757. assert Enum.member?(activities, activity_two)
  758. assert Enum.member?(activities, activity_three)
  759. assert Enum.member?(activities, boost_activity)
  760. assert Enum.member?(activities, activity_one)
  761. end
  762. test "doesn't return activities from deactivated users" do
  763. _user = insert(:user)
  764. deactivated = insert(:user)
  765. active = insert(:user)
  766. {:ok, activity_one} = CommonAPI.post(deactivated, %{status: "hey!"})
  767. {:ok, activity_two} = CommonAPI.post(active, %{status: "yay!"})
  768. {:ok, _updated_user} = User.set_activation(deactivated, false)
  769. activities = ActivityPub.fetch_activities([], %{})
  770. refute Enum.member?(activities, activity_one)
  771. assert Enum.member?(activities, activity_two)
  772. end
  773. test "always see your own posts even when they address people you block" do
  774. user = insert(:user)
  775. blockee = insert(:user)
  776. {:ok, _} = User.block(user, blockee)
  777. {:ok, activity} = CommonAPI.post(user, %{status: "hey! @#{blockee.nickname}"})
  778. activities = ActivityPub.fetch_activities([], %{blocking_user: user})
  779. assert Enum.member?(activities, activity)
  780. end
  781. test "doesn't return transitive interactions concerning blocked users" do
  782. blocker = insert(:user)
  783. blockee = insert(:user)
  784. friend = insert(:user)
  785. {:ok, _user_relationship} = User.block(blocker, blockee)
  786. {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"})
  787. {:ok, activity_two} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"})
  788. {:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"})
  789. {:ok, activity_four} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"})
  790. activities = ActivityPub.fetch_activities([], %{blocking_user: blocker})
  791. assert Enum.member?(activities, activity_one)
  792. refute Enum.member?(activities, activity_two)
  793. refute Enum.member?(activities, activity_three)
  794. refute Enum.member?(activities, activity_four)
  795. end
  796. test "doesn't return announce activities with blocked users in 'to'" do
  797. blocker = insert(:user)
  798. blockee = insert(:user)
  799. friend = insert(:user)
  800. {:ok, _user_relationship} = User.block(blocker, blockee)
  801. {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"})
  802. {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"})
  803. {:ok, activity_three} = CommonAPI.repeat(activity_two.id, friend)
  804. activities =
  805. ActivityPub.fetch_activities([], %{blocking_user: blocker})
  806. |> Enum.map(fn act -> act.id end)
  807. assert Enum.member?(activities, activity_one.id)
  808. refute Enum.member?(activities, activity_two.id)
  809. refute Enum.member?(activities, activity_three.id)
  810. end
  811. test "doesn't return announce activities with blocked users in 'cc'" do
  812. blocker = insert(:user)
  813. blockee = insert(:user)
  814. friend = insert(:user)
  815. {:ok, _user_relationship} = User.block(blocker, blockee)
  816. {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"})
  817. {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"})
  818. assert object = Pleroma.Object.normalize(activity_two, fetch: false)
  819. data = %{
  820. "actor" => friend.ap_id,
  821. "object" => object.data["id"],
  822. "context" => object.data["context"],
  823. "type" => "Announce",
  824. "to" => ["https://www.w3.org/ns/activitystreams#Public"],
  825. "cc" => [blockee.ap_id]
  826. }
  827. assert {:ok, activity_three} = ActivityPub.insert(data)
  828. activities =
  829. ActivityPub.fetch_activities([], %{blocking_user: blocker})
  830. |> Enum.map(fn act -> act.id end)
  831. assert Enum.member?(activities, activity_one.id)
  832. refute Enum.member?(activities, activity_two.id)
  833. refute Enum.member?(activities, activity_three.id)
  834. end
  835. test "doesn't return activities from blocked domains" do
  836. domain = "dogwhistle.zone"
  837. domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
  838. note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
  839. activity = insert(:note_activity, %{note: note})
  840. user = insert(:user)
  841. {:ok, user} = User.block_domain(user, domain)
  842. activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
  843. refute activity in activities
  844. followed_user = insert(:user)
  845. CommonAPI.follow(followed_user, user)
  846. {:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user)
  847. activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
  848. refute repeat_activity in activities
  849. end
  850. test "see your own posts even when they address actors from blocked domains" do
  851. user = insert(:user)
  852. domain = "dogwhistle.zone"
  853. domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
  854. {:ok, user} = User.block_domain(user, domain)
  855. {:ok, activity} = CommonAPI.post(user, %{status: "hey! @#{domain_user.nickname}"})
  856. activities = ActivityPub.fetch_activities([], %{blocking_user: user})
  857. assert Enum.member?(activities, activity)
  858. end
  859. test "does return activities from followed users on blocked domains" do
  860. domain = "meanies.social"
  861. domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
  862. blocker = insert(:user)
  863. {:ok, blocker, domain_user} = User.follow(blocker, domain_user)
  864. {:ok, blocker} = User.block_domain(blocker, domain)
  865. assert User.following?(blocker, domain_user)
  866. assert User.blocks_domain?(blocker, domain_user)
  867. refute User.blocks?(blocker, domain_user)
  868. note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
  869. activity = insert(:note_activity, %{note: note})
  870. activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true})
  871. assert activity in activities
  872. # And check that if the guy we DO follow boosts someone else from their domain,
  873. # that should be hidden
  874. another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"})
  875. bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}})
  876. bad_activity = insert(:note_activity, %{note: bad_note})
  877. {:ok, repeat_activity} = CommonAPI.repeat(bad_activity.id, domain_user)
  878. activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true})
  879. refute repeat_activity in activities
  880. end
  881. test "returns your own posts regardless of mute" do
  882. user = insert(:user)
  883. muted = insert(:user)
  884. {:ok, muted_post} = CommonAPI.post(muted, %{status: "Im stupid"})
  885. {:ok, reply} =
  886. CommonAPI.post(user, %{status: "I'm muting you", in_reply_to_status_id: muted_post.id})
  887. {:ok, _} = User.mute(user, muted)
  888. [activity] = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true})
  889. assert activity.id == reply.id
  890. end
  891. test "doesn't return muted activities" do
  892. activity_one = insert(:note_activity)
  893. activity_two = insert(:note_activity)
  894. activity_three = insert(:note_activity)
  895. user = insert(:user)
  896. booster = insert(:user)
  897. activity_one_actor = User.get_by_ap_id(activity_one.data["actor"])
  898. {:ok, _user_relationships} = User.mute(user, activity_one_actor)
  899. activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true})
  900. assert Enum.member?(activities, activity_two)
  901. assert Enum.member?(activities, activity_three)
  902. refute Enum.member?(activities, activity_one)
  903. # Calling with 'with_muted' will deliver muted activities, too.
  904. activities =
  905. ActivityPub.fetch_activities([], %{
  906. muting_user: user,
  907. with_muted: true,
  908. skip_preload: true
  909. })
  910. assert Enum.member?(activities, activity_two)
  911. assert Enum.member?(activities, activity_three)
  912. assert Enum.member?(activities, activity_one)
  913. {:ok, _user_mute} = User.unmute(user, activity_one_actor)
  914. activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true})
  915. assert Enum.member?(activities, activity_two)
  916. assert Enum.member?(activities, activity_three)
  917. assert Enum.member?(activities, activity_one)
  918. activity_three_actor = User.get_by_ap_id(activity_three.data["actor"])
  919. {:ok, _user_relationships} = User.mute(user, activity_three_actor)
  920. {:ok, %{data: %{"object" => id}}} = CommonAPI.repeat(activity_three.id, booster)
  921. %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
  922. activity_three = Activity.get_by_id(activity_three.id)
  923. activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true})
  924. assert Enum.member?(activities, activity_two)
  925. refute Enum.member?(activities, activity_three)
  926. refute Enum.member?(activities, boost_activity)
  927. assert Enum.member?(activities, activity_one)
  928. activities = ActivityPub.fetch_activities([], %{muting_user: nil, skip_preload: true})
  929. assert Enum.member?(activities, activity_two)
  930. assert Enum.member?(activities, activity_three)
  931. assert Enum.member?(activities, boost_activity)
  932. assert Enum.member?(activities, activity_one)
  933. end
  934. test "doesn't return thread muted activities" do
  935. user = insert(:user)
  936. _activity_one = insert(:note_activity)
  937. note_two = insert(:note, data: %{"context" => "suya.."})
  938. activity_two = insert(:note_activity, note: note_two)
  939. {:ok, _activity_two} = CommonAPI.add_mute(activity_two, user)
  940. assert [_activity_one] = ActivityPub.fetch_activities([], %{muting_user: user})
  941. end
  942. test "returns thread muted activities when with_muted is set" do
  943. user = insert(:user)
  944. _activity_one = insert(:note_activity)
  945. note_two = insert(:note, data: %{"context" => "suya.."})
  946. activity_two = insert(:note_activity, note: note_two)
  947. {:ok, _activity_two} = CommonAPI.add_mute(activity_two, user)
  948. assert [_activity_two, _activity_one] =
  949. ActivityPub.fetch_activities([], %{muting_user: user, with_muted: true})
  950. end
  951. test "does include announces on request" do
  952. activity_three = insert(:note_activity)
  953. user = insert(:user)
  954. booster = insert(:user)
  955. {:ok, user, booster} = User.follow(user, booster)
  956. {:ok, announce} = CommonAPI.repeat(activity_three.id, booster)
  957. [announce_activity] = ActivityPub.fetch_activities([user.ap_id | User.following(user)])
  958. assert announce_activity.id == announce.id
  959. end
  960. test "excludes reblogs on request" do
  961. user = insert(:user)
  962. {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
  963. {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
  964. [activity] = ActivityPub.fetch_user_activities(user, nil, %{exclude_reblogs: true})
  965. assert activity == expected_activity
  966. end
  967. describe "irreversible filters" do
  968. setup do
  969. user = insert(:user)
  970. user_two = insert(:user)
  971. insert(:filter, user: user_two, phrase: "cofe", hide: true)
  972. insert(:filter, user: user_two, phrase: "ok boomer", hide: true)
  973. insert(:filter, user: user_two, phrase: "test", hide: false)
  974. params = %{
  975. type: ["Create", "Announce"],
  976. user: user_two
  977. }
  978. {:ok, %{user: user, user_two: user_two, params: params}}
  979. end
  980. test "it returns statuses if they don't contain exact filter words", %{
  981. user: user,
  982. params: params
  983. } do
  984. {:ok, _} = CommonAPI.post(user, %{status: "hey"})
  985. {:ok, _} = CommonAPI.post(user, %{status: "got cofefe?"})
  986. {:ok, _} = CommonAPI.post(user, %{status: "I am not a boomer"})
  987. {:ok, _} = CommonAPI.post(user, %{status: "ok boomers"})
  988. {:ok, _} = CommonAPI.post(user, %{status: "ccofee is not a word"})
  989. {:ok, _} = CommonAPI.post(user, %{status: "this is a test"})
  990. activities = ActivityPub.fetch_activities([], params)
  991. assert Enum.count(activities) == 6
  992. end
  993. test "it does not filter user's own statuses", %{user_two: user_two, params: params} do
  994. {:ok, _} = CommonAPI.post(user_two, %{status: "Give me some cofe!"})
  995. {:ok, _} = CommonAPI.post(user_two, %{status: "ok boomer"})
  996. activities = ActivityPub.fetch_activities([], params)
  997. assert Enum.count(activities) == 2
  998. end
  999. test "it excludes statuses with filter words", %{user: user, params: params} do
  1000. {:ok, _} = CommonAPI.post(user, %{status: "Give me some cofe!"})
  1001. {:ok, _} = CommonAPI.post(user, %{status: "ok boomer"})
  1002. {:ok, _} = CommonAPI.post(user, %{status: "is it a cOfE?"})
  1003. {:ok, _} = CommonAPI.post(user, %{status: "cofe is all I need"})
  1004. {:ok, _} = CommonAPI.post(user, %{status: "— ok BOOMER\n"})
  1005. activities = ActivityPub.fetch_activities([], params)
  1006. assert Enum.empty?(activities)
  1007. end
  1008. test "it returns all statuses if user does not have any filters" do
  1009. another_user = insert(:user)
  1010. {:ok, _} = CommonAPI.post(another_user, %{status: "got cofe?"})
  1011. {:ok, _} = CommonAPI.post(another_user, %{status: "test!"})
  1012. activities =
  1013. ActivityPub.fetch_activities([], %{
  1014. type: ["Create", "Announce"],
  1015. user: another_user
  1016. })
  1017. assert Enum.count(activities) == 2
  1018. end
  1019. end
  1020. describe "public fetch activities" do
  1021. test "doesn't retrieve unlisted activities" do
  1022. user = insert(:user)
  1023. {:ok, _unlisted_activity} = CommonAPI.post(user, %{status: "yeah", visibility: "unlisted"})
  1024. {:ok, listed_activity} = CommonAPI.post(user, %{status: "yeah"})
  1025. [activity] = ActivityPub.fetch_public_activities()
  1026. assert activity == listed_activity
  1027. end
  1028. test "retrieves public activities" do
  1029. _activities = ActivityPub.fetch_public_activities()
  1030. %{public: public} = ActivityBuilder.public_and_non_public()
  1031. activities = ActivityPub.fetch_public_activities()
  1032. assert length(activities) == 1
  1033. assert Enum.at(activities, 0) == public
  1034. end
  1035. test "retrieves a maximum of 20 activities" do
  1036. ActivityBuilder.insert_list(10)
  1037. expected_activities = ActivityBuilder.insert_list(20)
  1038. activities = ActivityPub.fetch_public_activities()
  1039. assert collect_ids(activities) == collect_ids(expected_activities)
  1040. assert length(activities) == 20
  1041. end
  1042. test "retrieves ids starting from a since_id" do
  1043. activities = ActivityBuilder.insert_list(30)
  1044. expected_activities = ActivityBuilder.insert_list(10)
  1045. since_id = List.last(activities).id
  1046. activities = ActivityPub.fetch_public_activities(%{since_id: since_id})
  1047. assert collect_ids(activities) == collect_ids(expected_activities)
  1048. assert length(activities) == 10
  1049. end
  1050. test "retrieves ids up to max_id" do
  1051. ActivityBuilder.insert_list(10)
  1052. expected_activities = ActivityBuilder.insert_list(20)
  1053. %{id: max_id} =
  1054. 10
  1055. |> ActivityBuilder.insert_list()
  1056. |> List.first()
  1057. activities = ActivityPub.fetch_public_activities(%{max_id: max_id})
  1058. assert length(activities) == 20
  1059. assert collect_ids(activities) == collect_ids(expected_activities)
  1060. end
  1061. test "paginates via offset/limit" do
  1062. _first_part_activities = ActivityBuilder.insert_list(10)
  1063. second_part_activities = ActivityBuilder.insert_list(10)
  1064. later_activities = ActivityBuilder.insert_list(10)
  1065. activities = ActivityPub.fetch_public_activities(%{page: "2", page_size: "20"}, :offset)
  1066. assert length(activities) == 20
  1067. assert collect_ids(activities) ==
  1068. collect_ids(second_part_activities) ++ collect_ids(later_activities)
  1069. end
  1070. test "doesn't return reblogs for users for whom reblogs have been muted" do
  1071. activity = insert(:note_activity)
  1072. user = insert(:user)
  1073. booster = insert(:user)
  1074. {:ok, _reblog_mute} = CommonAPI.hide_reblogs(booster, user)
  1075. {:ok, activity} = CommonAPI.repeat(activity.id, booster)
  1076. activities = ActivityPub.fetch_activities([], %{muting_user: user})
  1077. refute Enum.any?(activities, fn %{id: id} -> id == activity.id end)
  1078. end
  1079. test "returns reblogs for users for whom reblogs have not been muted" do
  1080. activity = insert(:note_activity)
  1081. user = insert(:user)
  1082. booster = insert(:user)
  1083. {:ok, _reblog_mute} = CommonAPI.hide_reblogs(booster, user)
  1084. {:ok, _reblog_mute} = CommonAPI.show_reblogs(booster, user)
  1085. {:ok, activity} = CommonAPI.repeat(activity.id, booster)
  1086. activities = ActivityPub.fetch_activities([], %{muting_user: user})
  1087. assert Enum.any?(activities, fn %{id: id} -> id == activity.id end)
  1088. end
  1089. end
  1090. describe "uploading files" do
  1091. setup do
  1092. test_file = %Plug.Upload{
  1093. content_type: "image/jpeg",
  1094. path: Path.absname("test/fixtures/image.jpg"),
  1095. filename: "an_image.jpg"
  1096. }
  1097. %{test_file: test_file}
  1098. end
  1099. test "strips / from filename", %{test_file: file} do
  1100. file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"}
  1101. {:ok, %Object{} = object} = ActivityPub.upload(file)
  1102. [%{"href" => href}] = object.data["url"]
  1103. assert Regex.match?(~r"/bad.jpg$", href)
  1104. refute Regex.match?(~r"/nested/", href)
  1105. end
  1106. test "sets a description if given", %{test_file: file} do
  1107. {:ok, %Object{} = object} = ActivityPub.upload(file, description: "a cool file")
  1108. assert object.data["name"] == "a cool file"
  1109. end
  1110. test "it sets the default description depending on the configuration", %{test_file: file} do
  1111. clear_config([Pleroma.Upload, :default_description])
  1112. clear_config([Pleroma.Upload, :default_description], nil)
  1113. {:ok, %Object{} = object} = ActivityPub.upload(file)
  1114. assert object.data["name"] == ""
  1115. clear_config([Pleroma.Upload, :default_description], :filename)
  1116. {:ok, %Object{} = object} = ActivityPub.upload(file)
  1117. assert object.data["name"] == "an_image.jpg"
  1118. clear_config([Pleroma.Upload, :default_description], "unnamed attachment")
  1119. {:ok, %Object{} = object} = ActivityPub.upload(file)
  1120. assert object.data["name"] == "unnamed attachment"
  1121. end
  1122. test "copies the file to the configured folder", %{test_file: file} do
  1123. clear_config([Pleroma.Upload, :default_description], :filename)
  1124. {:ok, %Object{} = object} = ActivityPub.upload(file)
  1125. assert object.data["name"] == "an_image.jpg"
  1126. end
  1127. test "works with base64 encoded images" do
  1128. file = %{
  1129. img: data_uri()
  1130. }
  1131. {:ok, %Object{}} = ActivityPub.upload(file)
  1132. end
  1133. end
  1134. describe "fetch the latest Follow" do
  1135. test "fetches the latest Follow activity" do
  1136. %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
  1137. follower = Repo.get_by(User, ap_id: activity.data["actor"])
  1138. followed = Repo.get_by(User, ap_id: activity.data["object"])
  1139. assert activity == Utils.fetch_latest_follow(follower, followed)
  1140. end
  1141. end
  1142. describe "unfollowing" do
  1143. test "it reverts unfollow activity" do
  1144. follower = insert(:user)
  1145. followed = insert(:user)
  1146. {:ok, _, _, follow_activity} = CommonAPI.follow(followed, follower)
  1147. with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
  1148. assert {:error, :reverted} = ActivityPub.unfollow(follower, followed)
  1149. end
  1150. activity = Activity.get_by_id(follow_activity.id)
  1151. assert activity.data["type"] == "Follow"
  1152. assert activity.data["actor"] == follower.ap_id
  1153. assert activity.data["object"] == followed.ap_id
  1154. end
  1155. test "creates an undo activity for the last follow" do
  1156. follower = insert(:user)
  1157. followed = insert(:user)
  1158. {:ok, _, _, follow_activity} = CommonAPI.follow(followed, follower)
  1159. {:ok, activity} = ActivityPub.unfollow(follower, followed)
  1160. assert activity.data["type"] == "Undo"
  1161. assert activity.data["actor"] == follower.ap_id
  1162. embedded_object = activity.data["object"]
  1163. assert is_map(embedded_object)
  1164. assert embedded_object["type"] == "Follow"
  1165. assert embedded_object["object"] == followed.ap_id
  1166. assert embedded_object["id"] == follow_activity.data["id"]
  1167. end
  1168. test "creates an undo activity for a pending follow request" do
  1169. follower = insert(:user)
  1170. followed = insert(:user, %{is_locked: true})
  1171. {:ok, _, _, follow_activity} = CommonAPI.follow(followed, follower)
  1172. {:ok, activity} = ActivityPub.unfollow(follower, followed)
  1173. assert activity.data["type"] == "Undo"
  1174. assert activity.data["actor"] == follower.ap_id
  1175. embedded_object = activity.data["object"]
  1176. assert is_map(embedded_object)
  1177. assert embedded_object["type"] == "Follow"
  1178. assert embedded_object["object"] == followed.ap_id
  1179. assert embedded_object["id"] == follow_activity.data["id"]
  1180. end
  1181. end
  1182. describe "timeline post-processing" do
  1183. test "it filters broken threads" do
  1184. user1 = insert(:user)
  1185. user2 = insert(:user)
  1186. user3 = insert(:user)
  1187. {:ok, user1, user3} = User.follow(user1, user3)
  1188. assert User.following?(user1, user3)
  1189. {:ok, user2, user3} = User.follow(user2, user3)
  1190. assert User.following?(user2, user3)
  1191. {:ok, user3, user2} = User.follow(user3, user2)
  1192. assert User.following?(user3, user2)
  1193. {:ok, public_activity} = CommonAPI.post(user3, %{status: "hi 1"})
  1194. {:ok, private_activity_1} = CommonAPI.post(user3, %{status: "hi 2", visibility: "private"})
  1195. {:ok, private_activity_2} =
  1196. CommonAPI.post(user2, %{
  1197. status: "hi 3",
  1198. visibility: "private",
  1199. in_reply_to_status_id: private_activity_1.id
  1200. })
  1201. {:ok, private_activity_3} =
  1202. CommonAPI.post(user3, %{
  1203. status: "hi 4",
  1204. visibility: "private",
  1205. in_reply_to_status_id: private_activity_2.id
  1206. })
  1207. activities =
  1208. ActivityPub.fetch_activities([user1.ap_id | User.following(user1)])
  1209. |> Enum.map(fn a -> a.id end)
  1210. private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
  1211. assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities
  1212. assert length(activities) == 3
  1213. activities =
  1214. ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{user: user1})
  1215. |> Enum.map(fn a -> a.id end)
  1216. assert [public_activity.id, private_activity_1.id] == activities
  1217. assert length(activities) == 2
  1218. end
  1219. end
  1220. describe "flag/1" do
  1221. setup do
  1222. reporter = insert(:user)
  1223. target_account = insert(:user)
  1224. content = "foobar"
  1225. {:ok, activity} = CommonAPI.post(target_account, %{status: content})
  1226. context = Utils.generate_context_id()
  1227. reporter_ap_id = reporter.ap_id
  1228. target_ap_id = target_account.ap_id
  1229. activity_ap_id = activity.data["id"]
  1230. object_ap_id = activity.object.data["id"]
  1231. activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id)
  1232. {:ok,
  1233. %{
  1234. reporter: reporter,
  1235. context: context,
  1236. target_account: target_account,
  1237. reported_activity: activity,
  1238. content: content,
  1239. activity_ap_id: activity_ap_id,
  1240. object_ap_id: object_ap_id,
  1241. activity_with_object: activity_with_object,
  1242. reporter_ap_id: reporter_ap_id,
  1243. target_ap_id: target_ap_id
  1244. }}
  1245. end
  1246. test "it can create a Flag activity",
  1247. %{
  1248. reporter: reporter,
  1249. context: context,
  1250. target_account: target_account,
  1251. reported_activity: reported_activity,
  1252. content: content,
  1253. object_ap_id: object_ap_id,
  1254. activity_with_object: activity_with_object,
  1255. reporter_ap_id: reporter_ap_id,
  1256. target_ap_id: target_ap_id
  1257. } do
  1258. assert {:ok, activity} =
  1259. ActivityPub.flag(%{
  1260. actor: reporter,
  1261. context: context,
  1262. account: target_account,
  1263. statuses: [reported_activity],
  1264. content: content
  1265. })
  1266. note_obj = %{
  1267. "type" => "Note",
  1268. "id" => object_ap_id,
  1269. "content" => content,
  1270. "published" => activity_with_object.object.data["published"],
  1271. "actor" =>
  1272. AccountView.render("show.json", %{user: target_account, skip_visibility_check: true})
  1273. }
  1274. assert %Activity{
  1275. actor: ^reporter_ap_id,
  1276. data: %{
  1277. "type" => "Flag",
  1278. "content" => ^content,
  1279. "context" => ^context,
  1280. "object" => [^target_ap_id, ^note_obj]
  1281. }
  1282. } = activity
  1283. end
  1284. test_with_mock "strips status data from Flag, before federating it",
  1285. %{
  1286. reporter: reporter,
  1287. context: context,
  1288. target_account: target_account,
  1289. reported_activity: reported_activity,
  1290. object_ap_id: object_ap_id,
  1291. content: content
  1292. },
  1293. Utils,
  1294. [:passthrough],
  1295. [] do
  1296. {:ok, activity} =
  1297. ActivityPub.flag(%{
  1298. actor: reporter,
  1299. context: context,
  1300. account: target_account,
  1301. statuses: [reported_activity],
  1302. content: content
  1303. })
  1304. new_data = put_in(activity.data, ["object"], [target_account.ap_id, object_ap_id])
  1305. assert_called(Utils.maybe_federate(%{activity | data: new_data}))
  1306. end
  1307. test_with_mock "reverts on error",
  1308. %{
  1309. reporter: reporter,
  1310. context: context,
  1311. target_account: target_account,
  1312. reported_activity: reported_activity,
  1313. content: content
  1314. },
  1315. Utils,
  1316. [:passthrough],
  1317. maybe_federate: fn _ -> {:error, :reverted} end do
  1318. assert {:error, :reverted} =
  1319. ActivityPub.flag(%{
  1320. actor: reporter,
  1321. context: context,
  1322. account: target_account,
  1323. statuses: [reported_activity],
  1324. content: content
  1325. })
  1326. assert Repo.aggregate(Activity, :count, :id) == 1
  1327. assert Repo.aggregate(Object, :count, :id) == 1
  1328. assert Repo.aggregate(Notification, :count, :id) == 0
  1329. end
  1330. end
  1331. test "fetch_activities/2 returns activities addressed to a list " do
  1332. user = insert(:user)
  1333. member = insert(:user)
  1334. {:ok, list} = Pleroma.List.create("foo", user)
  1335. {:ok, list} = Pleroma.List.follow(list, member)
  1336. {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
  1337. activity = Repo.preload(activity, :bookmark)
  1338. activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
  1339. assert ActivityPub.fetch_activities([], %{user: user}) == [activity]
  1340. end
  1341. def data_uri do
  1342. File.read!("test/fixtures/avatar_data_uri")
  1343. end
  1344. describe "fetch_activities_bounded" do
  1345. test "fetches private posts for followed users" do
  1346. user = insert(:user)
  1347. {:ok, activity} =
  1348. CommonAPI.post(user, %{
  1349. status: "thought I looked cute might delete later :3",
  1350. visibility: "private"
  1351. })
  1352. [result] = ActivityPub.fetch_activities_bounded([user.follower_address], [])
  1353. assert result.id == activity.id
  1354. end
  1355. test "fetches only public posts for other users" do
  1356. user = insert(:user)
  1357. {:ok, activity} = CommonAPI.post(user, %{status: "#cofe", visibility: "public"})
  1358. {:ok, _private_activity} =
  1359. CommonAPI.post(user, %{
  1360. status: "why is tenshi eating a corndog so cute?",
  1361. visibility: "private"
  1362. })
  1363. [result] = ActivityPub.fetch_activities_bounded([], [user.follower_address])
  1364. assert result.id == activity.id
  1365. end
  1366. end
  1367. describe "fetch_follow_information_for_user" do
  1368. test "synchronizes following/followers counters" do
  1369. user =
  1370. insert(:user,
  1371. local: false,
  1372. follower_address: "http://localhost:4001/users/fuser2/followers",
  1373. following_address: "http://localhost:4001/users/fuser2/following"
  1374. )
  1375. {:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
  1376. assert info.follower_count == 527
  1377. assert info.following_count == 267
  1378. end
  1379. test "detects hidden followers" do
  1380. mock(fn env ->
  1381. case env.url do
  1382. "http://localhost:4001/users/masto_closed/followers?page=1" ->
  1383. %Tesla.Env{status: 403, body: ""}
  1384. _ ->
  1385. apply(HttpRequestMock, :request, [env])
  1386. end
  1387. end)
  1388. user =
  1389. insert(:user,
  1390. local: false,
  1391. follower_address: "http://localhost:4001/users/masto_closed/followers",
  1392. following_address: "http://localhost:4001/users/masto_closed/following"
  1393. )
  1394. {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
  1395. assert follow_info.hide_followers == true
  1396. assert follow_info.hide_follows == false
  1397. end
  1398. test "detects hidden follows" do
  1399. mock(fn env ->
  1400. case env.url do
  1401. "http://localhost:4001/users/masto_closed/following?page=1" ->
  1402. %Tesla.Env{status: 403, body: ""}
  1403. _ ->
  1404. apply(HttpRequestMock, :request, [env])
  1405. end
  1406. end)
  1407. user =
  1408. insert(:user,
  1409. local: false,
  1410. follower_address: "http://localhost:4001/users/masto_closed/followers",
  1411. following_address: "http://localhost:4001/users/masto_closed/following"
  1412. )
  1413. {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
  1414. assert follow_info.hide_followers == false
  1415. assert follow_info.hide_follows == true
  1416. end
  1417. test "detects hidden follows/followers for friendica" do
  1418. user =
  1419. insert(:user,
  1420. local: false,
  1421. follower_address: "http://localhost:8080/followers/fuser3",
  1422. following_address: "http://localhost:8080/following/fuser3"
  1423. )
  1424. {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
  1425. assert follow_info.hide_followers == true
  1426. assert follow_info.follower_count == 296
  1427. assert follow_info.following_count == 32
  1428. assert follow_info.hide_follows == true
  1429. end
  1430. test "doesn't crash when follower and following counters are hidden" do
  1431. mock(fn env ->
  1432. case env.url do
  1433. "http://localhost:4001/users/masto_hidden_counters/following" ->
  1434. json(
  1435. %{
  1436. "@context" => "https://www.w3.org/ns/activitystreams",
  1437. "id" => "http://localhost:4001/users/masto_hidden_counters/followers"
  1438. },
  1439. headers: HttpRequestMock.activitypub_object_headers()
  1440. )
  1441. "http://localhost:4001/users/masto_hidden_counters/following?page=1" ->
  1442. %Tesla.Env{status: 403, body: ""}
  1443. "http://localhost:4001/users/masto_hidden_counters/followers" ->
  1444. json(
  1445. %{
  1446. "@context" => "https://www.w3.org/ns/activitystreams",
  1447. "id" => "http://localhost:4001/users/masto_hidden_counters/following"
  1448. },
  1449. headers: HttpRequestMock.activitypub_object_headers()
  1450. )
  1451. "http://localhost:4001/users/masto_hidden_counters/followers?page=1" ->
  1452. %Tesla.Env{status: 403, body: ""}
  1453. end
  1454. end)
  1455. user =
  1456. insert(:user,
  1457. local: false,
  1458. follower_address: "http://localhost:4001/users/masto_hidden_counters/followers",
  1459. following_address: "http://localhost:4001/users/masto_hidden_counters/following"
  1460. )
  1461. {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
  1462. assert follow_info.hide_followers == true
  1463. assert follow_info.follower_count == 0
  1464. assert follow_info.hide_follows == true
  1465. assert follow_info.following_count == 0
  1466. end
  1467. end
  1468. describe "fetch_favourites/3" do
  1469. test "returns a favourite activities sorted by adds to favorite" do
  1470. user = insert(:user)
  1471. other_user = insert(:user)
  1472. user1 = insert(:user)
  1473. user2 = insert(:user)
  1474. {:ok, a1} = CommonAPI.post(user1, %{status: "bla"})
  1475. {:ok, _a2} = CommonAPI.post(user2, %{status: "traps are happy"})
  1476. {:ok, a3} = CommonAPI.post(user2, %{status: "Trees Are "})
  1477. {:ok, a4} = CommonAPI.post(user2, %{status: "Agent Smith "})
  1478. {:ok, a5} = CommonAPI.post(user1, %{status: "Red or Blue "})
  1479. {:ok, _} = CommonAPI.favorite(a4.id, user)
  1480. {:ok, _} = CommonAPI.favorite(a3.id, other_user)
  1481. {:ok, _} = CommonAPI.favorite(a3.id, user)
  1482. {:ok, _} = CommonAPI.favorite(a5.id, other_user)
  1483. {:ok, _} = CommonAPI.favorite(a5.id, user)
  1484. {:ok, _} = CommonAPI.favorite(a4.id, other_user)
  1485. {:ok, _} = CommonAPI.favorite(a1.id, user)
  1486. {:ok, _} = CommonAPI.favorite(a1.id, other_user)
  1487. result = ActivityPub.fetch_favourites(user)
  1488. assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]
  1489. result = ActivityPub.fetch_favourites(user, %{limit: 2})
  1490. assert Enum.map(result, & &1.id) == [a1.id, a5.id]
  1491. end
  1492. end
  1493. describe "Move activity" do
  1494. test "create" do
  1495. %{ap_id: old_ap_id} = old_user = insert(:user)
  1496. %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
  1497. follower = insert(:user)
  1498. follower_move_opted_out = insert(:user, allow_following_move: false)
  1499. User.follow(follower, old_user)
  1500. User.follow(follower_move_opted_out, old_user)
  1501. assert User.following?(follower, old_user)
  1502. assert User.following?(follower_move_opted_out, old_user)
  1503. assert {:ok, activity} = ActivityPub.move(old_user, new_user)
  1504. assert %Activity{
  1505. actor: ^old_ap_id,
  1506. data: %{
  1507. "actor" => ^old_ap_id,
  1508. "object" => ^old_ap_id,
  1509. "target" => ^new_ap_id,
  1510. "type" => "Move"
  1511. },
  1512. local: true,
  1513. recipients: recipients
  1514. } = activity
  1515. assert old_user.follower_address in recipients
  1516. params = %{
  1517. "op" => "move_following",
  1518. "origin_id" => old_user.id,
  1519. "target_id" => new_user.id
  1520. }
  1521. assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
  1522. Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params})
  1523. refute User.following?(follower, old_user)
  1524. assert User.following?(follower, new_user)
  1525. assert User.following?(follower_move_opted_out, old_user)
  1526. refute User.following?(follower_move_opted_out, new_user)
  1527. activity = %Activity{activity | object: nil}
  1528. assert [%Notification{activity: ^activity}] = Notification.for_user(follower)
  1529. assert [%Notification{activity: ^activity}] = Notification.for_user(follower_move_opted_out)
  1530. end
  1531. test "old user must be in the new user's `also_known_as` list" do
  1532. old_user = insert(:user)
  1533. new_user = insert(:user)
  1534. assert {:error, "Target account must have the origin in `alsoKnownAs`"} =
  1535. ActivityPub.move(old_user, new_user)
  1536. end
  1537. test "do not move remote user following relationships" do
  1538. %{ap_id: old_ap_id} = old_user = insert(:user)
  1539. %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
  1540. follower_remote = insert(:user, local: false)
  1541. User.follow(follower_remote, old_user)
  1542. assert User.following?(follower_remote, old_user)
  1543. assert {:ok, activity} = ActivityPub.move(old_user, new_user)
  1544. assert %Activity{
  1545. actor: ^old_ap_id,
  1546. data: %{
  1547. "actor" => ^old_ap_id,
  1548. "object" => ^old_ap_id,
  1549. "target" => ^new_ap_id,
  1550. "type" => "Move"
  1551. },
  1552. local: true
  1553. } = activity
  1554. params = %{
  1555. "op" => "move_following",
  1556. "origin_id" => old_user.id,
  1557. "target_id" => new_user.id
  1558. }
  1559. assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
  1560. Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params})
  1561. assert User.following?(follower_remote, old_user)
  1562. refute User.following?(follower_remote, new_user)
  1563. end
  1564. end
  1565. test "doesn't retrieve replies activities with exclude_replies" do
  1566. user = insert(:user)
  1567. {:ok, activity} = CommonAPI.post(user, %{status: "yeah"})
  1568. {:ok, _reply} = CommonAPI.post(user, %{status: "yeah", in_reply_to_status_id: activity.id})
  1569. [result] = ActivityPub.fetch_public_activities(%{exclude_replies: true})
  1570. assert result.id == activity.id
  1571. assert length(ActivityPub.fetch_public_activities()) == 2
  1572. end
  1573. describe "replies filtering with public messages" do
  1574. setup :public_messages
  1575. test "public timeline", %{users: %{u1: user}} do
  1576. activities_ids =
  1577. %{}
  1578. |> Map.put(:type, ["Create", "Announce"])
  1579. |> Map.put(:local_only, false)
  1580. |> Map.put(:blocking_user, user)
  1581. |> Map.put(:muting_user, user)
  1582. |> Map.put(:reply_filtering_user, user)
  1583. |> ActivityPub.fetch_public_activities()
  1584. |> Enum.map(& &1.id)
  1585. assert length(activities_ids) == 16
  1586. end
  1587. test "public timeline with reply_visibility `following`", %{
  1588. users: %{u1: user},
  1589. u1: u1,
  1590. u2: u2,
  1591. u3: u3,
  1592. u4: u4,
  1593. activities: activities
  1594. } do
  1595. activities_ids =
  1596. %{}
  1597. |> Map.put(:type, ["Create", "Announce"])
  1598. |> Map.put(:local_only, false)
  1599. |> Map.put(:blocking_user, user)
  1600. |> Map.put(:muting_user, user)
  1601. |> Map.put(:reply_visibility, "following")
  1602. |> Map.put(:reply_filtering_user, user)
  1603. |> ActivityPub.fetch_public_activities()
  1604. |> Enum.map(& &1.id)
  1605. assert length(activities_ids) == 14
  1606. visible_ids =
  1607. Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]]
  1608. assert Enum.all?(visible_ids, &(&1 in activities_ids))
  1609. end
  1610. test "public timeline with reply_visibility `self`", %{
  1611. users: %{u1: user},
  1612. u1: u1,
  1613. u2: u2,
  1614. u3: u3,
  1615. u4: u4,
  1616. activities: activities
  1617. } do
  1618. activities_ids =
  1619. %{}
  1620. |> Map.put(:type, ["Create", "Announce"])
  1621. |> Map.put(:local_only, false)
  1622. |> Map.put(:blocking_user, user)
  1623. |> Map.put(:muting_user, user)
  1624. |> Map.put(:reply_visibility, "self")
  1625. |> Map.put(:reply_filtering_user, user)
  1626. |> ActivityPub.fetch_public_activities()
  1627. |> Enum.map(& &1.id)
  1628. assert length(activities_ids) == 10
  1629. visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities)
  1630. assert Enum.all?(visible_ids, &(&1 in activities_ids))
  1631. end
  1632. test "home timeline", %{
  1633. users: %{u1: user},
  1634. activities: activities,
  1635. u1: u1,
  1636. u2: u2,
  1637. u3: u3,
  1638. u4: u4
  1639. } do
  1640. params =
  1641. %{}
  1642. |> Map.put(:type, ["Create", "Announce"])
  1643. |> Map.put(:blocking_user, user)
  1644. |> Map.put(:muting_user, user)
  1645. |> Map.put(:user, user)
  1646. |> Map.put(:reply_filtering_user, user)
  1647. activities_ids =
  1648. ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
  1649. |> Enum.map(& &1.id)
  1650. assert length(activities_ids) == 13
  1651. visible_ids =
  1652. Map.values(u1) ++
  1653. Map.values(u3) ++
  1654. [
  1655. activities[:a1],
  1656. activities[:a2],
  1657. activities[:a4],
  1658. u2[:r1],
  1659. u2[:r3],
  1660. u4[:r1],
  1661. u4[:r2]
  1662. ]
  1663. assert Enum.all?(visible_ids, &(&1 in activities_ids))
  1664. end
  1665. test "home timeline with reply_visibility `following`", %{
  1666. users: %{u1: user},
  1667. activities: activities,
  1668. u1: u1,
  1669. u2: u2,
  1670. u3: u3,
  1671. u4: u4
  1672. } do
  1673. params =
  1674. %{}
  1675. |> Map.put(:type, ["Create", "Announce"])
  1676. |> Map.put(:blocking_user, user)
  1677. |> Map.put(:muting_user, user)
  1678. |> Map.put(:user, user)
  1679. |> Map.put(:reply_visibility, "following")
  1680. |> Map.put(:reply_filtering_user, user)
  1681. activities_ids =
  1682. ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
  1683. |> Enum.map(& &1.id)
  1684. assert length(activities_ids) == 11
  1685. visible_ids =
  1686. Map.values(u1) ++
  1687. [
  1688. activities[:a1],
  1689. activities[:a2],
  1690. activities[:a4],
  1691. u2[:r1],
  1692. u2[:r3],
  1693. u3[:r1],
  1694. u4[:r1],
  1695. u4[:r2]
  1696. ]
  1697. assert Enum.all?(visible_ids, &(&1 in activities_ids))
  1698. end
  1699. test "home timeline with reply_visibility `self`", %{
  1700. users: %{u1: user},
  1701. activities: activities,
  1702. u1: u1,
  1703. u2: u2,
  1704. u3: u3,
  1705. u4: u4
  1706. } do
  1707. params =
  1708. %{}
  1709. |> Map.put(:type, ["Create", "Announce"])
  1710. |> Map.put(:blocking_user, user)
  1711. |> Map.put(:muting_user, user)
  1712. |> Map.put(:user, user)
  1713. |> Map.put(:reply_visibility, "self")
  1714. |> Map.put(:reply_filtering_user, user)
  1715. activities_ids =
  1716. ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
  1717. |> Enum.map(& &1.id)
  1718. assert length(activities_ids) == 9
  1719. visible_ids =
  1720. Map.values(u1) ++
  1721. [
  1722. activities[:a1],
  1723. activities[:a2],
  1724. activities[:a4],
  1725. u2[:r1],
  1726. u3[:r1],
  1727. u4[:r1]
  1728. ]
  1729. assert Enum.all?(visible_ids, &(&1 in activities_ids))
  1730. end
  1731. test "filtering out announces where the user is the actor of the announced message" do
  1732. user = insert(:user)
  1733. other_user = insert(:user)
  1734. third_user = insert(:user)
  1735. User.follow(user, other_user)
  1736. {:ok, post} = CommonAPI.post(user, %{status: "yo"})
  1737. {:ok, other_post} = CommonAPI.post(third_user, %{status: "yo"})
  1738. {:ok, _announce} = CommonAPI.repeat(post.id, other_user)
  1739. {:ok, _announce} = CommonAPI.repeat(post.id, third_user)
  1740. {:ok, announce} = CommonAPI.repeat(other_post.id, other_user)
  1741. params = %{
  1742. type: ["Announce"]
  1743. }
  1744. results =
  1745. [user.ap_id | User.following(user)]
  1746. |> ActivityPub.fetch_activities(params)
  1747. assert length(results) == 3
  1748. params = %{
  1749. type: ["Announce"],
  1750. announce_filtering_user: user
  1751. }
  1752. [result] =
  1753. [user.ap_id | User.following(user)]
  1754. |> ActivityPub.fetch_activities(params)
  1755. assert result.id == announce.id
  1756. end
  1757. end
  1758. describe "replies filtering with private messages" do
  1759. setup :private_messages
  1760. test "public timeline", %{users: %{u1: user}} do
  1761. activities_ids =
  1762. %{}
  1763. |> Map.put(:type, ["Create", "Announce"])
  1764. |> Map.put(:local_only, false)
  1765. |> Map.put(:blocking_user, user)
  1766. |> Map.put(:muting_user, user)
  1767. |> Map.put(:user, user)
  1768. |> ActivityPub.fetch_public_activities()
  1769. |> Enum.map(& &1.id)
  1770. assert activities_ids == []
  1771. end
  1772. test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do
  1773. activities_ids =
  1774. %{}
  1775. |> Map.put(:type, ["Create", "Announce"])
  1776. |> Map.put(:local_only, false)
  1777. |> Map.put(:blocking_user, user)
  1778. |> Map.put(:muting_user, user)
  1779. |> Map.put(:reply_visibility, "following")
  1780. |> Map.put(:reply_filtering_user, user)
  1781. |> Map.put(:user, user)
  1782. |> ActivityPub.fetch_public_activities()
  1783. |> Enum.map(& &1.id)
  1784. assert activities_ids == []
  1785. end
  1786. test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do
  1787. activities_ids =
  1788. %{}
  1789. |> Map.put(:type, ["Create", "Announce"])
  1790. |> Map.put(:local_only, false)
  1791. |> Map.put(:blocking_user, user)
  1792. |> Map.put(:muting_user, user)
  1793. |> Map.put(:reply_visibility, "self")
  1794. |> Map.put(:reply_filtering_user, user)
  1795. |> Map.put(:user, user)
  1796. |> ActivityPub.fetch_public_activities()
  1797. |> Enum.map(& &1.id)
  1798. assert activities_ids == []
  1799. activities_ids =
  1800. %{}
  1801. |> Map.put(:reply_visibility, "self")
  1802. |> Map.put(:reply_filtering_user, nil)
  1803. |> ActivityPub.fetch_public_activities()
  1804. assert activities_ids == []
  1805. end
  1806. test "home timeline", %{users: %{u1: user}} do
  1807. params =
  1808. %{}
  1809. |> Map.put(:type, ["Create", "Announce"])
  1810. |> Map.put(:blocking_user, user)
  1811. |> Map.put(:muting_user, user)
  1812. |> Map.put(:user, user)
  1813. activities_ids =
  1814. ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
  1815. |> Enum.map(& &1.id)
  1816. assert length(activities_ids) == 12
  1817. end
  1818. test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do
  1819. params =
  1820. %{}
  1821. |> Map.put(:type, ["Create", "Announce"])
  1822. |> Map.put(:blocking_user, user)
  1823. |> Map.put(:muting_user, user)
  1824. |> Map.put(:user, user)
  1825. |> Map.put(:reply_visibility, "following")
  1826. |> Map.put(:reply_filtering_user, user)
  1827. activities_ids =
  1828. ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
  1829. |> Enum.map(& &1.id)
  1830. assert length(activities_ids) == 12
  1831. end
  1832. test "home timeline with default reply_visibility `self`", %{
  1833. users: %{u1: user},
  1834. activities: activities,
  1835. u1: u1,
  1836. u2: u2,
  1837. u3: u3,
  1838. u4: u4
  1839. } do
  1840. params =
  1841. %{}
  1842. |> Map.put(:type, ["Create", "Announce"])
  1843. |> Map.put(:blocking_user, user)
  1844. |> Map.put(:muting_user, user)
  1845. |> Map.put(:user, user)
  1846. |> Map.put(:reply_visibility, "self")
  1847. |> Map.put(:reply_filtering_user, user)
  1848. activities_ids =
  1849. ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
  1850. |> Enum.map(& &1.id)
  1851. assert length(activities_ids) == 10
  1852. visible_ids =
  1853. Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities)
  1854. assert Enum.all?(visible_ids, &(&1 in activities_ids))
  1855. end
  1856. end
  1857. defp public_messages(_) do
  1858. [u1, u2, u3, u4] = insert_list(4, :user)
  1859. {:ok, u1, u2} = User.follow(u1, u2)
  1860. {:ok, u2, u1} = User.follow(u2, u1)
  1861. {:ok, u1, u4} = User.follow(u1, u4)
  1862. {:ok, u4, u1} = User.follow(u4, u1)
  1863. {:ok, u2, u3} = User.follow(u2, u3)
  1864. {:ok, u3, u2} = User.follow(u3, u2)
  1865. {:ok, a1} = CommonAPI.post(u1, %{status: "Status"})
  1866. {:ok, r1_1} =
  1867. CommonAPI.post(u2, %{
  1868. status: "@#{u1.nickname} reply from u2 to u1",
  1869. in_reply_to_status_id: a1.id
  1870. })
  1871. {:ok, r1_2} =
  1872. CommonAPI.post(u3, %{
  1873. status: "@#{u1.nickname} reply from u3 to u1",
  1874. in_reply_to_status_id: a1.id
  1875. })
  1876. {:ok, r1_3} =
  1877. CommonAPI.post(u4, %{
  1878. status: "@#{u1.nickname} reply from u4 to u1",
  1879. in_reply_to_status_id: a1.id
  1880. })
  1881. {:ok, a2} = CommonAPI.post(u2, %{status: "Status"})
  1882. {:ok, r2_1} =
  1883. CommonAPI.post(u1, %{
  1884. status: "@#{u2.nickname} reply from u1 to u2",
  1885. in_reply_to_status_id: a2.id
  1886. })
  1887. {:ok, r2_2} =
  1888. CommonAPI.post(u3, %{
  1889. status: "@#{u2.nickname} reply from u3 to u2",
  1890. in_reply_to_status_id: a2.id
  1891. })
  1892. {:ok, r2_3} =
  1893. CommonAPI.post(u4, %{
  1894. status: "@#{u2.nickname} reply from u4 to u2",
  1895. in_reply_to_status_id: a2.id
  1896. })
  1897. {:ok, a3} = CommonAPI.post(u3, %{status: "Status"})
  1898. {:ok, r3_1} =
  1899. CommonAPI.post(u1, %{
  1900. status: "@#{u3.nickname} reply from u1 to u3",
  1901. in_reply_to_status_id: a3.id
  1902. })
  1903. {:ok, r3_2} =
  1904. CommonAPI.post(u2, %{
  1905. status: "@#{u3.nickname} reply from u2 to u3",
  1906. in_reply_to_status_id: a3.id
  1907. })
  1908. {:ok, r3_3} =
  1909. CommonAPI.post(u4, %{
  1910. status: "@#{u3.nickname} reply from u4 to u3",
  1911. in_reply_to_status_id: a3.id
  1912. })
  1913. {:ok, a4} = CommonAPI.post(u4, %{status: "Status"})
  1914. {:ok, r4_1} =
  1915. CommonAPI.post(u1, %{
  1916. status: "@#{u4.nickname} reply from u1 to u4",
  1917. in_reply_to_status_id: a4.id
  1918. })
  1919. {:ok, r4_2} =
  1920. CommonAPI.post(u2, %{
  1921. status: "@#{u4.nickname} reply from u2 to u4",
  1922. in_reply_to_status_id: a4.id
  1923. })
  1924. {:ok, r4_3} =
  1925. CommonAPI.post(u3, %{
  1926. status: "@#{u4.nickname} reply from u3 to u4",
  1927. in_reply_to_status_id: a4.id
  1928. })
  1929. {:ok,
  1930. users: %{u1: u1, u2: u2, u3: u3, u4: u4},
  1931. activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
  1932. u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
  1933. u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id},
  1934. u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id},
  1935. u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}}
  1936. end
  1937. defp private_messages(_) do
  1938. [u1, u2, u3, u4] = insert_list(4, :user)
  1939. {:ok, u1, u2} = User.follow(u1, u2)
  1940. {:ok, u2, u1} = User.follow(u2, u1)
  1941. {:ok, u1, u3} = User.follow(u1, u3)
  1942. {:ok, u3, u1} = User.follow(u3, u1)
  1943. {:ok, u1, u4} = User.follow(u1, u4)
  1944. {:ok, u4, u1} = User.follow(u4, u1)
  1945. {:ok, u2, u3} = User.follow(u2, u3)
  1946. {:ok, u3, u2} = User.follow(u3, u2)
  1947. {:ok, a1} = CommonAPI.post(u1, %{status: "Status", visibility: "private"})
  1948. {:ok, r1_1} =
  1949. CommonAPI.post(u2, %{
  1950. status: "@#{u1.nickname} reply from u2 to u1",
  1951. in_reply_to_status_id: a1.id,
  1952. visibility: "private"
  1953. })
  1954. {:ok, r1_2} =
  1955. CommonAPI.post(u3, %{
  1956. status: "@#{u1.nickname} reply from u3 to u1",
  1957. in_reply_to_status_id: a1.id,
  1958. visibility: "private"
  1959. })
  1960. {:ok, r1_3} =
  1961. CommonAPI.post(u4, %{
  1962. status: "@#{u1.nickname} reply from u4 to u1",
  1963. in_reply_to_status_id: a1.id,
  1964. visibility: "private"
  1965. })
  1966. {:ok, a2} = CommonAPI.post(u2, %{status: "Status", visibility: "private"})
  1967. {:ok, r2_1} =
  1968. CommonAPI.post(u1, %{
  1969. status: "@#{u2.nickname} reply from u1 to u2",
  1970. in_reply_to_status_id: a2.id,
  1971. visibility: "private"
  1972. })
  1973. {:ok, r2_2} =
  1974. CommonAPI.post(u3, %{
  1975. status: "@#{u2.nickname} reply from u3 to u2",
  1976. in_reply_to_status_id: a2.id,
  1977. visibility: "private"
  1978. })
  1979. {:ok, a3} = CommonAPI.post(u3, %{status: "Status", visibility: "private"})
  1980. {:ok, r3_1} =
  1981. CommonAPI.post(u1, %{
  1982. status: "@#{u3.nickname} reply from u1 to u3",
  1983. in_reply_to_status_id: a3.id,
  1984. visibility: "private"
  1985. })
  1986. {:ok, r3_2} =
  1987. CommonAPI.post(u2, %{
  1988. status: "@#{u3.nickname} reply from u2 to u3",
  1989. in_reply_to_status_id: a3.id,
  1990. visibility: "private"
  1991. })
  1992. {:ok, a4} = CommonAPI.post(u4, %{status: "Status", visibility: "private"})
  1993. {:ok, r4_1} =
  1994. CommonAPI.post(u1, %{
  1995. status: "@#{u4.nickname} reply from u1 to u4",
  1996. in_reply_to_status_id: a4.id,
  1997. visibility: "private"
  1998. })
  1999. {:ok,
  2000. users: %{u1: u1, u2: u2, u3: u3, u4: u4},
  2001. activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
  2002. u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
  2003. u2: %{r1: r2_1.id, r2: r2_2.id},
  2004. u3: %{r1: r3_1.id, r2: r3_2.id},
  2005. u4: %{r1: r4_1.id}}
  2006. end
  2007. describe "maybe_update_follow_information/1" do
  2008. setup do
  2009. clear_config([:instance, :external_user_synchronization], true)
  2010. user = %{
  2011. local: false,
  2012. ap_id: "https://gensokyo.2hu/users/raymoo",
  2013. following_address: "https://gensokyo.2hu/users/following",
  2014. follower_address: "https://gensokyo.2hu/users/followers",
  2015. type: "Person"
  2016. }
  2017. %{user: user}
  2018. end
  2019. test "logs an error when it can't fetch the info", %{user: user} do
  2020. assert capture_log(fn ->
  2021. ActivityPub.maybe_update_follow_information(user)
  2022. end) =~ "Follower/Following counter update for #{user.ap_id} failed"
  2023. end
  2024. test "just returns the input if the user type is Application", %{
  2025. user: user
  2026. } do
  2027. user =
  2028. user
  2029. |> Map.put(:type, "Application")
  2030. refute capture_log(fn ->
  2031. assert ^user = ActivityPub.maybe_update_follow_information(user)
  2032. end) =~ "Follower/Following counter update for #{user.ap_id} failed"
  2033. end
  2034. test "it just returns the input if the user has no following/follower addresses", %{
  2035. user: user
  2036. } do
  2037. user =
  2038. user
  2039. |> Map.put(:following_address, nil)
  2040. |> Map.put(:follower_address, nil)
  2041. refute capture_log(fn ->
  2042. assert ^user = ActivityPub.maybe_update_follow_information(user)
  2043. end) =~ "Follower/Following counter update for #{user.ap_id} failed"
  2044. end
  2045. end
  2046. describe "global activity expiration" do
  2047. test "creates an activity expiration for local Create activities" do
  2048. clear_config([:mrf, :policies], Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy)
  2049. {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
  2050. {:ok, follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"})
  2051. assert_enqueued(
  2052. worker: Pleroma.Workers.PurgeExpiredActivity,
  2053. args: %{activity_id: activity.id},
  2054. scheduled_at:
  2055. activity.inserted_at
  2056. |> DateTime.from_naive!("Etc/UTC")
  2057. |> Timex.shift(days: 365)
  2058. )
  2059. refute_enqueued(
  2060. worker: Pleroma.Workers.PurgeExpiredActivity,
  2061. args: %{activity_id: follow.id}
  2062. )
  2063. end
  2064. end
  2065. describe "handling of clashing nicknames" do
  2066. test "renames an existing user with a clashing nickname and a different ap id" do
  2067. orig_user =
  2068. insert(
  2069. :user,
  2070. local: false,
  2071. nickname: "admin@mastodon.example.org",
  2072. ap_id: "http://mastodon.example.org/users/harinezumigari"
  2073. )
  2074. %{
  2075. nickname: orig_user.nickname,
  2076. ap_id: orig_user.ap_id <> "part_2"
  2077. }
  2078. |> ActivityPub.maybe_handle_clashing_nickname()
  2079. user = User.get_by_id(orig_user.id)
  2080. assert user.nickname == "#{orig_user.id}.admin@mastodon.example.org"
  2081. end
  2082. test "does nothing with a clashing nickname and the same ap id" do
  2083. orig_user =
  2084. insert(
  2085. :user,
  2086. local: false,
  2087. nickname: "admin@mastodon.example.org",
  2088. ap_id: "http://mastodon.example.org/users/harinezumigari"
  2089. )
  2090. %{
  2091. nickname: orig_user.nickname,
  2092. ap_id: orig_user.ap_id
  2093. }
  2094. |> ActivityPub.maybe_handle_clashing_nickname()
  2095. user = User.get_by_id(orig_user.id)
  2096. assert user.nickname == orig_user.nickname
  2097. end
  2098. end
  2099. describe "reply filtering" do
  2100. test "`following` still contains announcements by friends" do
  2101. user = insert(:user)
  2102. followed = insert(:user)
  2103. not_followed = insert(:user)
  2104. User.follow(user, followed)
  2105. {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"})
  2106. {:ok, not_followed_to_followed} =
  2107. CommonAPI.post(not_followed, %{
  2108. status: "Also hello",
  2109. in_reply_to_status_id: followed_post.id
  2110. })
  2111. {:ok, retoot} = CommonAPI.repeat(not_followed_to_followed.id, followed)
  2112. params =
  2113. %{}
  2114. |> Map.put(:type, ["Create", "Announce"])
  2115. |> Map.put(:blocking_user, user)
  2116. |> Map.put(:muting_user, user)
  2117. |> Map.put(:reply_filtering_user, user)
  2118. |> Map.put(:reply_visibility, "following")
  2119. |> Map.put(:announce_filtering_user, user)
  2120. |> Map.put(:user, user)
  2121. activities =
  2122. [user.ap_id | User.following(user)]
  2123. |> ActivityPub.fetch_activities(params)
  2124. followed_post_id = followed_post.id
  2125. retoot_id = retoot.id
  2126. assert [%{id: ^followed_post_id}, %{id: ^retoot_id}] = activities
  2127. assert length(activities) == 2
  2128. end
  2129. # This test is skipped because, while this is the desired behavior,
  2130. # there seems to be no good way to achieve it with the method that
  2131. # we currently use for detecting to who a reply is directed.
  2132. # This is a TODO and should be fixed by a later rewrite of the code
  2133. # in question.
  2134. @tag skip: true
  2135. test "`following` still contains self-replies by friends" do
  2136. user = insert(:user)
  2137. followed = insert(:user)
  2138. not_followed = insert(:user)
  2139. User.follow(user, followed)
  2140. {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"})
  2141. {:ok, not_followed_post} = CommonAPI.post(not_followed, %{status: "Also hello"})
  2142. {:ok, _followed_to_not_followed} =
  2143. CommonAPI.post(followed, %{status: "sup", in_reply_to_status_id: not_followed_post.id})
  2144. {:ok, _followed_self_reply} =
  2145. CommonAPI.post(followed, %{status: "Also cofe", in_reply_to_status_id: followed_post.id})
  2146. params =
  2147. %{}
  2148. |> Map.put(:type, ["Create", "Announce"])
  2149. |> Map.put(:blocking_user, user)
  2150. |> Map.put(:muting_user, user)
  2151. |> Map.put(:reply_filtering_user, user)
  2152. |> Map.put(:reply_visibility, "following")
  2153. |> Map.put(:announce_filtering_user, user)
  2154. |> Map.put(:user, user)
  2155. activities =
  2156. [user.ap_id | User.following(user)]
  2157. |> ActivityPub.fetch_activities(params)
  2158. assert length(activities) == 2
  2159. end
  2160. end
  2161. test "allow fetching of accounts with an empty string name field" do
  2162. Tesla.Mock.mock(fn
  2163. %{method: :get, url: "https://princess.cat/users/mewmew"} ->
  2164. file = File.read!("test/fixtures/mewmew_no_name.json")
  2165. %Tesla.Env{status: 200, body: file, headers: HttpRequestMock.activitypub_object_headers()}
  2166. end)
  2167. {:ok, user} = ActivityPub.make_user_from_ap_id("https://princess.cat/users/mewmew")
  2168. assert user.name == " "
  2169. end
  2170. test "pin_data_from_featured_collection will ignore unsupported values" do
  2171. assert %{} ==
  2172. ActivityPub.pin_data_from_featured_collection(%{
  2173. "type" => "OrderedCollection",
  2174. "first" => "https://social.example/users/alice/collections/featured?page=true"
  2175. })
  2176. end
  2177. end