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 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
700 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
702 |> put_view(StatusView)
703 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
707 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
708 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
709 %User{} = user <- User.get_cached_by_nickname(user.nickname),
710 true <- Visibility.visible_for_user?(activity, user),
711 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
713 |> put_view(StatusView)
714 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
718 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
719 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
720 %User{} = user <- User.get_cached_by_nickname(user.nickname),
721 true <- Visibility.visible_for_user?(activity, user),
722 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
724 |> put_view(StatusView)
725 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
729 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
730 activity = Activity.get_by_id(id)
732 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
734 |> put_view(StatusView)
735 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
739 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
740 activity = Activity.get_by_id(id)
742 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
744 |> put_view(StatusView)
745 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
749 def notifications(%{assigns: %{user: user}} = conn, params) do
750 notifications = MastodonAPI.get_notifications(user, params)
753 |> add_link_headers(:notifications, notifications)
754 |> put_view(NotificationView)
755 |> render("index.json", %{notifications: notifications, for: user})
758 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
759 with {:ok, notification} <- Notification.get(user, id) do
761 |> put_view(NotificationView)
762 |> render("show.json", %{notification: notification, for: user})
766 |> put_status(:forbidden)
767 |> json(%{"error" => reason})
771 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
772 Notification.clear(user)
776 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
777 with {:ok, _notif} <- Notification.dismiss(user, id) do
782 |> put_status(:forbidden)
783 |> json(%{"error" => reason})
787 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
788 Notification.destroy_multiple(user, ids)
792 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
794 q = from(u in User, where: u.id in ^id)
795 targets = Repo.all(q)
798 |> put_view(AccountView)
799 |> render("relationships.json", %{user: user, targets: targets})
802 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
803 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
805 def update_media(%{assigns: %{user: user}} = conn, data) do
806 with %Object{} = object <- Repo.get(Object, data["id"]),
807 true <- Object.authorize_mutation(object, user),
808 true <- is_binary(data["description"]),
809 description <- data["description"] do
810 new_data = %{object.data | "name" => description}
814 |> Object.change(%{data: new_data})
817 attachment_data = Map.put(new_data, "id", object.id)
820 |> put_view(StatusView)
821 |> render("attachment.json", %{attachment: attachment_data})
825 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
826 with {:ok, object} <-
829 actor: User.ap_id(user),
830 description: Map.get(data, "description")
832 attachment_data = Map.put(object.data, "id", object.id)
835 |> put_view(StatusView)
836 |> render("attachment.json", %{attachment: attachment_data})
840 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
841 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
842 %{} = attachment_data <- Map.put(object.data, "id", object.id),
843 %{type: type} = rendered <-
844 StatusView.render("attachment.json", %{attachment: attachment_data}) do
845 # Reject if not an image
846 if type == "image" do
848 # Save to the user's info
849 info_changeset = User.Info.mascot_update(user.info, rendered)
853 |> Ecto.Changeset.change()
854 |> Ecto.Changeset.put_embed(:info, info_changeset)
856 {:ok, _user} = User.update_and_set_cache(user_changeset)
861 render_error(conn, :unsupported_media_type, "mascots can only be images")
866 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
867 mascot = User.get_mascot(user)
873 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
874 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
875 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
876 q = from(u in User, where: u.ap_id in ^likes)
880 |> put_view(AccountView)
881 |> render("accounts.json", %{for: user, users: users, as: :user})
887 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
888 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
889 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
890 q = from(u in User, where: u.ap_id in ^announces)
894 |> put_view(AccountView)
895 |> render("accounts.json", %{for: user, users: users, as: :user})
901 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
902 local_only = params["local"] in [true, "True", "true", "1"]
905 [params["tag"], params["any"]]
909 |> Enum.map(&String.downcase(&1))
914 |> Enum.map(&String.downcase(&1))
919 |> Enum.map(&String.downcase(&1))
923 |> Map.put("type", "Create")
924 |> Map.put("local_only", local_only)
925 |> Map.put("blocking_user", user)
926 |> Map.put("muting_user", user)
927 |> Map.put("tag", tags)
928 |> Map.put("tag_all", tag_all)
929 |> Map.put("tag_reject", tag_reject)
930 |> ActivityPub.fetch_public_activities()
934 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
935 |> put_view(StatusView)
936 |> render("index.json", %{activities: activities, for: user, as: :activity})
939 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
940 with %User{} = user <- User.get_cached_by_id(id),
941 followers <- MastodonAPI.get_followers(user, params) do
944 for_user && user.id == for_user.id -> followers
945 user.info.hide_followers -> []
950 |> add_link_headers(:followers, followers, user)
951 |> put_view(AccountView)
952 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
956 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
957 with %User{} = user <- User.get_cached_by_id(id),
958 followers <- MastodonAPI.get_friends(user, params) do
961 for_user && user.id == for_user.id -> followers
962 user.info.hide_follows -> []
967 |> add_link_headers(:following, followers, user)
968 |> put_view(AccountView)
969 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
973 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
974 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
976 |> put_view(AccountView)
977 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
981 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
982 with %User{} = follower <- User.get_cached_by_id(id),
983 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
985 |> put_view(AccountView)
986 |> render("relationship.json", %{user: followed, target: follower})
990 |> put_status(:forbidden)
991 |> json(%{error: message})
995 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
996 with %User{} = follower <- User.get_cached_by_id(id),
997 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
999 |> put_view(AccountView)
1000 |> render("relationship.json", %{user: followed, target: follower})
1002 {:error, message} ->
1004 |> put_status(:forbidden)
1005 |> json(%{error: message})
1009 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1010 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1011 {_, true} <- {:followed, follower.id != followed.id},
1012 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1014 |> put_view(AccountView)
1015 |> render("relationship.json", %{user: follower, target: followed})
1018 {:error, :not_found}
1020 {:error, message} ->
1022 |> put_status(:forbidden)
1023 |> json(%{error: message})
1027 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1028 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1029 {_, true} <- {:followed, follower.id != followed.id},
1030 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1032 |> put_view(AccountView)
1033 |> render("account.json", %{user: followed, for: follower})
1036 {:error, :not_found}
1038 {:error, message} ->
1040 |> put_status(:forbidden)
1041 |> json(%{error: message})
1045 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1046 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1047 {_, true} <- {:followed, follower.id != followed.id},
1048 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1050 |> put_view(AccountView)
1051 |> render("relationship.json", %{user: follower, target: followed})
1054 {:error, :not_found}
1061 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1063 if Map.has_key?(params, "notifications"),
1064 do: params["notifications"] in [true, "True", "true", "1"],
1067 with %User{} = muted <- User.get_cached_by_id(id),
1068 {:ok, muter} <- User.mute(muter, muted, notifications) do
1070 |> put_view(AccountView)
1071 |> render("relationship.json", %{user: muter, target: muted})
1073 {:error, message} ->
1075 |> put_status(:forbidden)
1076 |> json(%{error: message})
1080 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1081 with %User{} = muted <- User.get_cached_by_id(id),
1082 {:ok, muter} <- User.unmute(muter, muted) do
1084 |> put_view(AccountView)
1085 |> render("relationship.json", %{user: muter, target: muted})
1087 {:error, message} ->
1089 |> put_status(:forbidden)
1090 |> json(%{error: message})
1094 def mutes(%{assigns: %{user: user}} = conn, _) do
1095 with muted_accounts <- User.muted_users(user) do
1096 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1101 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1102 with %User{} = blocked <- User.get_cached_by_id(id),
1103 {:ok, blocker} <- User.block(blocker, blocked),
1104 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1106 |> put_view(AccountView)
1107 |> render("relationship.json", %{user: blocker, target: blocked})
1109 {:error, message} ->
1111 |> put_status(:forbidden)
1112 |> json(%{error: message})
1116 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1117 with %User{} = blocked <- User.get_cached_by_id(id),
1118 {:ok, blocker} <- User.unblock(blocker, blocked),
1119 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1121 |> put_view(AccountView)
1122 |> render("relationship.json", %{user: blocker, target: blocked})
1124 {:error, message} ->
1126 |> put_status(:forbidden)
1127 |> json(%{error: message})
1131 def blocks(%{assigns: %{user: user}} = conn, _) do
1132 with blocked_accounts <- User.blocked_users(user) do
1133 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1138 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1139 json(conn, info.domain_blocks || [])
1142 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1143 User.block_domain(blocker, domain)
1147 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1148 User.unblock_domain(blocker, domain)
1152 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1153 with %User{} = subscription_target <- User.get_cached_by_id(id),
1154 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1156 |> put_view(AccountView)
1157 |> render("relationship.json", %{user: user, target: subscription_target})
1159 {:error, message} ->
1161 |> put_status(:forbidden)
1162 |> json(%{error: message})
1166 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1167 with %User{} = subscription_target <- User.get_cached_by_id(id),
1168 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1170 |> put_view(AccountView)
1171 |> render("relationship.json", %{user: user, target: subscription_target})
1173 {:error, message} ->
1175 |> put_status(:forbidden)
1176 |> json(%{error: message})
1180 def favourites(%{assigns: %{user: user}} = conn, params) do
1183 |> Map.put("type", "Create")
1184 |> Map.put("favorited_by", user.ap_id)
1185 |> Map.put("blocking_user", user)
1188 ActivityPub.fetch_activities([], params)
1192 |> add_link_headers(:favourites, activities)
1193 |> put_view(StatusView)
1194 |> render("index.json", %{activities: activities, for: user, as: :activity})
1197 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1198 with %User{} = user <- User.get_by_id(id),
1199 false <- user.info.hide_favorites do
1202 |> Map.put("type", "Create")
1203 |> Map.put("favorited_by", user.ap_id)
1204 |> Map.put("blocking_user", for_user)
1208 ["https://www.w3.org/ns/activitystreams#Public"] ++
1209 [for_user.ap_id | for_user.following]
1211 ["https://www.w3.org/ns/activitystreams#Public"]
1216 |> ActivityPub.fetch_activities(params)
1220 |> add_link_headers(:favourites, activities)
1221 |> put_view(StatusView)
1222 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1224 nil -> {:error, :not_found}
1225 true -> render_error(conn, :forbidden, "Can't get favorites")
1229 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1230 user = User.get_cached_by_id(user.id)
1233 Bookmark.for_user_query(user.id)
1234 |> Pagination.fetch_paginated(params)
1238 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1241 |> add_link_headers(:bookmarks, bookmarks)
1242 |> put_view(StatusView)
1243 |> render("index.json", %{activities: activities, for: user, as: :activity})
1246 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1247 lists = Pleroma.List.for_user(user, opts)
1248 res = ListView.render("lists.json", lists: lists)
1252 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1253 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1254 res = ListView.render("list.json", list: list)
1257 _e -> render_error(conn, :not_found, "Record not found")
1261 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1262 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1263 res = ListView.render("lists.json", lists: lists)
1267 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1268 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1269 {:ok, _list} <- Pleroma.List.delete(list) do
1273 json(conn, dgettext("errors", "error"))
1277 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1278 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1279 res = ListView.render("list.json", list: list)
1284 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1286 |> Enum.each(fn account_id ->
1287 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1288 %User{} = followed <- User.get_cached_by_id(account_id) do
1289 Pleroma.List.follow(list, followed)
1296 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1298 |> Enum.each(fn account_id ->
1299 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1300 %User{} = followed <- User.get_cached_by_id(account_id) do
1301 Pleroma.List.unfollow(list, followed)
1308 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1309 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1310 {:ok, users} = Pleroma.List.get_following(list) do
1312 |> put_view(AccountView)
1313 |> render("accounts.json", %{for: user, users: users, as: :user})
1317 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1318 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1319 {:ok, list} <- Pleroma.List.rename(list, title) do
1320 res = ListView.render("list.json", list: list)
1324 json(conn, dgettext("errors", "error"))
1328 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1329 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1332 |> Map.put("type", "Create")
1333 |> Map.put("blocking_user", user)
1334 |> Map.put("muting_user", user)
1336 # we must filter the following list for the user to avoid leaking statuses the user
1337 # does not actually have permission to see (for more info, peruse security issue #270).
1340 |> Enum.filter(fn x -> x in user.following end)
1341 |> ActivityPub.fetch_activities_bounded(following, params)
1345 |> put_view(StatusView)
1346 |> render("index.json", %{activities: activities, for: user, as: :activity})
1348 _e -> render_error(conn, :forbidden, "Error.")
1352 def index(%{assigns: %{user: user}} = conn, _params) do
1353 token = get_session(conn, :oauth_token)
1356 mastodon_emoji = mastodonized_emoji()
1358 limit = Config.get([:instance, :limit])
1361 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1366 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1367 access_token: token,
1369 domain: Pleroma.Web.Endpoint.host(),
1372 unfollow_modal: false,
1375 auto_play_gif: false,
1376 display_sensitive_media: false,
1377 reduce_motion: false,
1378 max_toot_chars: limit,
1379 mascot: User.get_mascot(user)["url"]
1381 poll_limits: Config.get([:instance, :poll_limits]),
1383 delete_others_notice: present?(user.info.is_moderator),
1384 admin: present?(user.info.is_admin)
1388 default_privacy: user.info.default_scope,
1389 default_sensitive: false,
1390 allow_content_types: Config.get([:instance, :allowed_post_formats])
1392 media_attachments: %{
1393 accept_content_types: [
1409 user.info.settings ||
1439 push_subscription: nil,
1441 custom_emojis: mastodon_emoji,
1447 |> put_layout(false)
1448 |> put_view(MastodonView)
1449 |> render("index.html", %{initial_state: initial_state})
1452 |> put_session(:return_to, conn.request_path)
1453 |> redirect(to: "/web/login")
1457 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1458 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1460 with changeset <- Ecto.Changeset.change(user),
1461 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1462 {:ok, _user} <- User.update_and_set_cache(changeset) do
1467 |> put_status(:internal_server_error)
1468 |> json(%{error: inspect(e)})
1472 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1473 redirect(conn, to: local_mastodon_root_path(conn))
1476 @doc "Local Mastodon FE login init action"
1477 def login(conn, %{"code" => auth_token}) do
1478 with {:ok, app} <- get_or_make_app(),
1479 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1480 {:ok, token} <- Token.exchange_token(app, auth) do
1482 |> put_session(:oauth_token, token.token)
1483 |> redirect(to: local_mastodon_root_path(conn))
1487 @doc "Local Mastodon FE callback action"
1488 def login(conn, _) do
1489 with {:ok, app} <- get_or_make_app() do
1494 response_type: "code",
1495 client_id: app.client_id,
1497 scope: Enum.join(app.scopes, " ")
1500 redirect(conn, to: path)
1504 defp local_mastodon_root_path(conn) do
1505 case get_session(conn, :return_to) do
1507 mastodon_api_path(conn, :index, ["getting-started"])
1510 delete_session(conn, :return_to)
1515 defp get_or_make_app do
1516 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1517 scopes = ["read", "write", "follow", "push"]
1519 with %App{} = app <- Repo.get_by(App, find_attrs) do
1521 if app.scopes == scopes do
1525 |> Ecto.Changeset.change(%{scopes: scopes})
1533 App.register_changeset(
1535 Map.put(find_attrs, :scopes, scopes)
1542 def logout(conn, _) do
1545 |> redirect(to: "/")
1548 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1549 Logger.debug("Unimplemented, returning unmodified relationship")
1551 with %User{} = target <- User.get_cached_by_id(id) do
1553 |> put_view(AccountView)
1554 |> render("relationship.json", %{user: user, target: target})
1558 def empty_array(conn, _) do
1559 Logger.debug("Unimplemented, returning an empty array")
1563 def empty_object(conn, _) do
1564 Logger.debug("Unimplemented, returning an empty object")
1568 def get_filters(%{assigns: %{user: user}} = conn, _) do
1569 filters = Filter.get_filters(user)
1570 res = FilterView.render("filters.json", filters: filters)
1575 %{assigns: %{user: user}} = conn,
1576 %{"phrase" => phrase, "context" => context} = params
1582 hide: Map.get(params, "irreversible", false),
1583 whole_word: Map.get(params, "boolean", true)
1587 {:ok, response} = Filter.create(query)
1588 res = FilterView.render("filter.json", filter: response)
1592 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1593 filter = Filter.get(filter_id, user)
1594 res = FilterView.render("filter.json", filter: filter)
1599 %{assigns: %{user: user}} = conn,
1600 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1604 filter_id: filter_id,
1607 hide: Map.get(params, "irreversible", nil),
1608 whole_word: Map.get(params, "boolean", true)
1612 {:ok, response} = Filter.update(query)
1613 res = FilterView.render("filter.json", filter: response)
1617 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1620 filter_id: filter_id
1623 {:ok, _} = Filter.delete(query)
1629 def errors(conn, {:error, %Changeset{} = changeset}) do
1632 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1633 |> Enum.map_join(", ", fn {_k, v} -> v end)
1636 |> put_status(:unprocessable_entity)
1637 |> json(%{error: error_message})
1640 def errors(conn, {:error, :not_found}) do
1641 render_error(conn, :not_found, "Record not found")
1644 def errors(conn, {:error, error_message}) do
1646 |> put_status(:bad_request)
1647 |> json(%{error: error_message})
1650 def errors(conn, _) do
1652 |> put_status(:internal_server_error)
1653 |> json(dgettext("errors", "Something went wrong"))
1656 def suggestions(%{assigns: %{user: user}} = conn, _) do
1657 suggestions = Config.get(:suggestions)
1659 if Keyword.get(suggestions, :enabled, false) do
1660 api = Keyword.get(suggestions, :third_party_engine, "")
1661 timeout = Keyword.get(suggestions, :timeout, 5000)
1662 limit = Keyword.get(suggestions, :limit, 23)
1664 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1666 user = user.nickname
1670 |> String.replace("{{host}}", host)
1671 |> String.replace("{{user}}", user)
1673 with {:ok, %{status: 200, body: body}} <-
1678 recv_timeout: timeout,
1682 {:ok, data} <- Jason.decode(body) do
1685 |> Enum.slice(0, limit)
1690 case User.get_or_fetch(x["acct"]) do
1691 {:ok, %User{id: id}} -> id
1697 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1700 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1706 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1713 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1714 with %Activity{} = activity <- Activity.get_by_id(status_id),
1715 true <- Visibility.visible_for_user?(activity, user) do
1719 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1729 def reports(%{assigns: %{user: user}} = conn, params) do
1730 case CommonAPI.report(user, params) do
1733 |> put_view(ReportView)
1734 |> try_render("report.json", %{activity: activity})
1738 |> put_status(:bad_request)
1739 |> json(%{error: err})
1743 def account_register(
1744 %{assigns: %{app: app}} = conn,
1745 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1753 "captcha_answer_data",
1757 |> Map.put("nickname", nickname)
1758 |> Map.put("fullname", params["fullname"] || nickname)
1759 |> Map.put("bio", params["bio"] || "")
1760 |> Map.put("confirm", params["password"])
1762 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1763 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1765 token_type: "Bearer",
1766 access_token: token.token,
1768 created_at: Token.Utils.format_created_at(token)
1773 |> put_status(:bad_request)
1778 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1779 render_error(conn, :bad_request, "Missing parameters")
1782 def account_register(conn, _) do
1783 render_error(conn, :forbidden, "Invalid credentials")
1786 def conversations(%{assigns: %{user: user}} = conn, params) do
1787 participations = Participation.for_user_with_last_activity_id(user, params)
1790 Enum.map(participations, fn participation ->
1791 ConversationView.render("participation.json", %{participation: participation, user: user})
1795 |> add_link_headers(:conversations, participations)
1796 |> json(conversations)
1799 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1800 with %Participation{} = participation <-
1801 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1802 {:ok, participation} <- Participation.mark_as_read(participation) do
1803 participation_view =
1804 ConversationView.render("participation.json", %{participation: participation, user: user})
1807 |> json(participation_view)
1811 def try_render(conn, target, params)
1812 when is_binary(target) do
1813 case render(conn, target, params) do
1814 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1819 def try_render(conn, _, _) do
1820 render_error(conn, :not_implemented, "Can't display this activity")
1823 defp present?(nil), do: false
1824 defp present?(false), do: false
1825 defp present?(_), do: true