1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Conversation.Participation
13 alias Pleroma.Formatter
15 alias Pleroma.Notification
17 alias Pleroma.Object.Fetcher
18 alias Pleroma.Pagination
20 alias Pleroma.ScheduledActivity
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Visibility
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.MastodonAPI.AccountView
28 alias Pleroma.Web.MastodonAPI.AppView
29 alias Pleroma.Web.MastodonAPI.ConversationView
30 alias Pleroma.Web.MastodonAPI.FilterView
31 alias Pleroma.Web.MastodonAPI.ListView
32 alias Pleroma.Web.MastodonAPI.MastodonAPI
33 alias Pleroma.Web.MastodonAPI.MastodonView
34 alias Pleroma.Web.MastodonAPI.NotificationView
35 alias Pleroma.Web.MastodonAPI.ReportView
36 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
37 alias Pleroma.Web.MastodonAPI.StatusView
38 alias Pleroma.Web.MediaProxy
39 alias Pleroma.Web.OAuth.App
40 alias Pleroma.Web.OAuth.Authorization
41 alias Pleroma.Web.OAuth.Scopes
42 alias Pleroma.Web.OAuth.Token
43 alias Pleroma.Web.TwitterAPI.TwitterAPI
45 alias Pleroma.Web.ControllerHelper
51 Pleroma.Plugs.RateLimitPlug,
53 max_requests: Config.get([:app_account_creation, :max_requests]),
54 interval: Config.get([:app_account_creation, :interval])
56 when action in [:account_register]
59 @local_mastodon_name "Mastodon-Local"
61 action_fallback(:errors)
63 def create_app(conn, params) do
64 scopes = Scopes.fetch_scopes(params, ["read"])
68 |> Map.drop(["scope", "scopes"])
69 |> Map.put("scopes", scopes)
71 with cs <- App.register_changeset(%App{}, app_attrs),
72 false <- cs.changes[:client_name] == @local_mastodon_name,
73 {:ok, app} <- Repo.insert(cs) do
76 |> render("show.json", %{app: app})
85 value_function \\ fn x -> {:ok, x} end
87 if Map.has_key?(params, params_field) do
88 case value_function.(params[params_field]) do
89 {:ok, new_value} -> Map.put(map, map_field, new_value)
97 def update_credentials(%{assigns: %{user: user}} = conn, params) do
102 |> add_if_present(params, "display_name", :name)
103 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
104 |> add_if_present(params, "avatar", :avatar, fn value ->
105 with %Plug.Upload{} <- value,
106 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
113 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
116 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
120 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
121 |> Enum.reduce(%{}, fn key, acc ->
122 add_if_present(acc, params, to_string(key), key, fn value ->
123 {:ok, ControllerHelper.truthy_param?(value)}
126 |> add_if_present(params, "default_scope", :default_scope)
127 |> add_if_present(params, "header", :banner, fn value ->
128 with %Plug.Upload{} <- value,
129 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
135 |> Map.put(:emoji, user_info_emojis)
137 info_cng = User.Info.profile_update(user.info, info_params)
139 with changeset <- User.update_changeset(user, user_params),
140 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
141 {:ok, user} <- User.update_and_set_cache(changeset) do
142 if original_user != user do
143 CommonAPI.update(user)
146 json(conn, AccountView.render("account.json", %{user: user, for: user}))
151 |> json(%{error: "Invalid request"})
155 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
156 account = AccountView.render("account.json", %{user: user, for: user})
160 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
161 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
164 |> render("short.json", %{app: app})
168 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
169 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
170 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
171 account = AccountView.render("account.json", %{user: user, for: for_user})
177 |> json(%{error: "Can't find user"})
181 @mastodon_api_level "2.7.2"
183 def masto_instance(conn, _params) do
184 instance = Config.get(:instance)
188 title: Keyword.get(instance, :name),
189 description: Keyword.get(instance, :description),
190 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
191 email: Keyword.get(instance, :email),
193 streaming_api: Pleroma.Web.Endpoint.websocket_url()
195 stats: Stats.get_stats(),
196 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
198 registrations: Pleroma.Config.get([:instance, :registrations_open]),
199 # Extra (not present in Mastodon):
200 max_toot_chars: Keyword.get(instance, :limit),
201 poll_limits: Keyword.get(instance, :poll_limits)
207 def peers(conn, _params) do
208 json(conn, Stats.get_peers())
211 defp mastodonized_emoji do
212 Pleroma.Emoji.get_all()
213 |> Enum.map(fn {shortcode, relative_url, tags} ->
214 url = to_string(URI.merge(Web.base_url(), relative_url))
217 "shortcode" => shortcode,
219 "visible_in_picker" => true,
226 def custom_emojis(conn, _params) do
227 mastodon_emoji = mastodonized_emoji()
228 json(conn, mastodon_emoji)
231 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
234 |> Map.drop(["since_id", "max_id", "min_id"])
237 last = List.last(activities)
244 |> Map.get("limit", "20")
245 |> String.to_integer()
248 if length(activities) <= limit do
254 |> Enum.at(limit * -1)
258 {next_url, prev_url} =
262 Pleroma.Web.Endpoint,
265 Map.merge(params, %{max_id: max_id})
268 Pleroma.Web.Endpoint,
271 Map.merge(params, %{min_id: min_id})
277 Pleroma.Web.Endpoint,
279 Map.merge(params, %{max_id: max_id})
282 Pleroma.Web.Endpoint,
284 Map.merge(params, %{min_id: min_id})
290 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
296 def home_timeline(%{assigns: %{user: user}} = conn, params) do
299 |> Map.put("type", ["Create", "Announce"])
300 |> Map.put("blocking_user", user)
301 |> Map.put("muting_user", user)
302 |> Map.put("user", user)
305 [user.ap_id | user.following]
306 |> ActivityPub.fetch_activities(params)
310 |> add_link_headers(:home_timeline, activities)
311 |> put_view(StatusView)
312 |> render("index.json", %{activities: activities, for: user, as: :activity})
315 def public_timeline(%{assigns: %{user: user}} = conn, params) do
316 local_only = params["local"] in [true, "True", "true", "1"]
320 |> Map.put("type", ["Create", "Announce"])
321 |> Map.put("local_only", local_only)
322 |> Map.put("blocking_user", user)
323 |> Map.put("muting_user", user)
324 |> ActivityPub.fetch_public_activities()
328 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
329 |> put_view(StatusView)
330 |> render("index.json", %{activities: activities, for: user, as: :activity})
333 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
334 with %User{} = user <- User.get_cached_by_id(params["id"]) do
335 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
338 |> add_link_headers(:user_statuses, activities, params["id"])
339 |> put_view(StatusView)
340 |> render("index.json", %{
341 activities: activities,
348 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
351 |> Map.put("type", "Create")
352 |> Map.put("blocking_user", user)
353 |> Map.put("user", user)
354 |> Map.put(:visibility, "direct")
358 |> ActivityPub.fetch_activities_query(params)
359 |> Pagination.fetch_paginated(params)
362 |> add_link_headers(:dm_timeline, activities)
363 |> put_view(StatusView)
364 |> render("index.json", %{activities: activities, for: user, as: :activity})
367 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
368 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
369 true <- Visibility.visible_for_user?(activity, user) do
371 |> put_view(StatusView)
372 |> try_render("status.json", %{activity: activity, for: user})
376 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
377 with %Activity{} = activity <- Activity.get_by_id(id),
379 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
380 "blocking_user" => user,
384 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
386 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
387 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
393 activities: grouped_activities[true] || [],
397 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
402 activities: grouped_activities[false] || [],
406 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
413 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
414 with %Object{} = object <- Object.get_by_id(id),
415 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
416 true <- Visibility.visible_for_user?(activity, user) do
418 |> put_view(StatusView)
419 |> try_render("poll.json", %{object: object, for: user})
424 |> json(%{error: "Record not found"})
429 |> json(%{error: "Record not found"})
433 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
434 with %Object{} = object <- Object.get_by_id(id),
435 true <- object.data["type"] == "Question",
436 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
437 true <- Visibility.visible_for_user?(activity, user),
438 {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do
440 |> put_view(StatusView)
441 |> try_render("poll.json", %{object: object, for: user})
446 |> json(%{error: "Record not found"})
451 |> json(%{error: "Record not found"})
456 |> json(%{error: message})
460 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
461 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
463 |> add_link_headers(:scheduled_statuses, scheduled_activities)
464 |> put_view(ScheduledActivityView)
465 |> render("index.json", %{scheduled_activities: scheduled_activities})
469 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
470 with %ScheduledActivity{} = scheduled_activity <-
471 ScheduledActivity.get(user, scheduled_activity_id) do
473 |> put_view(ScheduledActivityView)
474 |> render("show.json", %{scheduled_activity: scheduled_activity})
476 _ -> {:error, :not_found}
480 def update_scheduled_status(
481 %{assigns: %{user: user}} = conn,
482 %{"id" => scheduled_activity_id} = params
484 with %ScheduledActivity{} = scheduled_activity <-
485 ScheduledActivity.get(user, scheduled_activity_id),
486 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
488 |> put_view(ScheduledActivityView)
489 |> render("show.json", %{scheduled_activity: scheduled_activity})
491 nil -> {:error, :not_found}
496 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
497 with %ScheduledActivity{} = scheduled_activity <-
498 ScheduledActivity.get(user, scheduled_activity_id),
499 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
501 |> put_view(ScheduledActivityView)
502 |> render("show.json", %{scheduled_activity: scheduled_activity})
504 nil -> {:error, :not_found}
509 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
510 when length(media_ids) > 0 do
513 |> Map.put("status", ".")
515 post_status(conn, params)
518 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
521 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
523 scheduled_at = params["scheduled_at"]
525 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
526 with {:ok, scheduled_activity} <-
527 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
529 |> put_view(ScheduledActivityView)
530 |> render("show.json", %{scheduled_activity: scheduled_activity})
533 params = Map.drop(params, ["scheduled_at"])
535 case get_cached_status_or_post(conn, params) do
536 {:ignore, message} ->
539 |> json(%{error: message})
544 |> json(%{error: message})
548 |> put_view(StatusView)
549 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
554 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
556 case get_req_header(conn, "idempotency-key") do
558 _ -> Ecto.UUID.generate()
561 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
562 case CommonAPI.post(user, params) do
563 {:ok, activity} -> activity
564 {:error, message} -> {:ignore, message}
569 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
570 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
576 |> json(%{error: "Can't delete this post"})
580 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
581 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
582 %Activity{} = announce <- Activity.normalize(announce.data) do
584 |> put_view(StatusView)
585 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
589 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
590 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
591 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
593 |> put_view(StatusView)
594 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
598 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
599 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
600 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
602 |> put_view(StatusView)
603 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
607 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
608 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
609 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
611 |> put_view(StatusView)
612 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
616 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
617 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
619 |> put_view(StatusView)
620 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
624 |> put_resp_content_type("application/json")
625 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
629 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
630 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
632 |> put_view(StatusView)
633 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
637 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
638 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
639 %User{} = user <- User.get_cached_by_nickname(user.nickname),
640 true <- Visibility.visible_for_user?(activity, user),
641 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
643 |> put_view(StatusView)
644 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
648 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
649 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
650 %User{} = user <- User.get_cached_by_nickname(user.nickname),
651 true <- Visibility.visible_for_user?(activity, user),
652 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
654 |> put_view(StatusView)
655 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
659 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
660 activity = Activity.get_by_id(id)
662 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
664 |> put_view(StatusView)
665 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
669 |> put_resp_content_type("application/json")
670 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
674 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
675 activity = Activity.get_by_id(id)
677 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
679 |> put_view(StatusView)
680 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
684 def notifications(%{assigns: %{user: user}} = conn, params) do
685 notifications = MastodonAPI.get_notifications(user, params)
688 |> add_link_headers(:notifications, notifications)
689 |> put_view(NotificationView)
690 |> render("index.json", %{notifications: notifications, for: user})
693 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
694 with {:ok, notification} <- Notification.get(user, id) do
696 |> put_view(NotificationView)
697 |> render("show.json", %{notification: notification, for: user})
701 |> put_resp_content_type("application/json")
702 |> send_resp(403, Jason.encode!(%{"error" => reason}))
706 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
707 Notification.clear(user)
711 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
712 with {:ok, _notif} <- Notification.dismiss(user, id) do
717 |> put_resp_content_type("application/json")
718 |> send_resp(403, Jason.encode!(%{"error" => reason}))
722 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
723 Notification.destroy_multiple(user, ids)
727 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
729 q = from(u in User, where: u.id in ^id)
730 targets = Repo.all(q)
733 |> put_view(AccountView)
734 |> render("relationships.json", %{user: user, targets: targets})
737 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
738 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
740 def update_media(%{assigns: %{user: user}} = conn, data) do
741 with %Object{} = object <- Repo.get(Object, data["id"]),
742 true <- Object.authorize_mutation(object, user),
743 true <- is_binary(data["description"]),
744 description <- data["description"] do
745 new_data = %{object.data | "name" => description}
749 |> Object.change(%{data: new_data})
752 attachment_data = Map.put(new_data, "id", object.id)
755 |> put_view(StatusView)
756 |> render("attachment.json", %{attachment: attachment_data})
760 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
761 with {:ok, object} <-
764 actor: User.ap_id(user),
765 description: Map.get(data, "description")
767 attachment_data = Map.put(object.data, "id", object.id)
770 |> put_view(StatusView)
771 |> render("attachment.json", %{attachment: attachment_data})
775 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
776 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
777 %{} = attachment_data <- Map.put(object.data, "id", object.id),
778 %{type: type} = rendered <-
779 StatusView.render("attachment.json", %{attachment: attachment_data}) do
780 # Reject if not an image
781 if type == "image" do
783 # Save to the user's info
784 info_changeset = User.Info.mascot_update(user.info, rendered)
788 |> Ecto.Changeset.change()
789 |> Ecto.Changeset.put_embed(:info, info_changeset)
791 {:ok, _user} = User.update_and_set_cache(user_changeset)
797 |> put_resp_content_type("application/json")
798 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
803 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
804 mascot = User.get_mascot(user)
810 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
811 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
812 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
813 q = from(u in User, where: u.ap_id in ^likes)
817 |> put_view(AccountView)
818 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
824 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
825 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
826 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
827 q = from(u in User, where: u.ap_id in ^announces)
831 |> put_view(AccountView)
832 |> render("accounts.json", %{for: user, users: users, as: :user})
838 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
839 local_only = params["local"] in [true, "True", "true", "1"]
842 [params["tag"], params["any"]]
846 |> Enum.map(&String.downcase(&1))
851 |> Enum.map(&String.downcase(&1))
856 |> Enum.map(&String.downcase(&1))
860 |> Map.put("type", "Create")
861 |> Map.put("local_only", local_only)
862 |> Map.put("blocking_user", user)
863 |> Map.put("muting_user", user)
864 |> Map.put("tag", tags)
865 |> Map.put("tag_all", tag_all)
866 |> Map.put("tag_reject", tag_reject)
867 |> ActivityPub.fetch_public_activities()
871 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
872 |> put_view(StatusView)
873 |> render("index.json", %{activities: activities, for: user, as: :activity})
876 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
877 with %User{} = user <- User.get_cached_by_id(id),
878 followers <- MastodonAPI.get_followers(user, params) do
881 for_user && user.id == for_user.id -> followers
882 user.info.hide_followers -> []
887 |> add_link_headers(:followers, followers, user)
888 |> put_view(AccountView)
889 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
893 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
894 with %User{} = user <- User.get_cached_by_id(id),
895 followers <- MastodonAPI.get_friends(user, params) do
898 for_user && user.id == for_user.id -> followers
899 user.info.hide_follows -> []
904 |> add_link_headers(:following, followers, user)
905 |> put_view(AccountView)
906 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
910 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
911 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
913 |> put_view(AccountView)
914 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
918 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
919 with %User{} = follower <- User.get_cached_by_id(id),
920 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
922 |> put_view(AccountView)
923 |> render("relationship.json", %{user: followed, target: follower})
927 |> put_resp_content_type("application/json")
928 |> send_resp(403, Jason.encode!(%{"error" => message}))
932 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
933 with %User{} = follower <- User.get_cached_by_id(id),
934 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
936 |> put_view(AccountView)
937 |> render("relationship.json", %{user: followed, target: follower})
941 |> put_resp_content_type("application/json")
942 |> send_resp(403, Jason.encode!(%{"error" => message}))
946 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
947 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
948 {_, true} <- {:followed, follower.id != followed.id},
949 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
951 |> put_view(AccountView)
952 |> render("relationship.json", %{user: follower, target: followed})
959 |> put_resp_content_type("application/json")
960 |> send_resp(403, Jason.encode!(%{"error" => message}))
964 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
965 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
966 {_, true} <- {:followed, follower.id != followed.id},
967 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
969 |> put_view(AccountView)
970 |> render("account.json", %{user: followed, for: follower})
977 |> put_resp_content_type("application/json")
978 |> send_resp(403, Jason.encode!(%{"error" => message}))
982 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
983 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
984 {_, true} <- {:followed, follower.id != followed.id},
985 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
987 |> put_view(AccountView)
988 |> render("relationship.json", %{user: follower, target: followed})
998 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
999 with %User{} = muted <- User.get_cached_by_id(id),
1000 {:ok, muter} <- User.mute(muter, muted) do
1002 |> put_view(AccountView)
1003 |> render("relationship.json", %{user: muter, target: muted})
1005 {:error, message} ->
1007 |> put_resp_content_type("application/json")
1008 |> send_resp(403, Jason.encode!(%{"error" => message}))
1012 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1013 with %User{} = muted <- User.get_cached_by_id(id),
1014 {:ok, muter} <- User.unmute(muter, muted) do
1016 |> put_view(AccountView)
1017 |> render("relationship.json", %{user: muter, target: muted})
1019 {:error, message} ->
1021 |> put_resp_content_type("application/json")
1022 |> send_resp(403, Jason.encode!(%{"error" => message}))
1026 def mutes(%{assigns: %{user: user}} = conn, _) do
1027 with muted_accounts <- User.muted_users(user) do
1028 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1033 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1034 with %User{} = blocked <- User.get_cached_by_id(id),
1035 {:ok, blocker} <- User.block(blocker, blocked),
1036 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1038 |> put_view(AccountView)
1039 |> render("relationship.json", %{user: blocker, target: blocked})
1041 {:error, message} ->
1043 |> put_resp_content_type("application/json")
1044 |> send_resp(403, Jason.encode!(%{"error" => message}))
1048 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1049 with %User{} = blocked <- User.get_cached_by_id(id),
1050 {:ok, blocker} <- User.unblock(blocker, blocked),
1051 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1053 |> put_view(AccountView)
1054 |> render("relationship.json", %{user: blocker, target: blocked})
1056 {:error, message} ->
1058 |> put_resp_content_type("application/json")
1059 |> send_resp(403, Jason.encode!(%{"error" => message}))
1063 def blocks(%{assigns: %{user: user}} = conn, _) do
1064 with blocked_accounts <- User.blocked_users(user) do
1065 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1070 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1071 json(conn, info.domain_blocks || [])
1074 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1075 User.block_domain(blocker, domain)
1079 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1080 User.unblock_domain(blocker, domain)
1084 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1085 with %User{} = subscription_target <- User.get_cached_by_id(id),
1086 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1088 |> put_view(AccountView)
1089 |> render("relationship.json", %{user: user, target: subscription_target})
1091 {:error, message} ->
1093 |> put_resp_content_type("application/json")
1094 |> send_resp(403, Jason.encode!(%{"error" => message}))
1098 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1099 with %User{} = subscription_target <- User.get_cached_by_id(id),
1100 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1102 |> put_view(AccountView)
1103 |> render("relationship.json", %{user: user, target: subscription_target})
1105 {:error, message} ->
1107 |> put_resp_content_type("application/json")
1108 |> send_resp(403, Jason.encode!(%{"error" => message}))
1112 def status_search_query_with_gin(q, query) do
1116 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1120 order_by: [desc: :id]
1124 def status_search_query_with_rum(q, query) do
1128 "? @@ plainto_tsquery('english', ?)",
1132 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1136 def status_search(user, query) do
1138 if Regex.match?(~r/https?:/, query) do
1139 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1140 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1141 true <- Visibility.visible_for_user?(activity, user) do
1149 from([a, o] in Activity.with_preloaded_object(Activity),
1150 where: fragment("?->>'type' = 'Create'", a.data),
1151 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1156 if Pleroma.Config.get([:database, :rum_enabled]) do
1157 status_search_query_with_rum(q, query)
1159 status_search_query_with_gin(q, query)
1162 Repo.all(q) ++ fetched
1165 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1166 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1168 statuses = status_search(user, query)
1170 tags_path = Web.base_url() <> "/tag/"
1176 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1177 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1178 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1181 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1183 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1190 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1191 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1193 statuses = status_search(user, query)
1199 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1200 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1203 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1205 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1212 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1213 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1215 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1220 def favourites(%{assigns: %{user: user}} = conn, params) do
1223 |> Map.put("type", "Create")
1224 |> Map.put("favorited_by", user.ap_id)
1225 |> Map.put("blocking_user", user)
1228 ActivityPub.fetch_activities([], params)
1232 |> add_link_headers(:favourites, activities)
1233 |> put_view(StatusView)
1234 |> render("index.json", %{activities: activities, for: user, as: :activity})
1237 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1238 with %User{} = user <- User.get_by_id(id),
1239 false <- user.info.hide_favorites do
1242 |> Map.put("type", "Create")
1243 |> Map.put("favorited_by", user.ap_id)
1244 |> Map.put("blocking_user", for_user)
1248 ["https://www.w3.org/ns/activitystreams#Public"] ++
1249 [for_user.ap_id | for_user.following]
1251 ["https://www.w3.org/ns/activitystreams#Public"]
1256 |> ActivityPub.fetch_activities(params)
1260 |> add_link_headers(:favourites, activities)
1261 |> put_view(StatusView)
1262 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1265 {:error, :not_found}
1270 |> json(%{error: "Can't get favorites"})
1274 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1275 user = User.get_cached_by_id(user.id)
1278 Bookmark.for_user_query(user.id)
1279 |> Pagination.fetch_paginated(params)
1283 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1286 |> add_link_headers(:bookmarks, bookmarks)
1287 |> put_view(StatusView)
1288 |> render("index.json", %{activities: activities, for: user, as: :activity})
1291 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1292 lists = Pleroma.List.for_user(user, opts)
1293 res = ListView.render("lists.json", lists: lists)
1297 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1298 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1299 res = ListView.render("list.json", list: list)
1305 |> json(%{error: "Record not found"})
1309 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1310 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1311 res = ListView.render("lists.json", lists: lists)
1315 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1316 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1317 {:ok, _list} <- Pleroma.List.delete(list) do
1325 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1326 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1327 res = ListView.render("list.json", list: list)
1332 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1334 |> Enum.each(fn account_id ->
1335 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1336 %User{} = followed <- User.get_cached_by_id(account_id) do
1337 Pleroma.List.follow(list, followed)
1344 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1346 |> Enum.each(fn account_id ->
1347 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1348 %User{} = followed <- User.get_cached_by_id(account_id) do
1349 Pleroma.List.unfollow(list, followed)
1356 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1357 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1358 {:ok, users} = Pleroma.List.get_following(list) do
1360 |> put_view(AccountView)
1361 |> render("accounts.json", %{for: user, users: users, as: :user})
1365 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1366 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1367 {:ok, list} <- Pleroma.List.rename(list, title) do
1368 res = ListView.render("list.json", list: list)
1376 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1377 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1380 |> Map.put("type", "Create")
1381 |> Map.put("blocking_user", user)
1382 |> Map.put("muting_user", user)
1384 # we must filter the following list for the user to avoid leaking statuses the user
1385 # does not actually have permission to see (for more info, peruse security issue #270).
1388 |> Enum.filter(fn x -> x in user.following end)
1389 |> ActivityPub.fetch_activities_bounded(following, params)
1393 |> put_view(StatusView)
1394 |> render("index.json", %{activities: activities, for: user, as: :activity})
1399 |> json(%{error: "Error."})
1403 def index(%{assigns: %{user: user}} = conn, _params) do
1404 token = get_session(conn, :oauth_token)
1407 mastodon_emoji = mastodonized_emoji()
1409 limit = Config.get([:instance, :limit])
1412 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1414 flavour = get_user_flavour(user)
1419 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1420 access_token: token,
1422 domain: Pleroma.Web.Endpoint.host(),
1425 unfollow_modal: false,
1428 auto_play_gif: false,
1429 display_sensitive_media: false,
1430 reduce_motion: false,
1431 max_toot_chars: limit,
1432 mascot: User.get_mascot(user)["url"]
1434 poll_limits: Config.get([:instance, :poll_limits]),
1436 delete_others_notice: present?(user.info.is_moderator),
1437 admin: present?(user.info.is_admin)
1441 default_privacy: user.info.default_scope,
1442 default_sensitive: false,
1443 allow_content_types: Config.get([:instance, :allowed_post_formats])
1445 media_attachments: %{
1446 accept_content_types: [
1462 user.info.settings ||
1492 push_subscription: nil,
1494 custom_emojis: mastodon_emoji,
1500 |> put_layout(false)
1501 |> put_view(MastodonView)
1502 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1505 |> put_session(:return_to, conn.request_path)
1506 |> redirect(to: "/web/login")
1510 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1511 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1513 with changeset <- Ecto.Changeset.change(user),
1514 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1515 {:ok, _user} <- User.update_and_set_cache(changeset) do
1520 |> put_resp_content_type("application/json")
1521 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1525 @supported_flavours ["glitch", "vanilla"]
1527 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1528 when flavour in @supported_flavours do
1529 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1531 with changeset <- Ecto.Changeset.change(user),
1532 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1533 {:ok, user} <- User.update_and_set_cache(changeset),
1534 flavour <- user.info.flavour do
1539 |> put_resp_content_type("application/json")
1540 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1544 def set_flavour(conn, _params) do
1547 |> json(%{error: "Unsupported flavour"})
1550 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1551 json(conn, get_user_flavour(user))
1554 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1558 defp get_user_flavour(_) do
1562 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1563 redirect(conn, to: local_mastodon_root_path(conn))
1566 @doc "Local Mastodon FE login init action"
1567 def login(conn, %{"code" => auth_token}) do
1568 with {:ok, app} <- get_or_make_app(),
1569 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1570 {:ok, token} <- Token.exchange_token(app, auth) do
1572 |> put_session(:oauth_token, token.token)
1573 |> redirect(to: local_mastodon_root_path(conn))
1577 @doc "Local Mastodon FE callback action"
1578 def login(conn, _) do
1579 with {:ok, app} <- get_or_make_app() do
1584 response_type: "code",
1585 client_id: app.client_id,
1587 scope: Enum.join(app.scopes, " ")
1590 redirect(conn, to: path)
1594 defp local_mastodon_root_path(conn) do
1595 case get_session(conn, :return_to) do
1597 mastodon_api_path(conn, :index, ["getting-started"])
1600 delete_session(conn, :return_to)
1605 defp get_or_make_app do
1606 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1607 scopes = ["read", "write", "follow", "push"]
1609 with %App{} = app <- Repo.get_by(App, find_attrs) do
1611 if app.scopes == scopes do
1615 |> Ecto.Changeset.change(%{scopes: scopes})
1623 App.register_changeset(
1625 Map.put(find_attrs, :scopes, scopes)
1632 def logout(conn, _) do
1635 |> redirect(to: "/")
1638 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1639 Logger.debug("Unimplemented, returning unmodified relationship")
1641 with %User{} = target <- User.get_cached_by_id(id) do
1643 |> put_view(AccountView)
1644 |> render("relationship.json", %{user: user, target: target})
1648 def empty_array(conn, _) do
1649 Logger.debug("Unimplemented, returning an empty array")
1653 def empty_object(conn, _) do
1654 Logger.debug("Unimplemented, returning an empty object")
1658 def get_filters(%{assigns: %{user: user}} = conn, _) do
1659 filters = Filter.get_filters(user)
1660 res = FilterView.render("filters.json", filters: filters)
1665 %{assigns: %{user: user}} = conn,
1666 %{"phrase" => phrase, "context" => context} = params
1672 hide: Map.get(params, "irreversible", false),
1673 whole_word: Map.get(params, "boolean", true)
1677 {:ok, response} = Filter.create(query)
1678 res = FilterView.render("filter.json", filter: response)
1682 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1683 filter = Filter.get(filter_id, user)
1684 res = FilterView.render("filter.json", filter: filter)
1689 %{assigns: %{user: user}} = conn,
1690 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1694 filter_id: filter_id,
1697 hide: Map.get(params, "irreversible", nil),
1698 whole_word: Map.get(params, "boolean", true)
1702 {:ok, response} = Filter.update(query)
1703 res = FilterView.render("filter.json", filter: response)
1707 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1710 filter_id: filter_id
1713 {:ok, _} = Filter.delete(query)
1719 def errors(conn, {:error, %Changeset{} = changeset}) do
1722 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1723 |> Enum.map_join(", ", fn {_k, v} -> v end)
1727 |> json(%{error: error_message})
1730 def errors(conn, {:error, :not_found}) do
1733 |> json(%{error: "Record not found"})
1736 def errors(conn, _) do
1739 |> json("Something went wrong")
1742 def suggestions(%{assigns: %{user: user}} = conn, _) do
1743 suggestions = Config.get(:suggestions)
1745 if Keyword.get(suggestions, :enabled, false) do
1746 api = Keyword.get(suggestions, :third_party_engine, "")
1747 timeout = Keyword.get(suggestions, :timeout, 5000)
1748 limit = Keyword.get(suggestions, :limit, 23)
1750 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1752 user = user.nickname
1756 |> String.replace("{{host}}", host)
1757 |> String.replace("{{user}}", user)
1759 with {:ok, %{status: 200, body: body}} <-
1764 recv_timeout: timeout,
1768 {:ok, data} <- Jason.decode(body) do
1771 |> Enum.slice(0, limit)
1776 case User.get_or_fetch(x["acct"]) do
1777 {:ok, %User{id: id}} -> id
1783 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1786 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1792 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1799 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1800 with %Activity{} = activity <- Activity.get_by_id(status_id),
1801 true <- Visibility.visible_for_user?(activity, user) do
1805 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1815 def reports(%{assigns: %{user: user}} = conn, params) do
1816 case CommonAPI.report(user, params) do
1819 |> put_view(ReportView)
1820 |> try_render("report.json", %{activity: activity})
1824 |> put_status(:bad_request)
1825 |> json(%{error: err})
1829 def account_register(
1830 %{assigns: %{app: app}} = conn,
1831 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1839 "captcha_answer_data",
1843 |> Map.put("nickname", nickname)
1844 |> Map.put("fullname", params["fullname"] || nickname)
1845 |> Map.put("bio", params["bio"] || "")
1846 |> Map.put("confirm", params["password"])
1848 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1849 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1851 token_type: "Bearer",
1852 access_token: token.token,
1854 created_at: Token.Utils.format_created_at(token)
1860 |> json(Jason.encode!(errors))
1864 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1867 |> json(%{error: "Missing parameters"})
1870 def account_register(conn, _) do
1873 |> json(%{error: "Invalid credentials"})
1876 def conversations(%{assigns: %{user: user}} = conn, params) do
1877 participations = Participation.for_user_with_last_activity_id(user, params)
1880 Enum.map(participations, fn participation ->
1881 ConversationView.render("participation.json", %{participation: participation, user: user})
1885 |> add_link_headers(:conversations, participations)
1886 |> json(conversations)
1889 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1890 with %Participation{} = participation <-
1891 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1892 {:ok, participation} <- Participation.mark_as_read(participation) do
1893 participation_view =
1894 ConversationView.render("participation.json", %{participation: participation, user: user})
1897 |> json(participation_view)
1901 def try_render(conn, target, params)
1902 when is_binary(target) do
1903 res = render(conn, target, params)
1908 |> json(%{error: "Can't display this activity"})
1914 def try_render(conn, _, _) do
1917 |> json(%{error: "Can't display this activity"})
1920 defp present?(nil), do: false
1921 defp present?(false), do: false
1922 defp present?(_), do: true