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.Pagination
19 alias Pleroma.ScheduledActivity
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.AccountView
27 alias Pleroma.Web.MastodonAPI.AppView
28 alias Pleroma.Web.MastodonAPI.ConversationView
29 alias Pleroma.Web.MastodonAPI.FilterView
30 alias Pleroma.Web.MastodonAPI.ListView
31 alias Pleroma.Web.MastodonAPI.MastodonAPI
32 alias Pleroma.Web.MastodonAPI.MastodonView
33 alias Pleroma.Web.MastodonAPI.NotificationView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36 alias Pleroma.Web.MastodonAPI.StatusView
37 alias Pleroma.Web.MediaProxy
38 alias Pleroma.Web.OAuth.App
39 alias Pleroma.Web.OAuth.Authorization
40 alias Pleroma.Web.OAuth.Scopes
41 alias Pleroma.Web.OAuth.Token
42 alias Pleroma.Web.TwitterAPI.TwitterAPI
44 alias Pleroma.Web.ControllerHelper
49 plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register)
50 plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
52 @local_mastodon_name "Mastodon-Local"
54 action_fallback(:errors)
56 def create_app(conn, params) do
57 scopes = Scopes.fetch_scopes(params, ["read"])
61 |> Map.drop(["scope", "scopes"])
62 |> Map.put("scopes", scopes)
64 with cs <- App.register_changeset(%App{}, app_attrs),
65 false <- cs.changes[:client_name] == @local_mastodon_name,
66 {:ok, app} <- Repo.insert(cs) do
69 |> render("show.json", %{app: app})
78 value_function \\ fn x -> {:ok, x} end
80 if Map.has_key?(params, params_field) do
81 case value_function.(params[params_field]) do
82 {:ok, new_value} -> Map.put(map, map_field, new_value)
90 def update_credentials(%{assigns: %{user: user}} = conn, params) do
95 |> add_if_present(params, "display_name", :name)
96 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
97 |> add_if_present(params, "avatar", :avatar, fn value ->
98 with %Plug.Upload{} <- value,
99 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
106 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
109 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
120 :skip_thread_containment
122 |> Enum.reduce(%{}, fn key, acc ->
123 add_if_present(acc, params, to_string(key), key, fn value ->
124 {:ok, ControllerHelper.truthy_param?(value)}
127 |> add_if_present(params, "default_scope", :default_scope)
128 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
129 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
131 |> add_if_present(params, "header", :banner, fn value ->
132 with %Plug.Upload{} <- value,
133 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
139 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
140 with %Plug.Upload{} <- value,
141 {:ok, object} <- ActivityPub.upload(value, type: :background) do
147 |> Map.put(:emoji, user_info_emojis)
149 info_cng = User.Info.profile_update(user.info, info_params)
151 with changeset <- User.update_changeset(user, user_params),
152 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
153 {:ok, user} <- User.update_and_set_cache(changeset) do
154 if original_user != user do
155 CommonAPI.update(user)
160 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
166 |> json(%{error: "Invalid request"})
170 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
171 change = Changeset.change(user, %{avatar: nil})
172 {:ok, user} = User.update_and_set_cache(change)
173 CommonAPI.update(user)
175 json(conn, %{url: nil})
178 def update_avatar(%{assigns: %{user: user}} = conn, params) do
179 {:ok, object} = ActivityPub.upload(params, type: :avatar)
180 change = Changeset.change(user, %{avatar: object.data})
181 {:ok, user} = User.update_and_set_cache(change)
182 CommonAPI.update(user)
183 %{"url" => [%{"href" => href} | _]} = object.data
185 json(conn, %{url: href})
188 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
189 with new_info <- %{"banner" => %{}},
190 info_cng <- User.Info.profile_update(user.info, new_info),
191 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
192 {:ok, user} <- User.update_and_set_cache(changeset) do
193 CommonAPI.update(user)
195 json(conn, %{url: nil})
199 def update_banner(%{assigns: %{user: user}} = conn, params) do
200 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
201 new_info <- %{"banner" => object.data},
202 info_cng <- User.Info.profile_update(user.info, new_info),
203 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
204 {:ok, user} <- User.update_and_set_cache(changeset) do
205 CommonAPI.update(user)
206 %{"url" => [%{"href" => href} | _]} = object.data
208 json(conn, %{url: href})
212 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
213 with new_info <- %{"background" => %{}},
214 info_cng <- User.Info.profile_update(user.info, new_info),
215 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
216 {:ok, _user} <- User.update_and_set_cache(changeset) do
217 json(conn, %{url: nil})
221 def update_background(%{assigns: %{user: user}} = conn, params) do
222 with {:ok, object} <- ActivityPub.upload(params, type: :background),
223 new_info <- %{"background" => object.data},
224 info_cng <- User.Info.profile_update(user.info, new_info),
225 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
226 {:ok, _user} <- User.update_and_set_cache(changeset) do
227 %{"url" => [%{"href" => href} | _]} = object.data
229 json(conn, %{url: href})
233 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
234 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
237 AccountView.render("account.json", %{
240 with_pleroma_settings: true,
241 with_chat_token: chat_token
247 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
248 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
251 |> render("short.json", %{app: app})
255 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
256 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
257 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
258 account = AccountView.render("account.json", %{user: user, for: for_user})
264 |> json(%{error: "Can't find user"})
268 @mastodon_api_level "2.7.2"
270 def masto_instance(conn, _params) do
271 instance = Config.get(:instance)
275 title: Keyword.get(instance, :name),
276 description: Keyword.get(instance, :description),
277 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
278 email: Keyword.get(instance, :email),
280 streaming_api: Pleroma.Web.Endpoint.websocket_url()
282 stats: Stats.get_stats(),
283 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
285 registrations: Pleroma.Config.get([:instance, :registrations_open]),
286 # Extra (not present in Mastodon):
287 max_toot_chars: Keyword.get(instance, :limit),
288 poll_limits: Keyword.get(instance, :poll_limits)
294 def peers(conn, _params) do
295 json(conn, Stats.get_peers())
298 defp mastodonized_emoji do
299 Pleroma.Emoji.get_all()
300 |> Enum.map(fn {shortcode, relative_url, tags} ->
301 url = to_string(URI.merge(Web.base_url(), relative_url))
304 "shortcode" => shortcode,
306 "visible_in_picker" => true,
313 def custom_emojis(conn, _params) do
314 mastodon_emoji = mastodonized_emoji()
315 json(conn, mastodon_emoji)
318 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
321 |> Map.drop(["since_id", "max_id", "min_id"])
324 last = List.last(activities)
331 |> Map.get("limit", "20")
332 |> String.to_integer()
335 if length(activities) <= limit do
341 |> Enum.at(limit * -1)
345 {next_url, prev_url} =
349 Pleroma.Web.Endpoint,
352 Map.merge(params, %{max_id: max_id})
355 Pleroma.Web.Endpoint,
358 Map.merge(params, %{min_id: min_id})
364 Pleroma.Web.Endpoint,
366 Map.merge(params, %{max_id: max_id})
369 Pleroma.Web.Endpoint,
371 Map.merge(params, %{min_id: min_id})
377 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
383 def home_timeline(%{assigns: %{user: user}} = conn, params) do
386 |> Map.put("type", ["Create", "Announce"])
387 |> Map.put("blocking_user", user)
388 |> Map.put("muting_user", user)
389 |> Map.put("user", user)
392 [user.ap_id | user.following]
393 |> ActivityPub.fetch_activities(params)
397 |> add_link_headers(:home_timeline, activities)
398 |> put_view(StatusView)
399 |> render("index.json", %{activities: activities, for: user, as: :activity})
402 def public_timeline(%{assigns: %{user: user}} = conn, params) do
403 local_only = params["local"] in [true, "True", "true", "1"]
407 |> Map.put("type", ["Create", "Announce"])
408 |> Map.put("local_only", local_only)
409 |> Map.put("blocking_user", user)
410 |> Map.put("muting_user", user)
411 |> ActivityPub.fetch_public_activities()
415 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
416 |> put_view(StatusView)
417 |> render("index.json", %{activities: activities, for: user, as: :activity})
420 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
421 with %User{} = user <- User.get_cached_by_id(params["id"]) do
422 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
425 |> add_link_headers(:user_statuses, activities, params["id"])
426 |> put_view(StatusView)
427 |> render("index.json", %{
428 activities: activities,
435 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
438 |> Map.put("type", "Create")
439 |> Map.put("blocking_user", user)
440 |> Map.put("user", user)
441 |> Map.put(:visibility, "direct")
445 |> ActivityPub.fetch_activities_query(params)
446 |> Pagination.fetch_paginated(params)
449 |> add_link_headers(:dm_timeline, activities)
450 |> put_view(StatusView)
451 |> render("index.json", %{activities: activities, for: user, as: :activity})
454 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
455 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
456 true <- Visibility.visible_for_user?(activity, user) do
458 |> put_view(StatusView)
459 |> try_render("status.json", %{activity: activity, for: user})
463 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
464 with %Activity{} = activity <- Activity.get_by_id(id),
466 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
467 "blocking_user" => user,
471 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
473 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
474 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
480 activities: grouped_activities[true] || [],
484 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
489 activities: grouped_activities[false] || [],
493 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
500 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
501 with %Object{} = object <- Object.get_by_id(id),
502 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
503 true <- Visibility.visible_for_user?(activity, user) do
505 |> put_view(StatusView)
506 |> try_render("poll.json", %{object: object, for: user})
511 |> json(%{error: "Record not found"})
516 |> json(%{error: "Record not found"})
520 defp get_cached_vote_or_vote(user, object, choices) do
521 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
524 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
525 case CommonAPI.vote(user, object, choices) do
526 {:error, _message} = res -> {:ignore, res}
527 res -> {:commit, res}
534 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
535 with %Object{} = object <- Object.get_by_id(id),
536 true <- object.data["type"] == "Question",
537 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
538 true <- Visibility.visible_for_user?(activity, user),
539 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
541 |> put_view(StatusView)
542 |> try_render("poll.json", %{object: object, for: user})
547 |> json(%{error: "Record not found"})
552 |> json(%{error: "Record not found"})
557 |> json(%{error: message})
561 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
562 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
564 |> add_link_headers(:scheduled_statuses, scheduled_activities)
565 |> put_view(ScheduledActivityView)
566 |> render("index.json", %{scheduled_activities: scheduled_activities})
570 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
571 with %ScheduledActivity{} = scheduled_activity <-
572 ScheduledActivity.get(user, scheduled_activity_id) do
574 |> put_view(ScheduledActivityView)
575 |> render("show.json", %{scheduled_activity: scheduled_activity})
577 _ -> {:error, :not_found}
581 def update_scheduled_status(
582 %{assigns: %{user: user}} = conn,
583 %{"id" => scheduled_activity_id} = params
585 with %ScheduledActivity{} = scheduled_activity <-
586 ScheduledActivity.get(user, scheduled_activity_id),
587 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
589 |> put_view(ScheduledActivityView)
590 |> render("show.json", %{scheduled_activity: scheduled_activity})
592 nil -> {:error, :not_found}
597 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
598 with %ScheduledActivity{} = scheduled_activity <-
599 ScheduledActivity.get(user, scheduled_activity_id),
600 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
602 |> put_view(ScheduledActivityView)
603 |> render("show.json", %{scheduled_activity: scheduled_activity})
605 nil -> {:error, :not_found}
610 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
613 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
615 scheduled_at = params["scheduled_at"]
617 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
618 with {:ok, scheduled_activity} <-
619 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
621 |> put_view(ScheduledActivityView)
622 |> render("show.json", %{scheduled_activity: scheduled_activity})
625 params = Map.drop(params, ["scheduled_at"])
627 case get_cached_status_or_post(conn, params) do
628 {:ignore, message} ->
631 |> json(%{error: message})
636 |> json(%{error: message})
640 |> put_view(StatusView)
641 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
646 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
648 case get_req_header(conn, "idempotency-key") do
650 _ -> Ecto.UUID.generate()
653 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
654 case CommonAPI.post(user, params) do
655 {:ok, activity} -> activity
656 {:error, message} -> {:ignore, message}
661 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
662 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
668 |> json(%{error: "Can't delete this post"})
672 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
673 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
674 %Activity{} = announce <- Activity.normalize(announce.data) do
676 |> put_view(StatusView)
677 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
681 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
682 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
683 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
685 |> put_view(StatusView)
686 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
690 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
691 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
692 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
694 |> put_view(StatusView)
695 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
699 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
700 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
701 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
703 |> put_view(StatusView)
704 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
708 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
709 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
711 |> put_view(StatusView)
712 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
716 |> put_resp_content_type("application/json")
717 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
721 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
722 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
724 |> put_view(StatusView)
725 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
729 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
730 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
731 %User{} = user <- User.get_cached_by_nickname(user.nickname),
732 true <- Visibility.visible_for_user?(activity, user),
733 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
735 |> put_view(StatusView)
736 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
740 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
741 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
742 %User{} = user <- User.get_cached_by_nickname(user.nickname),
743 true <- Visibility.visible_for_user?(activity, user),
744 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
746 |> put_view(StatusView)
747 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
751 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
752 activity = Activity.get_by_id(id)
754 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
756 |> put_view(StatusView)
757 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
761 |> put_resp_content_type("application/json")
762 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
766 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
767 activity = Activity.get_by_id(id)
769 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
771 |> put_view(StatusView)
772 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
776 def notifications(%{assigns: %{user: user}} = conn, params) do
777 notifications = MastodonAPI.get_notifications(user, params)
780 |> add_link_headers(:notifications, notifications)
781 |> put_view(NotificationView)
782 |> render("index.json", %{notifications: notifications, for: user})
785 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
786 with {:ok, notification} <- Notification.get(user, id) do
788 |> put_view(NotificationView)
789 |> render("show.json", %{notification: notification, for: user})
793 |> put_resp_content_type("application/json")
794 |> send_resp(403, Jason.encode!(%{"error" => reason}))
798 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
799 Notification.clear(user)
803 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
804 with {:ok, _notif} <- Notification.dismiss(user, id) do
809 |> put_resp_content_type("application/json")
810 |> send_resp(403, Jason.encode!(%{"error" => reason}))
814 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
815 Notification.destroy_multiple(user, ids)
819 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
821 q = from(u in User, where: u.id in ^id)
822 targets = Repo.all(q)
825 |> put_view(AccountView)
826 |> render("relationships.json", %{user: user, targets: targets})
829 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
830 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
832 def update_media(%{assigns: %{user: user}} = conn, data) do
833 with %Object{} = object <- Repo.get(Object, data["id"]),
834 true <- Object.authorize_mutation(object, user),
835 true <- is_binary(data["description"]),
836 description <- data["description"] do
837 new_data = %{object.data | "name" => description}
841 |> Object.change(%{data: new_data})
844 attachment_data = Map.put(new_data, "id", object.id)
847 |> put_view(StatusView)
848 |> render("attachment.json", %{attachment: attachment_data})
852 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
853 with {:ok, object} <-
856 actor: User.ap_id(user),
857 description: Map.get(data, "description")
859 attachment_data = Map.put(object.data, "id", object.id)
862 |> put_view(StatusView)
863 |> render("attachment.json", %{attachment: attachment_data})
867 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
868 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
869 %{} = attachment_data <- Map.put(object.data, "id", object.id),
870 %{type: type} = rendered <-
871 StatusView.render("attachment.json", %{attachment: attachment_data}) do
872 # Reject if not an image
873 if type == "image" do
875 # Save to the user's info
876 info_changeset = User.Info.mascot_update(user.info, rendered)
880 |> Ecto.Changeset.change()
881 |> Ecto.Changeset.put_embed(:info, info_changeset)
883 {:ok, _user} = User.update_and_set_cache(user_changeset)
889 |> put_resp_content_type("application/json")
890 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
895 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
896 mascot = User.get_mascot(user)
902 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
903 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
904 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
905 q = from(u in User, where: u.ap_id in ^likes)
909 |> put_view(AccountView)
910 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
916 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
917 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
918 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
919 q = from(u in User, where: u.ap_id in ^announces)
923 |> put_view(AccountView)
924 |> render("accounts.json", %{for: user, users: users, as: :user})
930 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
931 local_only = params["local"] in [true, "True", "true", "1"]
934 [params["tag"], params["any"]]
938 |> Enum.map(&String.downcase(&1))
943 |> Enum.map(&String.downcase(&1))
948 |> Enum.map(&String.downcase(&1))
952 |> Map.put("type", "Create")
953 |> Map.put("local_only", local_only)
954 |> Map.put("blocking_user", user)
955 |> Map.put("muting_user", user)
956 |> Map.put("tag", tags)
957 |> Map.put("tag_all", tag_all)
958 |> Map.put("tag_reject", tag_reject)
959 |> ActivityPub.fetch_public_activities()
963 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
964 |> put_view(StatusView)
965 |> render("index.json", %{activities: activities, for: user, as: :activity})
968 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
969 with %User{} = user <- User.get_cached_by_id(id),
970 followers <- MastodonAPI.get_followers(user, params) do
973 for_user && user.id == for_user.id -> followers
974 user.info.hide_followers -> []
979 |> add_link_headers(:followers, followers, user)
980 |> put_view(AccountView)
981 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
985 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
986 with %User{} = user <- User.get_cached_by_id(id),
987 followers <- MastodonAPI.get_friends(user, params) do
990 for_user && user.id == for_user.id -> followers
991 user.info.hide_follows -> []
996 |> add_link_headers(:following, followers, user)
997 |> put_view(AccountView)
998 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1002 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
1003 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
1005 |> put_view(AccountView)
1006 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1010 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1011 with %User{} = follower <- User.get_cached_by_id(id),
1012 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1014 |> put_view(AccountView)
1015 |> render("relationship.json", %{user: followed, target: follower})
1017 {:error, message} ->
1019 |> put_resp_content_type("application/json")
1020 |> send_resp(403, Jason.encode!(%{"error" => message}))
1024 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1025 with %User{} = follower <- User.get_cached_by_id(id),
1026 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1028 |> put_view(AccountView)
1029 |> render("relationship.json", %{user: followed, target: follower})
1031 {:error, message} ->
1033 |> put_resp_content_type("application/json")
1034 |> send_resp(403, Jason.encode!(%{"error" => message}))
1038 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1039 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1040 {_, true} <- {:followed, follower.id != followed.id},
1041 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1043 |> put_view(AccountView)
1044 |> render("relationship.json", %{user: follower, target: followed})
1047 {:error, :not_found}
1049 {:error, message} ->
1051 |> put_resp_content_type("application/json")
1052 |> send_resp(403, Jason.encode!(%{"error" => message}))
1056 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1057 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1058 {_, true} <- {:followed, follower.id != followed.id},
1059 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1061 |> put_view(AccountView)
1062 |> render("account.json", %{user: followed, for: follower})
1065 {:error, :not_found}
1067 {:error, message} ->
1069 |> put_resp_content_type("application/json")
1070 |> send_resp(403, Jason.encode!(%{"error" => message}))
1074 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1075 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1076 {_, true} <- {:followed, follower.id != followed.id},
1077 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1079 |> put_view(AccountView)
1080 |> render("relationship.json", %{user: follower, target: followed})
1083 {:error, :not_found}
1090 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1091 with %User{} = muted <- User.get_cached_by_id(id),
1092 {:ok, muter} <- User.mute(muter, muted) do
1094 |> put_view(AccountView)
1095 |> render("relationship.json", %{user: muter, target: muted})
1097 {:error, message} ->
1099 |> put_resp_content_type("application/json")
1100 |> send_resp(403, Jason.encode!(%{"error" => message}))
1104 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1105 with %User{} = muted <- User.get_cached_by_id(id),
1106 {:ok, muter} <- User.unmute(muter, muted) do
1108 |> put_view(AccountView)
1109 |> render("relationship.json", %{user: muter, target: muted})
1111 {:error, message} ->
1113 |> put_resp_content_type("application/json")
1114 |> send_resp(403, Jason.encode!(%{"error" => message}))
1118 def mutes(%{assigns: %{user: user}} = conn, _) do
1119 with muted_accounts <- User.muted_users(user) do
1120 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1125 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1126 with %User{} = blocked <- User.get_cached_by_id(id),
1127 {:ok, blocker} <- User.block(blocker, blocked),
1128 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1130 |> put_view(AccountView)
1131 |> render("relationship.json", %{user: blocker, target: blocked})
1133 {:error, message} ->
1135 |> put_resp_content_type("application/json")
1136 |> send_resp(403, Jason.encode!(%{"error" => message}))
1140 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1141 with %User{} = blocked <- User.get_cached_by_id(id),
1142 {:ok, blocker} <- User.unblock(blocker, blocked),
1143 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1145 |> put_view(AccountView)
1146 |> render("relationship.json", %{user: blocker, target: blocked})
1148 {:error, message} ->
1150 |> put_resp_content_type("application/json")
1151 |> send_resp(403, Jason.encode!(%{"error" => message}))
1155 def blocks(%{assigns: %{user: user}} = conn, _) do
1156 with blocked_accounts <- User.blocked_users(user) do
1157 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1162 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1163 json(conn, info.domain_blocks || [])
1166 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1167 User.block_domain(blocker, domain)
1171 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1172 User.unblock_domain(blocker, domain)
1176 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1177 with %User{} = subscription_target <- User.get_cached_by_id(id),
1178 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1180 |> put_view(AccountView)
1181 |> render("relationship.json", %{user: user, target: subscription_target})
1183 {:error, message} ->
1185 |> put_resp_content_type("application/json")
1186 |> send_resp(403, Jason.encode!(%{"error" => message}))
1190 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1191 with %User{} = subscription_target <- User.get_cached_by_id(id),
1192 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1194 |> put_view(AccountView)
1195 |> render("relationship.json", %{user: user, target: subscription_target})
1197 {:error, message} ->
1199 |> put_resp_content_type("application/json")
1200 |> send_resp(403, Jason.encode!(%{"error" => message}))
1204 def favourites(%{assigns: %{user: user}} = conn, params) do
1207 |> Map.put("type", "Create")
1208 |> Map.put("favorited_by", user.ap_id)
1209 |> Map.put("blocking_user", user)
1212 ActivityPub.fetch_activities([], params)
1216 |> add_link_headers(:favourites, activities)
1217 |> put_view(StatusView)
1218 |> render("index.json", %{activities: activities, for: user, as: :activity})
1221 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1222 with %User{} = user <- User.get_by_id(id),
1223 false <- user.info.hide_favorites do
1226 |> Map.put("type", "Create")
1227 |> Map.put("favorited_by", user.ap_id)
1228 |> Map.put("blocking_user", for_user)
1232 ["https://www.w3.org/ns/activitystreams#Public"] ++
1233 [for_user.ap_id | for_user.following]
1235 ["https://www.w3.org/ns/activitystreams#Public"]
1240 |> ActivityPub.fetch_activities(params)
1244 |> add_link_headers(:favourites, activities)
1245 |> put_view(StatusView)
1246 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1249 {:error, :not_found}
1254 |> json(%{error: "Can't get favorites"})
1258 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1259 user = User.get_cached_by_id(user.id)
1262 Bookmark.for_user_query(user.id)
1263 |> Pagination.fetch_paginated(params)
1267 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1270 |> add_link_headers(:bookmarks, bookmarks)
1271 |> put_view(StatusView)
1272 |> render("index.json", %{activities: activities, for: user, as: :activity})
1275 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1276 lists = Pleroma.List.for_user(user, opts)
1277 res = ListView.render("lists.json", lists: lists)
1281 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1282 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1283 res = ListView.render("list.json", list: list)
1289 |> json(%{error: "Record not found"})
1293 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1294 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1295 res = ListView.render("lists.json", lists: lists)
1299 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1300 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1301 {:ok, _list} <- Pleroma.List.delete(list) do
1309 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1310 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1311 res = ListView.render("list.json", list: list)
1316 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1318 |> Enum.each(fn account_id ->
1319 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1320 %User{} = followed <- User.get_cached_by_id(account_id) do
1321 Pleroma.List.follow(list, followed)
1328 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1330 |> Enum.each(fn account_id ->
1331 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1332 %User{} = followed <- User.get_cached_by_id(account_id) do
1333 Pleroma.List.unfollow(list, followed)
1340 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1341 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1342 {:ok, users} = Pleroma.List.get_following(list) do
1344 |> put_view(AccountView)
1345 |> render("accounts.json", %{for: user, users: users, as: :user})
1349 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1350 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1351 {:ok, list} <- Pleroma.List.rename(list, title) do
1352 res = ListView.render("list.json", list: list)
1360 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1361 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1364 |> Map.put("type", "Create")
1365 |> Map.put("blocking_user", user)
1366 |> Map.put("muting_user", user)
1368 # we must filter the following list for the user to avoid leaking statuses the user
1369 # does not actually have permission to see (for more info, peruse security issue #270).
1372 |> Enum.filter(fn x -> x in user.following end)
1373 |> ActivityPub.fetch_activities_bounded(following, params)
1377 |> put_view(StatusView)
1378 |> render("index.json", %{activities: activities, for: user, as: :activity})
1383 |> json(%{error: "Error."})
1387 def index(%{assigns: %{user: user}} = conn, _params) do
1388 token = get_session(conn, :oauth_token)
1391 mastodon_emoji = mastodonized_emoji()
1393 limit = Config.get([:instance, :limit])
1396 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1401 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1402 access_token: token,
1404 domain: Pleroma.Web.Endpoint.host(),
1407 unfollow_modal: false,
1410 auto_play_gif: false,
1411 display_sensitive_media: false,
1412 reduce_motion: false,
1413 max_toot_chars: limit,
1414 mascot: User.get_mascot(user)["url"]
1416 poll_limits: Config.get([:instance, :poll_limits]),
1418 delete_others_notice: present?(user.info.is_moderator),
1419 admin: present?(user.info.is_admin)
1423 default_privacy: user.info.default_scope,
1424 default_sensitive: false,
1425 allow_content_types: Config.get([:instance, :allowed_post_formats])
1427 media_attachments: %{
1428 accept_content_types: [
1444 user.info.settings ||
1474 push_subscription: nil,
1476 custom_emojis: mastodon_emoji,
1482 |> put_layout(false)
1483 |> put_view(MastodonView)
1484 |> render("index.html", %{initial_state: initial_state})
1487 |> put_session(:return_to, conn.request_path)
1488 |> redirect(to: "/web/login")
1492 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1493 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1495 with changeset <- Ecto.Changeset.change(user),
1496 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1497 {:ok, _user} <- User.update_and_set_cache(changeset) do
1502 |> put_resp_content_type("application/json")
1503 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1507 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1508 redirect(conn, to: local_mastodon_root_path(conn))
1511 @doc "Local Mastodon FE login init action"
1512 def login(conn, %{"code" => auth_token}) do
1513 with {:ok, app} <- get_or_make_app(),
1514 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1515 {:ok, token} <- Token.exchange_token(app, auth) do
1517 |> put_session(:oauth_token, token.token)
1518 |> redirect(to: local_mastodon_root_path(conn))
1522 @doc "Local Mastodon FE callback action"
1523 def login(conn, _) do
1524 with {:ok, app} <- get_or_make_app() do
1529 response_type: "code",
1530 client_id: app.client_id,
1532 scope: Enum.join(app.scopes, " ")
1535 redirect(conn, to: path)
1539 defp local_mastodon_root_path(conn) do
1540 case get_session(conn, :return_to) do
1542 mastodon_api_path(conn, :index, ["getting-started"])
1545 delete_session(conn, :return_to)
1550 defp get_or_make_app do
1551 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1552 scopes = ["read", "write", "follow", "push"]
1554 with %App{} = app <- Repo.get_by(App, find_attrs) do
1556 if app.scopes == scopes do
1560 |> Ecto.Changeset.change(%{scopes: scopes})
1568 App.register_changeset(
1570 Map.put(find_attrs, :scopes, scopes)
1577 def logout(conn, _) do
1580 |> redirect(to: "/")
1583 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1584 Logger.debug("Unimplemented, returning unmodified relationship")
1586 with %User{} = target <- User.get_cached_by_id(id) do
1588 |> put_view(AccountView)
1589 |> render("relationship.json", %{user: user, target: target})
1593 def empty_array(conn, _) do
1594 Logger.debug("Unimplemented, returning an empty array")
1598 def empty_object(conn, _) do
1599 Logger.debug("Unimplemented, returning an empty object")
1603 def get_filters(%{assigns: %{user: user}} = conn, _) do
1604 filters = Filter.get_filters(user)
1605 res = FilterView.render("filters.json", filters: filters)
1610 %{assigns: %{user: user}} = conn,
1611 %{"phrase" => phrase, "context" => context} = params
1617 hide: Map.get(params, "irreversible", false),
1618 whole_word: Map.get(params, "boolean", true)
1622 {:ok, response} = Filter.create(query)
1623 res = FilterView.render("filter.json", filter: response)
1627 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1628 filter = Filter.get(filter_id, user)
1629 res = FilterView.render("filter.json", filter: filter)
1634 %{assigns: %{user: user}} = conn,
1635 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1639 filter_id: filter_id,
1642 hide: Map.get(params, "irreversible", nil),
1643 whole_word: Map.get(params, "boolean", true)
1647 {:ok, response} = Filter.update(query)
1648 res = FilterView.render("filter.json", filter: response)
1652 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1655 filter_id: filter_id
1658 {:ok, _} = Filter.delete(query)
1664 def errors(conn, {:error, %Changeset{} = changeset}) do
1667 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1668 |> Enum.map_join(", ", fn {_k, v} -> v end)
1672 |> json(%{error: error_message})
1675 def errors(conn, {:error, :not_found}) do
1678 |> json(%{error: "Record not found"})
1681 def errors(conn, _) do
1684 |> json("Something went wrong")
1687 def suggestions(%{assigns: %{user: user}} = conn, _) do
1688 suggestions = Config.get(:suggestions)
1690 if Keyword.get(suggestions, :enabled, false) do
1691 api = Keyword.get(suggestions, :third_party_engine, "")
1692 timeout = Keyword.get(suggestions, :timeout, 5000)
1693 limit = Keyword.get(suggestions, :limit, 23)
1695 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1697 user = user.nickname
1701 |> String.replace("{{host}}", host)
1702 |> String.replace("{{user}}", user)
1704 with {:ok, %{status: 200, body: body}} <-
1709 recv_timeout: timeout,
1713 {:ok, data} <- Jason.decode(body) do
1716 |> Enum.slice(0, limit)
1721 case User.get_or_fetch(x["acct"]) do
1722 {:ok, %User{id: id}} -> id
1728 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1731 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1737 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1744 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1745 with %Activity{} = activity <- Activity.get_by_id(status_id),
1746 true <- Visibility.visible_for_user?(activity, user) do
1750 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1760 def reports(%{assigns: %{user: user}} = conn, params) do
1761 case CommonAPI.report(user, params) do
1764 |> put_view(ReportView)
1765 |> try_render("report.json", %{activity: activity})
1769 |> put_status(:bad_request)
1770 |> json(%{error: err})
1774 def account_register(
1775 %{assigns: %{app: app}} = conn,
1776 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1784 "captcha_answer_data",
1788 |> Map.put("nickname", nickname)
1789 |> Map.put("fullname", params["fullname"] || nickname)
1790 |> Map.put("bio", params["bio"] || "")
1791 |> Map.put("confirm", params["password"])
1793 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1794 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1796 token_type: "Bearer",
1797 access_token: token.token,
1799 created_at: Token.Utils.format_created_at(token)
1805 |> json(Jason.encode!(errors))
1809 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1812 |> json(%{error: "Missing parameters"})
1815 def account_register(conn, _) do
1818 |> json(%{error: "Invalid credentials"})
1821 def conversations(%{assigns: %{user: user}} = conn, params) do
1822 participations = Participation.for_user_with_last_activity_id(user, params)
1825 Enum.map(participations, fn participation ->
1826 ConversationView.render("participation.json", %{participation: participation, user: user})
1830 |> add_link_headers(:conversations, participations)
1831 |> json(conversations)
1834 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1835 with %Participation{} = participation <-
1836 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1837 {:ok, participation} <- Participation.mark_as_read(participation) do
1838 participation_view =
1839 ConversationView.render("participation.json", %{participation: participation, user: user})
1842 |> json(participation_view)
1846 def try_render(conn, target, params)
1847 when is_binary(target) do
1848 res = render(conn, target, params)
1853 |> json(%{error: "Can't display this activity"})
1859 def try_render(conn, _, _) do
1862 |> json(%{error: "Can't display this activity"})
1865 defp present?(nil), do: false
1866 defp present?(false), do: false
1867 defp present?(_), do: true