1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
6 use Pleroma.Web.ConnCase, async: false
8 alias Pleroma.Notification
11 alias Pleroma.Web.CommonAPI
13 import Pleroma.Factory
15 test "does NOT render account/pleroma/relationship by default" do
16 %{user: user, conn: conn} = oauth_access(["read:notifications"])
17 other_user = insert(:user)
19 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
20 {:ok, [_notification]} = Notification.create_notifications(activity)
24 |> assign(:user, user)
25 |> get("/api/v1/notifications")
26 |> json_response_and_validate_schema(200)
28 assert Enum.all?(response, fn n ->
29 get_in(n, ["account", "pleroma", "relationship"]) == %{}
33 test "list of notifications" do
34 %{user: user, conn: conn} = oauth_access(["read:notifications"])
35 other_user = insert(:user)
37 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
39 {:ok, [_notification]} = Notification.create_notifications(activity)
43 |> assign(:user, user)
44 |> get("/api/v1/notifications")
47 "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{user.ap_id}\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
49 assert [%{"status" => %{"content" => response}} | _rest] =
50 json_response_and_validate_schema(conn, 200)
52 assert response == expected_response
55 test "by default, does not contain pleroma:chat_mention" do
56 %{user: user, conn: conn} = oauth_access(["read:notifications"])
57 other_user = insert(:user)
59 {:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey")
63 |> get("/api/v1/notifications")
64 |> json_response_and_validate_schema(200)
70 |> get("/api/v1/notifications?include_types[]=pleroma:chat_mention")
71 |> json_response_and_validate_schema(200)
76 test "by default, does not contain pleroma:report" do
77 clear_config([:instance, :moderator_privileges], [:reports_manage_reports])
80 other_user = insert(:user)
81 third_user = insert(:user)
83 {:ok, user} = user |> User.admin_api_update(%{is_moderator: true})
85 %{conn: conn} = oauth_access(["read:notifications"], user: user)
87 {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})
90 CommonAPI.report(third_user, %{account_id: other_user.id, status_ids: [activity.id]})
94 |> get("/api/v1/notifications")
95 |> json_response_and_validate_schema(200)
101 |> get("/api/v1/notifications?include_types[]=pleroma:report")
102 |> json_response_and_validate_schema(200)
107 test "Pleroma:report is hidden for non-privileged users" do
108 clear_config([:instance, :moderator_privileges], [:reports_manage_reports])
111 other_user = insert(:user)
112 third_user = insert(:user)
114 {:ok, user} = user |> User.admin_api_update(%{is_moderator: true})
116 %{conn: conn} = oauth_access(["read:notifications"], user: user)
118 {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})
121 CommonAPI.report(third_user, %{account_id: other_user.id, status_ids: [activity.id]})
125 |> get("/api/v1/notifications?include_types[]=pleroma:report")
126 |> json_response_and_validate_schema(200)
130 clear_config([:instance, :moderator_privileges], [])
134 |> get("/api/v1/notifications?include_types[]=pleroma:report")
135 |> json_response_and_validate_schema(200)
140 test "excludes mentions from blockers when blockers_visible is false" do
141 clear_config([:activitypub, :blockers_visible], false)
143 %{user: user, conn: conn} = oauth_access(["read:notifications"])
144 blocker = insert(:user)
146 {:ok, _} = CommonAPI.block(blocker, user)
147 {:ok, activity} = CommonAPI.post(blocker, %{status: "hi @#{user.nickname}"})
149 {:ok, [_notification]} = Notification.create_notifications(activity)
153 |> assign(:user, user)
154 |> get("/api/v1/notifications")
156 assert [] == json_response_and_validate_schema(conn, 200)
159 test "getting a single notification" do
160 %{user: user, conn: conn} = oauth_access(["read:notifications"])
161 other_user = insert(:user)
163 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
165 {:ok, [notification]} = Notification.create_notifications(activity)
167 conn = get(conn, "/api/v1/notifications/#{notification.id}")
170 "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{user.ap_id}\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
172 assert %{"status" => %{"content" => response}} = json_response_and_validate_schema(conn, 200)
173 assert response == expected_response
176 test "dismissing a single notification (deprecated endpoint)" do
177 %{user: user, conn: conn} = oauth_access(["write:notifications"])
178 other_user = insert(:user)
180 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
182 {:ok, [notification]} = Notification.create_notifications(activity)
186 |> assign(:user, user)
187 |> put_req_header("content-type", "application/json")
188 |> post("/api/v1/notifications/dismiss", %{"id" => to_string(notification.id)})
190 assert %{} = json_response_and_validate_schema(conn, 200)
193 test "dismissing a single notification" do
194 %{user: user, conn: conn} = oauth_access(["write:notifications"])
195 other_user = insert(:user)
197 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
199 {:ok, [notification]} = Notification.create_notifications(activity)
203 |> assign(:user, user)
204 |> post("/api/v1/notifications/#{notification.id}/dismiss")
206 assert %{} = json_response_and_validate_schema(conn, 200)
209 test "clearing all notifications" do
210 %{user: user, conn: conn} = oauth_access(["write:notifications", "read:notifications"])
211 other_user = insert(:user)
213 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
215 {:ok, [_notification]} = Notification.create_notifications(activity)
217 ret_conn = post(conn, "/api/v1/notifications/clear")
219 assert %{} = json_response_and_validate_schema(ret_conn, 200)
221 ret_conn = get(conn, "/api/v1/notifications")
223 assert all = json_response_and_validate_schema(ret_conn, 200)
227 test "paginates notifications using min_id, since_id, max_id, and limit" do
228 %{user: user, conn: conn} = oauth_access(["read:notifications"])
229 other_user = insert(:user)
231 {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
232 {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
233 {:ok, activity3} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
234 {:ok, activity4} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
236 notification1_id = get_notification_id_by_activity(activity1)
237 notification2_id = get_notification_id_by_activity(activity2)
238 notification3_id = get_notification_id_by_activity(activity3)
239 notification4_id = get_notification_id_by_activity(activity4)
241 conn = assign(conn, :user, user)
246 |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}")
247 |> json_response_and_validate_schema(:ok)
249 assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
254 |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}")
255 |> json_response_and_validate_schema(:ok)
257 assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
262 |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}")
263 |> json_response_and_validate_schema(:ok)
265 assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
268 describe "exclude_visibilities" do
269 test "filters notifications for mentions" do
270 %{user: user, conn: conn} = oauth_access(["read:notifications"])
271 other_user = insert(:user)
273 {:ok, public_activity} =
274 CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "public"})
276 {:ok, direct_activity} =
277 CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"})
279 {:ok, unlisted_activity} =
280 CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "unlisted"})
282 {:ok, private_activity} =
283 CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "private"})
285 query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "private"]})
286 conn_res = get(conn, "/api/v1/notifications?" <> query)
288 assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
289 assert id == direct_activity.id
291 query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "direct"]})
292 conn_res = get(conn, "/api/v1/notifications?" <> query)
294 assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
295 assert id == private_activity.id
297 query = params_to_query(%{exclude_visibilities: ["public", "private", "direct"]})
298 conn_res = get(conn, "/api/v1/notifications?" <> query)
300 assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
301 assert id == unlisted_activity.id
303 query = params_to_query(%{exclude_visibilities: ["unlisted", "private", "direct"]})
304 conn_res = get(conn, "/api/v1/notifications?" <> query)
306 assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
307 assert id == public_activity.id
310 test "filters notifications for Like activities" do
312 %{user: other_user, conn: conn} = oauth_access(["read:notifications"])
314 {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"})
316 {:ok, direct_activity} =
317 CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"})
319 {:ok, unlisted_activity} =
320 CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"})
322 {:ok, private_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "private"})
324 {:ok, _} = CommonAPI.favorite(user, public_activity.id)
325 {:ok, _} = CommonAPI.favorite(user, direct_activity.id)
326 {:ok, _} = CommonAPI.favorite(user, unlisted_activity.id)
327 {:ok, _} = CommonAPI.favorite(user, private_activity.id)
331 |> get("/api/v1/notifications?exclude_visibilities[]=direct")
332 |> json_response_and_validate_schema(200)
333 |> Enum.map(& &1["status"]["id"])
335 assert public_activity.id in activity_ids
336 assert unlisted_activity.id in activity_ids
337 assert private_activity.id in activity_ids
338 refute direct_activity.id in activity_ids
342 |> get("/api/v1/notifications?exclude_visibilities[]=unlisted")
343 |> json_response_and_validate_schema(200)
344 |> Enum.map(& &1["status"]["id"])
346 assert public_activity.id in activity_ids
347 refute unlisted_activity.id in activity_ids
348 assert private_activity.id in activity_ids
349 assert direct_activity.id in activity_ids
353 |> get("/api/v1/notifications?exclude_visibilities[]=private")
354 |> json_response_and_validate_schema(200)
355 |> Enum.map(& &1["status"]["id"])
357 assert public_activity.id in activity_ids
358 assert unlisted_activity.id in activity_ids
359 refute private_activity.id in activity_ids
360 assert direct_activity.id in activity_ids
364 |> get("/api/v1/notifications?exclude_visibilities[]=public")
365 |> json_response_and_validate_schema(200)
366 |> Enum.map(& &1["status"]["id"])
368 refute public_activity.id in activity_ids
369 assert unlisted_activity.id in activity_ids
370 assert private_activity.id in activity_ids
371 assert direct_activity.id in activity_ids
374 test "filters notifications for Announce activities" do
376 %{user: other_user, conn: conn} = oauth_access(["read:notifications"])
378 {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"})
380 {:ok, unlisted_activity} =
381 CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"})
383 {:ok, _} = CommonAPI.repeat(public_activity.id, user)
384 {:ok, _} = CommonAPI.repeat(unlisted_activity.id, user)
388 |> get("/api/v1/notifications?exclude_visibilities[]=unlisted")
389 |> json_response_and_validate_schema(200)
390 |> Enum.map(& &1["status"]["id"])
392 assert public_activity.id in activity_ids
393 refute unlisted_activity.id in activity_ids
396 test "doesn't return less than the requested amount of records when the user's reply is liked" do
398 %{user: other_user, conn: conn} = oauth_access(["read:notifications"])
401 CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"})
403 {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"})
406 CommonAPI.post(other_user, %{
408 visibility: "public",
409 in_reply_to_status_id: activity.id
412 {:ok, _favorite} = CommonAPI.favorite(user, reply.id)
416 |> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2")
417 |> json_response_and_validate_schema(200)
418 |> Enum.map(& &1["status"]["id"])
420 assert [reply.id, mention.id] == activity_ids
424 test "filters notifications using exclude_types" do
425 %{user: user, conn: conn} = oauth_access(["read:notifications"])
426 other_user = insert(:user)
428 {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"})
429 {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"})
430 {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id)
431 {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user)
432 {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
434 mention_notification_id = get_notification_id_by_activity(mention_activity)
435 favorite_notification_id = get_notification_id_by_activity(favorite_activity)
436 reblog_notification_id = get_notification_id_by_activity(reblog_activity)
437 follow_notification_id = get_notification_id_by_activity(follow_activity)
439 query = params_to_query(%{exclude_types: ["mention", "favourite", "reblog"]})
440 conn_res = get(conn, "/api/v1/notifications?" <> query)
442 assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
444 query = params_to_query(%{exclude_types: ["favourite", "reblog", "follow"]})
445 conn_res = get(conn, "/api/v1/notifications?" <> query)
447 assert [%{"id" => ^mention_notification_id}] =
448 json_response_and_validate_schema(conn_res, 200)
450 query = params_to_query(%{exclude_types: ["reblog", "follow", "mention"]})
451 conn_res = get(conn, "/api/v1/notifications?" <> query)
453 assert [%{"id" => ^favorite_notification_id}] =
454 json_response_and_validate_schema(conn_res, 200)
456 query = params_to_query(%{exclude_types: ["follow", "mention", "favourite"]})
457 conn_res = get(conn, "/api/v1/notifications?" <> query)
459 assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200)
462 test "filters notifications using types" do
463 %{user: user, conn: conn} = oauth_access(["read:notifications"])
464 other_user = insert(:user)
466 {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"})
467 {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"})
468 {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id)
469 {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user)
470 {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
472 mention_notification_id = get_notification_id_by_activity(mention_activity)
473 favorite_notification_id = get_notification_id_by_activity(favorite_activity)
474 reblog_notification_id = get_notification_id_by_activity(reblog_activity)
475 follow_notification_id = get_notification_id_by_activity(follow_activity)
477 conn_res = get(conn, "/api/v1/notifications?types[]=follow")
479 assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
481 conn_res = get(conn, "/api/v1/notifications?types[]=mention")
483 assert [%{"id" => ^mention_notification_id}] =
484 json_response_and_validate_schema(conn_res, 200)
486 conn_res = get(conn, "/api/v1/notifications?types[]=favourite")
488 assert [%{"id" => ^favorite_notification_id}] =
489 json_response_and_validate_schema(conn_res, 200)
491 conn_res = get(conn, "/api/v1/notifications?types[]=reblog")
493 assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200)
495 result = conn |> get("/api/v1/notifications") |> json_response_and_validate_schema(200)
497 assert length(result) == 4
499 query = params_to_query(%{types: ["follow", "mention", "favourite", "reblog"]})
503 |> get("/api/v1/notifications?" <> query)
504 |> json_response_and_validate_schema(200)
506 assert length(result) == 4
509 test "filtering falls back to include_types" do
510 %{user: user, conn: conn} = oauth_access(["read:notifications"])
511 other_user = insert(:user)
513 {:ok, _activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"})
514 {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"})
515 {:ok, _activity} = CommonAPI.favorite(other_user, create_activity.id)
516 {:ok, _activity} = CommonAPI.repeat(create_activity.id, other_user)
517 {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
519 follow_notification_id = get_notification_id_by_activity(follow_activity)
521 conn_res = get(conn, "/api/v1/notifications?include_types[]=follow")
523 assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
526 test "destroy multiple" do
527 %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"])
528 other_user = insert(:user)
530 {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
531 {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
532 {:ok, activity3} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"})
533 {:ok, activity4} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"})
535 notification1_id = get_notification_id_by_activity(activity1)
536 notification2_id = get_notification_id_by_activity(activity2)
537 notification3_id = get_notification_id_by_activity(activity3)
538 notification4_id = get_notification_id_by_activity(activity4)
542 |> get("/api/v1/notifications")
543 |> json_response_and_validate_schema(:ok)
545 assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result
549 |> assign(:user, other_user)
550 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:notifications"]))
554 |> get("/api/v1/notifications")
555 |> json_response_and_validate_schema(:ok)
557 assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
559 query = params_to_query(%{ids: [notification1_id, notification2_id]})
560 conn_destroy = delete(conn, "/api/v1/notifications/destroy_multiple?" <> query)
562 assert json_response_and_validate_schema(conn_destroy, 200) == %{}
566 |> get("/api/v1/notifications")
567 |> json_response_and_validate_schema(:ok)
569 assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
572 test "doesn't see notifications after muting user with notifications" do
573 %{user: user, conn: conn} = oauth_access(["read:notifications"])
574 user2 = insert(:user)
576 {:ok, _, _, _} = CommonAPI.follow(user, user2)
577 {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"})
579 ret_conn = get(conn, "/api/v1/notifications")
581 assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
583 {:ok, _user_relationships} = User.mute(user, user2)
585 conn = get(conn, "/api/v1/notifications")
587 assert json_response_and_validate_schema(conn, 200) == []
590 test "see notifications after muting user without notifications" do
591 %{user: user, conn: conn} = oauth_access(["read:notifications"])
592 user2 = insert(:user)
594 {:ok, _, _, _} = CommonAPI.follow(user, user2)
595 {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"})
597 ret_conn = get(conn, "/api/v1/notifications")
599 assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
601 {:ok, _user_relationships} = User.mute(user, user2, %{notifications: false})
603 conn = get(conn, "/api/v1/notifications")
605 assert length(json_response_and_validate_schema(conn, 200)) == 1
608 test "see notifications after muting user with notifications and with_muted parameter" do
609 %{user: user, conn: conn} = oauth_access(["read:notifications"])
610 user2 = insert(:user)
612 {:ok, _, _, _} = CommonAPI.follow(user, user2)
613 {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"})
615 ret_conn = get(conn, "/api/v1/notifications")
617 assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
619 {:ok, _user_relationships} = User.mute(user, user2)
621 conn = get(conn, "/api/v1/notifications?with_muted=true")
623 assert length(json_response_and_validate_schema(conn, 200)) == 1
626 test "see move notifications" do
627 old_user = insert(:user)
628 new_user = insert(:user, also_known_as: [old_user.ap_id])
629 %{user: follower, conn: conn} = oauth_access(["read:notifications"])
631 User.follow(follower, old_user)
632 Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
633 Pleroma.Tests.ObanHelpers.perform_all()
635 conn = get(conn, "/api/v1/notifications")
637 assert length(json_response_and_validate_schema(conn, 200)) == 1
640 describe "link headers" do
641 test "preserves parameters in link headers" do
642 %{user: user, conn: conn} = oauth_access(["read:notifications"])
643 other_user = insert(:user)
646 CommonAPI.post(other_user, %{
647 status: "hi @#{user.nickname}",
652 CommonAPI.post(other_user, %{
653 status: "hi @#{user.nickname}",
657 notification1 = Repo.get_by(Notification, activity_id: activity1.id)
658 notification2 = Repo.get_by(Notification, activity_id: activity2.id)
662 |> assign(:user, user)
663 |> get("/api/v1/notifications?limit=5")
665 assert [link_header] = get_resp_header(conn, "link")
666 assert link_header =~ ~r/limit=5/
667 assert link_header =~ ~r/min_id=#{notification2.id}/
668 assert link_header =~ ~r/max_id=#{notification1.id}/
672 describe "from specified user" do
674 %{user: user, conn: conn} = oauth_access(["read:notifications"])
676 %{id: account_id} = other_user1 = insert(:user)
677 other_user2 = insert(:user)
679 {:ok, _activity} = CommonAPI.post(other_user1, %{status: "hi @#{user.nickname}"})
680 {:ok, _activity} = CommonAPI.post(other_user2, %{status: "bye @#{user.nickname}"})
682 assert [%{"account" => %{"id" => ^account_id}}] =
684 |> assign(:user, user)
685 |> get("/api/v1/notifications?account_id=#{account_id}")
686 |> json_response_and_validate_schema(200)
688 assert %{"error" => "Account is not found"} =
690 |> assign(:user, user)
691 |> get("/api/v1/notifications?account_id=cofe")
692 |> json_response_and_validate_schema(404)
696 defp get_notification_id_by_activity(%{id: id}) do
698 |> Repo.get_by(activity_id: id)
703 defp params_to_query(%{} = params) do
704 Enum.map_join(params, "&", fn
705 {k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}")
706 {k, v} -> k <> "=" <> v