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
18 alias Pleroma.Plugs.RateLimiter
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
50 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
51 post_status delete_status)a
55 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
56 when action in ~w(reblog_status unreblog_status)a
61 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
62 when action in ~w(fav_status unfav_status)a
65 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
66 plug(RateLimiter, :app_account_creation when action == :account_register)
67 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
69 @local_mastodon_name "Mastodon-Local"
71 action_fallback(:errors)
73 def create_app(conn, params) do
74 scopes = Scopes.fetch_scopes(params, ["read"])
78 |> Map.drop(["scope", "scopes"])
79 |> Map.put("scopes", scopes)
81 with cs <- App.register_changeset(%App{}, app_attrs),
82 false <- cs.changes[:client_name] == @local_mastodon_name,
83 {:ok, app} <- Repo.insert(cs) do
86 |> render("show.json", %{app: app})
95 value_function \\ fn x -> {:ok, x} end
97 if Map.has_key?(params, params_field) do
98 case value_function.(params[params_field]) do
99 {:ok, new_value} -> Map.put(map, map_field, new_value)
107 def update_credentials(%{assigns: %{user: user}} = conn, params) do
112 |> add_if_present(params, "display_name", :name)
113 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
114 |> add_if_present(params, "avatar", :avatar, fn value ->
115 with %Plug.Upload{} <- value,
116 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
123 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
126 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
137 :skip_thread_containment
139 |> Enum.reduce(%{}, fn key, acc ->
140 add_if_present(acc, params, to_string(key), key, fn value ->
141 {:ok, ControllerHelper.truthy_param?(value)}
144 |> add_if_present(params, "default_scope", :default_scope)
145 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
146 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
148 |> add_if_present(params, "header", :banner, fn value ->
149 with %Plug.Upload{} <- value,
150 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
156 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
157 with %Plug.Upload{} <- value,
158 {:ok, object} <- ActivityPub.upload(value, type: :background) do
164 |> Map.put(:emoji, user_info_emojis)
166 info_cng = User.Info.profile_update(user.info, info_params)
168 with changeset <- User.update_changeset(user, user_params),
169 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
170 {:ok, user} <- User.update_and_set_cache(changeset) do
171 if original_user != user do
172 CommonAPI.update(user)
177 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
180 _e -> render_error(conn, :forbidden, "Invalid request")
184 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
185 change = Changeset.change(user, %{avatar: nil})
186 {:ok, user} = User.update_and_set_cache(change)
187 CommonAPI.update(user)
189 json(conn, %{url: nil})
192 def update_avatar(%{assigns: %{user: user}} = conn, params) do
193 {:ok, object} = ActivityPub.upload(params, type: :avatar)
194 change = Changeset.change(user, %{avatar: object.data})
195 {:ok, user} = User.update_and_set_cache(change)
196 CommonAPI.update(user)
197 %{"url" => [%{"href" => href} | _]} = object.data
199 json(conn, %{url: href})
202 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
203 with new_info <- %{"banner" => %{}},
204 info_cng <- User.Info.profile_update(user.info, new_info),
205 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
206 {:ok, user} <- User.update_and_set_cache(changeset) do
207 CommonAPI.update(user)
209 json(conn, %{url: nil})
213 def update_banner(%{assigns: %{user: user}} = conn, params) do
214 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
215 new_info <- %{"banner" => object.data},
216 info_cng <- User.Info.profile_update(user.info, new_info),
217 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
218 {:ok, user} <- User.update_and_set_cache(changeset) do
219 CommonAPI.update(user)
220 %{"url" => [%{"href" => href} | _]} = object.data
222 json(conn, %{url: href})
226 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
227 with new_info <- %{"background" => %{}},
228 info_cng <- User.Info.profile_update(user.info, new_info),
229 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
230 {:ok, _user} <- User.update_and_set_cache(changeset) do
231 json(conn, %{url: nil})
235 def update_background(%{assigns: %{user: user}} = conn, params) do
236 with {:ok, object} <- ActivityPub.upload(params, type: :background),
237 new_info <- %{"background" => object.data},
238 info_cng <- User.Info.profile_update(user.info, new_info),
239 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
240 {:ok, _user} <- User.update_and_set_cache(changeset) do
241 %{"url" => [%{"href" => href} | _]} = object.data
243 json(conn, %{url: href})
247 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
248 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
251 AccountView.render("account.json", %{
254 with_pleroma_settings: true,
255 with_chat_token: chat_token
261 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
262 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
265 |> render("short.json", %{app: app})
269 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
270 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
271 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
272 account = AccountView.render("account.json", %{user: user, for: for_user})
275 _e -> render_error(conn, :not_found, "Can't find user")
279 @mastodon_api_level "2.7.2"
281 def masto_instance(conn, _params) do
282 instance = Config.get(:instance)
286 title: Keyword.get(instance, :name),
287 description: Keyword.get(instance, :description),
288 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
289 email: Keyword.get(instance, :email),
291 streaming_api: Pleroma.Web.Endpoint.websocket_url()
293 stats: Stats.get_stats(),
294 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
296 registrations: Pleroma.Config.get([:instance, :registrations_open]),
297 # Extra (not present in Mastodon):
298 max_toot_chars: Keyword.get(instance, :limit),
299 poll_limits: Keyword.get(instance, :poll_limits)
305 def peers(conn, _params) do
306 json(conn, Stats.get_peers())
309 defp mastodonized_emoji do
310 Pleroma.Emoji.get_all()
311 |> Enum.map(fn {shortcode, relative_url, tags} ->
312 url = to_string(URI.merge(Web.base_url(), relative_url))
315 "shortcode" => shortcode,
317 "visible_in_picker" => true,
320 # Assuming that a comma is authorized in the category name
321 "category" => (tags -- ["Custom"]) |> Enum.join(",")
326 def custom_emojis(conn, _params) do
327 mastodon_emoji = mastodonized_emoji()
328 json(conn, mastodon_emoji)
331 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
334 |> Map.drop(["since_id", "max_id", "min_id"])
337 last = List.last(activities)
344 |> Map.get("limit", "20")
345 |> String.to_integer()
348 if length(activities) <= limit do
354 |> Enum.at(limit * -1)
358 {next_url, prev_url} =
362 Pleroma.Web.Endpoint,
365 Map.merge(params, %{max_id: max_id})
368 Pleroma.Web.Endpoint,
371 Map.merge(params, %{min_id: min_id})
377 Pleroma.Web.Endpoint,
379 Map.merge(params, %{max_id: max_id})
382 Pleroma.Web.Endpoint,
384 Map.merge(params, %{min_id: min_id})
390 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
396 def home_timeline(%{assigns: %{user: user}} = conn, params) do
399 |> Map.put("type", ["Create", "Announce"])
400 |> Map.put("blocking_user", user)
401 |> Map.put("muting_user", user)
402 |> Map.put("user", user)
405 [user.ap_id | user.following]
406 |> ActivityPub.fetch_activities(params)
410 |> add_link_headers(:home_timeline, activities)
411 |> put_view(StatusView)
412 |> render("index.json", %{activities: activities, for: user, as: :activity})
415 def public_timeline(%{assigns: %{user: user}} = conn, params) do
416 local_only = params["local"] in [true, "True", "true", "1"]
420 |> Map.put("type", ["Create", "Announce"])
421 |> Map.put("local_only", local_only)
422 |> Map.put("blocking_user", user)
423 |> Map.put("muting_user", user)
424 |> ActivityPub.fetch_public_activities()
428 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
429 |> put_view(StatusView)
430 |> render("index.json", %{activities: activities, for: user, as: :activity})
433 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
434 with %User{} = user <- User.get_cached_by_id(params["id"]) do
437 |> Map.put("tag", params["tagged"])
439 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
442 |> add_link_headers(:user_statuses, activities, params["id"])
443 |> put_view(StatusView)
444 |> render("index.json", %{
445 activities: activities,
452 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
455 |> Map.put("type", "Create")
456 |> Map.put("blocking_user", user)
457 |> Map.put("user", user)
458 |> Map.put(:visibility, "direct")
462 |> ActivityPub.fetch_activities_query(params)
463 |> Pagination.fetch_paginated(params)
466 |> add_link_headers(:dm_timeline, activities)
467 |> put_view(StatusView)
468 |> render("index.json", %{activities: activities, for: user, as: :activity})
471 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
472 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
473 true <- Visibility.visible_for_user?(activity, user) do
475 |> put_view(StatusView)
476 |> try_render("status.json", %{activity: activity, for: user})
480 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
481 with %Activity{} = activity <- Activity.get_by_id(id),
483 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
484 "blocking_user" => user,
488 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
490 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
491 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
497 activities: grouped_activities[true] || [],
501 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
506 activities: grouped_activities[false] || [],
510 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
517 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
518 with %Object{} = object <- Object.get_by_id(id),
519 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
520 true <- Visibility.visible_for_user?(activity, user) do
522 |> put_view(StatusView)
523 |> try_render("poll.json", %{object: object, for: user})
525 nil -> render_error(conn, :not_found, "Record not found")
526 false -> render_error(conn, :not_found, "Record not found")
530 defp get_cached_vote_or_vote(user, object, choices) do
531 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
534 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
535 case CommonAPI.vote(user, object, choices) do
536 {:error, _message} = res -> {:ignore, res}
537 res -> {:commit, res}
544 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
545 with %Object{} = object <- Object.get_by_id(id),
546 true <- object.data["type"] == "Question",
547 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
548 true <- Visibility.visible_for_user?(activity, user),
549 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
551 |> put_view(StatusView)
552 |> try_render("poll.json", %{object: object, for: user})
555 render_error(conn, :not_found, "Record not found")
558 render_error(conn, :not_found, "Record not found")
562 |> put_status(:unprocessable_entity)
563 |> json(%{error: message})
567 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
568 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
570 |> add_link_headers(:scheduled_statuses, scheduled_activities)
571 |> put_view(ScheduledActivityView)
572 |> render("index.json", %{scheduled_activities: scheduled_activities})
576 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
577 with %ScheduledActivity{} = scheduled_activity <-
578 ScheduledActivity.get(user, scheduled_activity_id) do
580 |> put_view(ScheduledActivityView)
581 |> render("show.json", %{scheduled_activity: scheduled_activity})
583 _ -> {:error, :not_found}
587 def update_scheduled_status(
588 %{assigns: %{user: user}} = conn,
589 %{"id" => scheduled_activity_id} = params
591 with %ScheduledActivity{} = scheduled_activity <-
592 ScheduledActivity.get(user, scheduled_activity_id),
593 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
595 |> put_view(ScheduledActivityView)
596 |> render("show.json", %{scheduled_activity: scheduled_activity})
598 nil -> {:error, :not_found}
603 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
604 with %ScheduledActivity{} = scheduled_activity <-
605 ScheduledActivity.get(user, scheduled_activity_id),
606 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
608 |> put_view(ScheduledActivityView)
609 |> render("show.json", %{scheduled_activity: scheduled_activity})
611 nil -> {:error, :not_found}
616 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
619 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
621 scheduled_at = params["scheduled_at"]
623 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
624 with {:ok, scheduled_activity} <-
625 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
627 |> put_view(ScheduledActivityView)
628 |> render("show.json", %{scheduled_activity: scheduled_activity})
631 params = Map.drop(params, ["scheduled_at"])
633 case CommonAPI.post(user, params) do
636 |> put_status(:unprocessable_entity)
637 |> json(%{error: message})
641 |> put_view(StatusView)
642 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
647 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
648 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
651 _e -> render_error(conn, :forbidden, "Can't delete this post")
655 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
656 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
657 %Activity{} = announce <- Activity.normalize(announce.data) do
659 |> put_view(StatusView)
660 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
664 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
665 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
666 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
668 |> put_view(StatusView)
669 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
673 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
674 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
675 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
677 |> put_view(StatusView)
678 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
682 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
683 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
684 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
686 |> put_view(StatusView)
687 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
691 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
692 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
694 |> put_view(StatusView)
695 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
699 |> put_status(:bad_request)
700 |> json(%{"error" => reason})
704 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
705 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
707 |> put_view(StatusView)
708 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
712 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
713 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
714 %User{} = user <- User.get_cached_by_nickname(user.nickname),
715 true <- Visibility.visible_for_user?(activity, user),
716 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
718 |> put_view(StatusView)
719 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
723 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
724 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
725 %User{} = user <- User.get_cached_by_nickname(user.nickname),
726 true <- Visibility.visible_for_user?(activity, user),
727 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
729 |> put_view(StatusView)
730 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
734 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
735 activity = Activity.get_by_id(id)
737 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
739 |> put_view(StatusView)
740 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
744 |> put_resp_content_type("application/json")
745 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
749 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
750 activity = Activity.get_by_id(id)
752 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
754 |> put_view(StatusView)
755 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
759 def notifications(%{assigns: %{user: user}} = conn, params) do
760 notifications = MastodonAPI.get_notifications(user, params)
763 |> add_link_headers(:notifications, notifications)
764 |> put_view(NotificationView)
765 |> render("index.json", %{notifications: notifications, for: user})
768 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
769 with {:ok, notification} <- Notification.get(user, id) do
771 |> put_view(NotificationView)
772 |> render("show.json", %{notification: notification, for: user})
776 |> put_status(:forbidden)
777 |> json(%{"error" => reason})
781 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
782 Notification.clear(user)
786 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
787 with {:ok, _notif} <- Notification.dismiss(user, id) do
792 |> put_status(:forbidden)
793 |> json(%{"error" => reason})
797 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
798 Notification.destroy_multiple(user, ids)
802 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
804 q = from(u in User, where: u.id in ^id)
805 targets = Repo.all(q)
808 |> put_view(AccountView)
809 |> render("relationships.json", %{user: user, targets: targets})
812 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
813 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
815 def update_media(%{assigns: %{user: user}} = conn, data) do
816 with %Object{} = object <- Repo.get(Object, data["id"]),
817 true <- Object.authorize_mutation(object, user),
818 true <- is_binary(data["description"]),
819 description <- data["description"] do
820 new_data = %{object.data | "name" => description}
824 |> Object.change(%{data: new_data})
827 attachment_data = Map.put(new_data, "id", object.id)
830 |> put_view(StatusView)
831 |> render("attachment.json", %{attachment: attachment_data})
835 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
836 with {:ok, object} <-
839 actor: User.ap_id(user),
840 description: Map.get(data, "description")
842 attachment_data = Map.put(object.data, "id", object.id)
845 |> put_view(StatusView)
846 |> render("attachment.json", %{attachment: attachment_data})
850 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
851 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
852 %{} = attachment_data <- Map.put(object.data, "id", object.id),
853 %{type: type} = rendered <-
854 StatusView.render("attachment.json", %{attachment: attachment_data}) do
855 # Reject if not an image
856 if type == "image" do
858 # Save to the user's info
859 info_changeset = User.Info.mascot_update(user.info, rendered)
863 |> Ecto.Changeset.change()
864 |> Ecto.Changeset.put_embed(:info, info_changeset)
866 {:ok, _user} = User.update_and_set_cache(user_changeset)
871 render_error(conn, :unsupported_media_type, "mascots can only be images")
876 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
877 mascot = User.get_mascot(user)
883 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
884 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
885 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
886 q = from(u in User, where: u.ap_id in ^likes)
890 |> put_view(AccountView)
891 |> render("accounts.json", %{for: user, users: users, as: :user})
897 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
898 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
899 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
900 q = from(u in User, where: u.ap_id in ^announces)
904 |> put_view(AccountView)
905 |> render("accounts.json", %{for: user, users: users, as: :user})
911 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
912 local_only = params["local"] in [true, "True", "true", "1"]
915 [params["tag"], params["any"]]
919 |> Enum.map(&String.downcase(&1))
924 |> Enum.map(&String.downcase(&1))
929 |> Enum.map(&String.downcase(&1))
933 |> Map.put("type", "Create")
934 |> Map.put("local_only", local_only)
935 |> Map.put("blocking_user", user)
936 |> Map.put("muting_user", user)
937 |> Map.put("tag", tags)
938 |> Map.put("tag_all", tag_all)
939 |> Map.put("tag_reject", tag_reject)
940 |> ActivityPub.fetch_public_activities()
944 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
945 |> put_view(StatusView)
946 |> render("index.json", %{activities: activities, for: user, as: :activity})
949 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
950 with %User{} = user <- User.get_cached_by_id(id),
951 followers <- MastodonAPI.get_followers(user, params) do
954 for_user && user.id == for_user.id -> followers
955 user.info.hide_followers -> []
960 |> add_link_headers(:followers, followers, user)
961 |> put_view(AccountView)
962 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
966 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
967 with %User{} = user <- User.get_cached_by_id(id),
968 followers <- MastodonAPI.get_friends(user, params) do
971 for_user && user.id == for_user.id -> followers
972 user.info.hide_follows -> []
977 |> add_link_headers(:following, followers, user)
978 |> put_view(AccountView)
979 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
983 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
984 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
986 |> put_view(AccountView)
987 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
991 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
992 with %User{} = follower <- User.get_cached_by_id(id),
993 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
995 |> put_view(AccountView)
996 |> render("relationship.json", %{user: followed, target: follower})
1000 |> put_status(:forbidden)
1001 |> json(%{error: message})
1005 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1006 with %User{} = follower <- User.get_cached_by_id(id),
1007 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1009 |> put_view(AccountView)
1010 |> render("relationship.json", %{user: followed, target: follower})
1012 {:error, message} ->
1014 |> put_status(:forbidden)
1015 |> json(%{error: message})
1019 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1020 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1021 {_, true} <- {:followed, follower.id != followed.id},
1022 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1024 |> put_view(AccountView)
1025 |> render("relationship.json", %{user: follower, target: followed})
1028 {:error, :not_found}
1030 {:error, message} ->
1032 |> put_status(:forbidden)
1033 |> json(%{error: message})
1037 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1038 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1039 {_, true} <- {:followed, follower.id != followed.id},
1040 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1042 |> put_view(AccountView)
1043 |> render("account.json", %{user: followed, for: follower})
1046 {:error, :not_found}
1048 {:error, message} ->
1050 |> put_status(:forbidden)
1051 |> json(%{error: message})
1055 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1056 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1057 {_, true} <- {:followed, follower.id != followed.id},
1058 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1060 |> put_view(AccountView)
1061 |> render("relationship.json", %{user: follower, target: followed})
1064 {:error, :not_found}
1071 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1072 with %User{} = muted <- User.get_cached_by_id(id),
1073 {:ok, muter} <- User.mute(muter, muted) do
1075 |> put_view(AccountView)
1076 |> render("relationship.json", %{user: muter, target: muted})
1078 {:error, message} ->
1080 |> put_status(:forbidden)
1081 |> json(%{error: message})
1085 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1086 with %User{} = muted <- User.get_cached_by_id(id),
1087 {:ok, muter} <- User.unmute(muter, muted) do
1089 |> put_view(AccountView)
1090 |> render("relationship.json", %{user: muter, target: muted})
1092 {:error, message} ->
1094 |> put_status(:forbidden)
1095 |> json(%{error: message})
1099 def mutes(%{assigns: %{user: user}} = conn, _) do
1100 with muted_accounts <- User.muted_users(user) do
1101 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1106 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1107 with %User{} = blocked <- User.get_cached_by_id(id),
1108 {:ok, blocker} <- User.block(blocker, blocked),
1109 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1111 |> put_view(AccountView)
1112 |> render("relationship.json", %{user: blocker, target: blocked})
1114 {:error, message} ->
1116 |> put_status(:forbidden)
1117 |> json(%{error: message})
1121 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1122 with %User{} = blocked <- User.get_cached_by_id(id),
1123 {:ok, blocker} <- User.unblock(blocker, blocked),
1124 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1126 |> put_view(AccountView)
1127 |> render("relationship.json", %{user: blocker, target: blocked})
1129 {:error, message} ->
1131 |> put_status(:forbidden)
1132 |> json(%{error: message})
1136 def blocks(%{assigns: %{user: user}} = conn, _) do
1137 with blocked_accounts <- User.blocked_users(user) do
1138 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1143 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1144 json(conn, info.domain_blocks || [])
1147 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1148 User.block_domain(blocker, domain)
1152 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1153 User.unblock_domain(blocker, domain)
1157 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1158 with %User{} = subscription_target <- User.get_cached_by_id(id),
1159 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1161 |> put_view(AccountView)
1162 |> render("relationship.json", %{user: user, target: subscription_target})
1164 {:error, message} ->
1166 |> put_status(:forbidden)
1167 |> json(%{error: message})
1171 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1172 with %User{} = subscription_target <- User.get_cached_by_id(id),
1173 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1175 |> put_view(AccountView)
1176 |> render("relationship.json", %{user: user, target: subscription_target})
1178 {:error, message} ->
1180 |> put_status(:forbidden)
1181 |> json(%{error: message})
1185 def favourites(%{assigns: %{user: user}} = conn, params) do
1188 |> Map.put("type", "Create")
1189 |> Map.put("favorited_by", user.ap_id)
1190 |> Map.put("blocking_user", user)
1193 ActivityPub.fetch_activities([], params)
1197 |> add_link_headers(:favourites, activities)
1198 |> put_view(StatusView)
1199 |> render("index.json", %{activities: activities, for: user, as: :activity})
1202 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1203 with %User{} = user <- User.get_by_id(id),
1204 false <- user.info.hide_favorites do
1207 |> Map.put("type", "Create")
1208 |> Map.put("favorited_by", user.ap_id)
1209 |> Map.put("blocking_user", for_user)
1213 ["https://www.w3.org/ns/activitystreams#Public"] ++
1214 [for_user.ap_id | for_user.following]
1216 ["https://www.w3.org/ns/activitystreams#Public"]
1221 |> ActivityPub.fetch_activities(params)
1225 |> add_link_headers(:favourites, activities)
1226 |> put_view(StatusView)
1227 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1229 nil -> {:error, :not_found}
1230 true -> render_error(conn, :forbidden, "Can't get favorites")
1234 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1235 user = User.get_cached_by_id(user.id)
1238 Bookmark.for_user_query(user.id)
1239 |> Pagination.fetch_paginated(params)
1243 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1246 |> add_link_headers(:bookmarks, bookmarks)
1247 |> put_view(StatusView)
1248 |> render("index.json", %{activities: activities, for: user, as: :activity})
1251 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1252 lists = Pleroma.List.for_user(user, opts)
1253 res = ListView.render("lists.json", lists: lists)
1257 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1258 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1259 res = ListView.render("list.json", list: list)
1262 _e -> render_error(conn, :not_found, "Record not found")
1266 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1267 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1268 res = ListView.render("lists.json", lists: lists)
1272 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1273 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1274 {:ok, _list} <- Pleroma.List.delete(list) do
1278 json(conn, dgettext("errors", "error"))
1282 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1283 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1284 res = ListView.render("list.json", list: list)
1289 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1291 |> Enum.each(fn account_id ->
1292 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1293 %User{} = followed <- User.get_cached_by_id(account_id) do
1294 Pleroma.List.follow(list, followed)
1301 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1303 |> Enum.each(fn account_id ->
1304 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1305 %User{} = followed <- User.get_cached_by_id(account_id) do
1306 Pleroma.List.unfollow(list, followed)
1313 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1314 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1315 {:ok, users} = Pleroma.List.get_following(list) do
1317 |> put_view(AccountView)
1318 |> render("accounts.json", %{for: user, users: users, as: :user})
1322 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1323 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1324 {:ok, list} <- Pleroma.List.rename(list, title) do
1325 res = ListView.render("list.json", list: list)
1329 json(conn, dgettext("errors", "error"))
1333 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1334 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1337 |> Map.put("type", "Create")
1338 |> Map.put("blocking_user", user)
1339 |> Map.put("muting_user", user)
1341 # we must filter the following list for the user to avoid leaking statuses the user
1342 # does not actually have permission to see (for more info, peruse security issue #270).
1345 |> Enum.filter(fn x -> x in user.following end)
1346 |> ActivityPub.fetch_activities_bounded(following, params)
1350 |> put_view(StatusView)
1351 |> render("index.json", %{activities: activities, for: user, as: :activity})
1353 _e -> render_error(conn, :forbidden, "Error.")
1357 def index(%{assigns: %{user: user}} = conn, _params) do
1358 token = get_session(conn, :oauth_token)
1361 mastodon_emoji = mastodonized_emoji()
1363 limit = Config.get([:instance, :limit])
1366 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1371 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1372 access_token: token,
1374 domain: Pleroma.Web.Endpoint.host(),
1377 unfollow_modal: false,
1380 auto_play_gif: false,
1381 display_sensitive_media: false,
1382 reduce_motion: false,
1383 max_toot_chars: limit,
1384 mascot: User.get_mascot(user)["url"]
1386 poll_limits: Config.get([:instance, :poll_limits]),
1388 delete_others_notice: present?(user.info.is_moderator),
1389 admin: present?(user.info.is_admin)
1393 default_privacy: user.info.default_scope,
1394 default_sensitive: false,
1395 allow_content_types: Config.get([:instance, :allowed_post_formats])
1397 media_attachments: %{
1398 accept_content_types: [
1414 user.info.settings ||
1444 push_subscription: nil,
1446 custom_emojis: mastodon_emoji,
1452 |> put_layout(false)
1453 |> put_view(MastodonView)
1454 |> render("index.html", %{initial_state: initial_state})
1457 |> put_session(:return_to, conn.request_path)
1458 |> redirect(to: "/web/login")
1462 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1463 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1465 with changeset <- Ecto.Changeset.change(user),
1466 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1467 {:ok, _user} <- User.update_and_set_cache(changeset) do
1472 |> put_status(:internal_server_error)
1473 |> json(%{error: inspect(e)})
1477 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1478 redirect(conn, to: local_mastodon_root_path(conn))
1481 @doc "Local Mastodon FE login init action"
1482 def login(conn, %{"code" => auth_token}) do
1483 with {:ok, app} <- get_or_make_app(),
1484 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1485 {:ok, token} <- Token.exchange_token(app, auth) do
1487 |> put_session(:oauth_token, token.token)
1488 |> redirect(to: local_mastodon_root_path(conn))
1492 @doc "Local Mastodon FE callback action"
1493 def login(conn, _) do
1494 with {:ok, app} <- get_or_make_app() do
1499 response_type: "code",
1500 client_id: app.client_id,
1502 scope: Enum.join(app.scopes, " ")
1505 redirect(conn, to: path)
1509 defp local_mastodon_root_path(conn) do
1510 case get_session(conn, :return_to) do
1512 mastodon_api_path(conn, :index, ["getting-started"])
1515 delete_session(conn, :return_to)
1520 defp get_or_make_app do
1521 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1522 scopes = ["read", "write", "follow", "push"]
1524 with %App{} = app <- Repo.get_by(App, find_attrs) do
1526 if app.scopes == scopes do
1530 |> Ecto.Changeset.change(%{scopes: scopes})
1538 App.register_changeset(
1540 Map.put(find_attrs, :scopes, scopes)
1547 def logout(conn, _) do
1550 |> redirect(to: "/")
1553 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1554 Logger.debug("Unimplemented, returning unmodified relationship")
1556 with %User{} = target <- User.get_cached_by_id(id) do
1558 |> put_view(AccountView)
1559 |> render("relationship.json", %{user: user, target: target})
1563 def empty_array(conn, _) do
1564 Logger.debug("Unimplemented, returning an empty array")
1568 def empty_object(conn, _) do
1569 Logger.debug("Unimplemented, returning an empty object")
1573 def get_filters(%{assigns: %{user: user}} = conn, _) do
1574 filters = Filter.get_filters(user)
1575 res = FilterView.render("filters.json", filters: filters)
1580 %{assigns: %{user: user}} = conn,
1581 %{"phrase" => phrase, "context" => context} = params
1587 hide: Map.get(params, "irreversible", false),
1588 whole_word: Map.get(params, "boolean", true)
1592 {:ok, response} = Filter.create(query)
1593 res = FilterView.render("filter.json", filter: response)
1597 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1598 filter = Filter.get(filter_id, user)
1599 res = FilterView.render("filter.json", filter: filter)
1604 %{assigns: %{user: user}} = conn,
1605 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1609 filter_id: filter_id,
1612 hide: Map.get(params, "irreversible", nil),
1613 whole_word: Map.get(params, "boolean", true)
1617 {:ok, response} = Filter.update(query)
1618 res = FilterView.render("filter.json", filter: response)
1622 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1625 filter_id: filter_id
1628 {:ok, _} = Filter.delete(query)
1634 def errors(conn, {:error, %Changeset{} = changeset}) do
1637 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1638 |> Enum.map_join(", ", fn {_k, v} -> v end)
1641 |> put_status(:unprocessable_entity)
1642 |> json(%{error: error_message})
1645 def errors(conn, {:error, :not_found}) do
1646 render_error(conn, :not_found, "Record not found")
1649 def errors(conn, _) do
1651 |> put_status(:internal_server_error)
1652 |> json(dgettext("errors", "Something went wrong"))
1655 def suggestions(%{assigns: %{user: user}} = conn, _) do
1656 suggestions = Config.get(:suggestions)
1658 if Keyword.get(suggestions, :enabled, false) do
1659 api = Keyword.get(suggestions, :third_party_engine, "")
1660 timeout = Keyword.get(suggestions, :timeout, 5000)
1661 limit = Keyword.get(suggestions, :limit, 23)
1663 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1665 user = user.nickname
1669 |> String.replace("{{host}}", host)
1670 |> String.replace("{{user}}", user)
1672 with {:ok, %{status: 200, body: body}} <-
1677 recv_timeout: timeout,
1681 {:ok, data} <- Jason.decode(body) do
1684 |> Enum.slice(0, limit)
1689 case User.get_or_fetch(x["acct"]) do
1690 {:ok, %User{id: id}} -> id
1696 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1699 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1705 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1712 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1713 with %Activity{} = activity <- Activity.get_by_id(status_id),
1714 true <- Visibility.visible_for_user?(activity, user) do
1718 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1728 def reports(%{assigns: %{user: user}} = conn, params) do
1729 case CommonAPI.report(user, params) do
1732 |> put_view(ReportView)
1733 |> try_render("report.json", %{activity: activity})
1737 |> put_status(:bad_request)
1738 |> json(%{error: err})
1742 def account_register(
1743 %{assigns: %{app: app}} = conn,
1744 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1752 "captcha_answer_data",
1756 |> Map.put("nickname", nickname)
1757 |> Map.put("fullname", params["fullname"] || nickname)
1758 |> Map.put("bio", params["bio"] || "")
1759 |> Map.put("confirm", params["password"])
1761 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1762 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1764 token_type: "Bearer",
1765 access_token: token.token,
1767 created_at: Token.Utils.format_created_at(token)
1772 |> put_status(:bad_request)
1777 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1778 render_error(conn, :bad_request, "Missing parameters")
1781 def account_register(conn, _) do
1782 render_error(conn, :forbidden, "Invalid credentials")
1785 def conversations(%{assigns: %{user: user}} = conn, params) do
1786 participations = Participation.for_user_with_last_activity_id(user, params)
1789 Enum.map(participations, fn participation ->
1790 ConversationView.render("participation.json", %{participation: participation, user: user})
1794 |> add_link_headers(:conversations, participations)
1795 |> json(conversations)
1798 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1799 with %Participation{} = participation <-
1800 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1801 {:ok, participation} <- Participation.mark_as_read(participation) do
1802 participation_view =
1803 ConversationView.render("participation.json", %{participation: participation, user: user})
1806 |> json(participation_view)
1810 def try_render(conn, target, params)
1811 when is_binary(target) do
1812 case render(conn, target, params) do
1813 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1818 def try_render(conn, _, _) do
1819 render_error(conn, :not_implemented, "Can't display this activity")
1822 defp present?(nil), do: false
1823 defp present?(false), do: false
1824 defp present?(_), do: true