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} = params) do
1073 if Map.has_key?(params, "notifications"),
1074 do: params["notifications"] in [true, "True", "true", "1"],
1077 with %User{} = muted <- User.get_cached_by_id(id),
1078 {:ok, muter} <- User.mute(muter, muted, notifications) do
1080 |> put_view(AccountView)
1081 |> render("relationship.json", %{user: muter, target: muted})
1083 {:error, message} ->
1085 |> put_status(:forbidden)
1086 |> json(%{error: message})
1090 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1091 with %User{} = muted <- User.get_cached_by_id(id),
1092 {:ok, muter} <- User.unmute(muter, muted) do
1094 |> put_view(AccountView)
1095 |> render("relationship.json", %{user: muter, target: muted})
1097 {:error, message} ->
1099 |> put_status(:forbidden)
1100 |> json(%{error: message})
1104 def mutes(%{assigns: %{user: user}} = conn, _) do
1105 with muted_accounts <- User.muted_users(user) do
1106 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1111 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1112 with %User{} = blocked <- User.get_cached_by_id(id),
1113 {:ok, blocker} <- User.block(blocker, blocked),
1114 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1116 |> put_view(AccountView)
1117 |> render("relationship.json", %{user: blocker, target: blocked})
1119 {:error, message} ->
1121 |> put_status(:forbidden)
1122 |> json(%{error: message})
1126 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1127 with %User{} = blocked <- User.get_cached_by_id(id),
1128 {:ok, blocker} <- User.unblock(blocker, blocked),
1129 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1131 |> put_view(AccountView)
1132 |> render("relationship.json", %{user: blocker, target: blocked})
1134 {:error, message} ->
1136 |> put_status(:forbidden)
1137 |> json(%{error: message})
1141 def blocks(%{assigns: %{user: user}} = conn, _) do
1142 with blocked_accounts <- User.blocked_users(user) do
1143 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1148 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1149 json(conn, info.domain_blocks || [])
1152 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1153 User.block_domain(blocker, domain)
1157 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1158 User.unblock_domain(blocker, domain)
1162 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1163 with %User{} = subscription_target <- User.get_cached_by_id(id),
1164 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1166 |> put_view(AccountView)
1167 |> render("relationship.json", %{user: user, target: subscription_target})
1169 {:error, message} ->
1171 |> put_status(:forbidden)
1172 |> json(%{error: message})
1176 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1177 with %User{} = subscription_target <- User.get_cached_by_id(id),
1178 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1180 |> put_view(AccountView)
1181 |> render("relationship.json", %{user: user, target: subscription_target})
1183 {:error, message} ->
1185 |> put_status(:forbidden)
1186 |> json(%{error: message})
1190 def favourites(%{assigns: %{user: user}} = conn, params) do
1193 |> Map.put("type", "Create")
1194 |> Map.put("favorited_by", user.ap_id)
1195 |> Map.put("blocking_user", user)
1198 ActivityPub.fetch_activities([], params)
1202 |> add_link_headers(:favourites, activities)
1203 |> put_view(StatusView)
1204 |> render("index.json", %{activities: activities, for: user, as: :activity})
1207 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1208 with %User{} = user <- User.get_by_id(id),
1209 false <- user.info.hide_favorites do
1212 |> Map.put("type", "Create")
1213 |> Map.put("favorited_by", user.ap_id)
1214 |> Map.put("blocking_user", for_user)
1218 ["https://www.w3.org/ns/activitystreams#Public"] ++
1219 [for_user.ap_id | for_user.following]
1221 ["https://www.w3.org/ns/activitystreams#Public"]
1226 |> ActivityPub.fetch_activities(params)
1230 |> add_link_headers(:favourites, activities)
1231 |> put_view(StatusView)
1232 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1234 nil -> {:error, :not_found}
1235 true -> render_error(conn, :forbidden, "Can't get favorites")
1239 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1240 user = User.get_cached_by_id(user.id)
1243 Bookmark.for_user_query(user.id)
1244 |> Pagination.fetch_paginated(params)
1248 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1251 |> add_link_headers(:bookmarks, bookmarks)
1252 |> put_view(StatusView)
1253 |> render("index.json", %{activities: activities, for: user, as: :activity})
1256 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1257 lists = Pleroma.List.for_user(user, opts)
1258 res = ListView.render("lists.json", lists: lists)
1262 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1263 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1264 res = ListView.render("list.json", list: list)
1267 _e -> render_error(conn, :not_found, "Record not found")
1271 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1272 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1273 res = ListView.render("lists.json", lists: lists)
1277 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1278 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1279 {:ok, _list} <- Pleroma.List.delete(list) do
1283 json(conn, dgettext("errors", "error"))
1287 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1288 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1289 res = ListView.render("list.json", list: list)
1294 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1296 |> Enum.each(fn account_id ->
1297 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1298 %User{} = followed <- User.get_cached_by_id(account_id) do
1299 Pleroma.List.follow(list, followed)
1306 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1308 |> Enum.each(fn account_id ->
1309 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1310 %User{} = followed <- User.get_cached_by_id(account_id) do
1311 Pleroma.List.unfollow(list, followed)
1318 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1319 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1320 {:ok, users} = Pleroma.List.get_following(list) do
1322 |> put_view(AccountView)
1323 |> render("accounts.json", %{for: user, users: users, as: :user})
1327 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1328 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1329 {:ok, list} <- Pleroma.List.rename(list, title) do
1330 res = ListView.render("list.json", list: list)
1334 json(conn, dgettext("errors", "error"))
1338 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1339 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1342 |> Map.put("type", "Create")
1343 |> Map.put("blocking_user", user)
1344 |> Map.put("muting_user", user)
1346 # we must filter the following list for the user to avoid leaking statuses the user
1347 # does not actually have permission to see (for more info, peruse security issue #270).
1350 |> Enum.filter(fn x -> x in user.following end)
1351 |> ActivityPub.fetch_activities_bounded(following, params)
1355 |> put_view(StatusView)
1356 |> render("index.json", %{activities: activities, for: user, as: :activity})
1358 _e -> render_error(conn, :forbidden, "Error.")
1362 def index(%{assigns: %{user: user}} = conn, _params) do
1363 token = get_session(conn, :oauth_token)
1366 mastodon_emoji = mastodonized_emoji()
1368 limit = Config.get([:instance, :limit])
1371 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1376 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1377 access_token: token,
1379 domain: Pleroma.Web.Endpoint.host(),
1382 unfollow_modal: false,
1385 auto_play_gif: false,
1386 display_sensitive_media: false,
1387 reduce_motion: false,
1388 max_toot_chars: limit,
1389 mascot: User.get_mascot(user)["url"]
1391 poll_limits: Config.get([:instance, :poll_limits]),
1393 delete_others_notice: present?(user.info.is_moderator),
1394 admin: present?(user.info.is_admin)
1398 default_privacy: user.info.default_scope,
1399 default_sensitive: false,
1400 allow_content_types: Config.get([:instance, :allowed_post_formats])
1402 media_attachments: %{
1403 accept_content_types: [
1419 user.info.settings ||
1449 push_subscription: nil,
1451 custom_emojis: mastodon_emoji,
1457 |> put_layout(false)
1458 |> put_view(MastodonView)
1459 |> render("index.html", %{initial_state: initial_state})
1462 |> put_session(:return_to, conn.request_path)
1463 |> redirect(to: "/web/login")
1467 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1468 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1470 with changeset <- Ecto.Changeset.change(user),
1471 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1472 {:ok, _user} <- User.update_and_set_cache(changeset) do
1477 |> put_status(:internal_server_error)
1478 |> json(%{error: inspect(e)})
1482 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1483 redirect(conn, to: local_mastodon_root_path(conn))
1486 @doc "Local Mastodon FE login init action"
1487 def login(conn, %{"code" => auth_token}) do
1488 with {:ok, app} <- get_or_make_app(),
1489 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1490 {:ok, token} <- Token.exchange_token(app, auth) do
1492 |> put_session(:oauth_token, token.token)
1493 |> redirect(to: local_mastodon_root_path(conn))
1497 @doc "Local Mastodon FE callback action"
1498 def login(conn, _) do
1499 with {:ok, app} <- get_or_make_app() do
1504 response_type: "code",
1505 client_id: app.client_id,
1507 scope: Enum.join(app.scopes, " ")
1510 redirect(conn, to: path)
1514 defp local_mastodon_root_path(conn) do
1515 case get_session(conn, :return_to) do
1517 mastodon_api_path(conn, :index, ["getting-started"])
1520 delete_session(conn, :return_to)
1525 defp get_or_make_app do
1526 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1527 scopes = ["read", "write", "follow", "push"]
1529 with %App{} = app <- Repo.get_by(App, find_attrs) do
1531 if app.scopes == scopes do
1535 |> Ecto.Changeset.change(%{scopes: scopes})
1543 App.register_changeset(
1545 Map.put(find_attrs, :scopes, scopes)
1552 def logout(conn, _) do
1555 |> redirect(to: "/")
1558 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1559 Logger.debug("Unimplemented, returning unmodified relationship")
1561 with %User{} = target <- User.get_cached_by_id(id) do
1563 |> put_view(AccountView)
1564 |> render("relationship.json", %{user: user, target: target})
1568 def empty_array(conn, _) do
1569 Logger.debug("Unimplemented, returning an empty array")
1573 def empty_object(conn, _) do
1574 Logger.debug("Unimplemented, returning an empty object")
1578 def get_filters(%{assigns: %{user: user}} = conn, _) do
1579 filters = Filter.get_filters(user)
1580 res = FilterView.render("filters.json", filters: filters)
1585 %{assigns: %{user: user}} = conn,
1586 %{"phrase" => phrase, "context" => context} = params
1592 hide: Map.get(params, "irreversible", false),
1593 whole_word: Map.get(params, "boolean", true)
1597 {:ok, response} = Filter.create(query)
1598 res = FilterView.render("filter.json", filter: response)
1602 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1603 filter = Filter.get(filter_id, user)
1604 res = FilterView.render("filter.json", filter: filter)
1609 %{assigns: %{user: user}} = conn,
1610 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1614 filter_id: filter_id,
1617 hide: Map.get(params, "irreversible", nil),
1618 whole_word: Map.get(params, "boolean", true)
1622 {:ok, response} = Filter.update(query)
1623 res = FilterView.render("filter.json", filter: response)
1627 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1630 filter_id: filter_id
1633 {:ok, _} = Filter.delete(query)
1639 def errors(conn, {:error, %Changeset{} = changeset}) do
1642 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1643 |> Enum.map_join(", ", fn {_k, v} -> v end)
1646 |> put_status(:unprocessable_entity)
1647 |> json(%{error: error_message})
1650 def errors(conn, {:error, :not_found}) do
1651 render_error(conn, :not_found, "Record not found")
1654 def errors(conn, _) do
1656 |> put_status(:internal_server_error)
1657 |> json(dgettext("errors", "Something went wrong"))
1660 def suggestions(%{assigns: %{user: user}} = conn, _) do
1661 suggestions = Config.get(:suggestions)
1663 if Keyword.get(suggestions, :enabled, false) do
1664 api = Keyword.get(suggestions, :third_party_engine, "")
1665 timeout = Keyword.get(suggestions, :timeout, 5000)
1666 limit = Keyword.get(suggestions, :limit, 23)
1668 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1670 user = user.nickname
1674 |> String.replace("{{host}}", host)
1675 |> String.replace("{{user}}", user)
1677 with {:ok, %{status: 200, body: body}} <-
1682 recv_timeout: timeout,
1686 {:ok, data} <- Jason.decode(body) do
1689 |> Enum.slice(0, limit)
1694 case User.get_or_fetch(x["acct"]) do
1695 {:ok, %User{id: id}} -> id
1701 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1704 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1710 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1717 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1718 with %Activity{} = activity <- Activity.get_by_id(status_id),
1719 true <- Visibility.visible_for_user?(activity, user) do
1723 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1733 def reports(%{assigns: %{user: user}} = conn, params) do
1734 case CommonAPI.report(user, params) do
1737 |> put_view(ReportView)
1738 |> try_render("report.json", %{activity: activity})
1742 |> put_status(:bad_request)
1743 |> json(%{error: err})
1747 def account_register(
1748 %{assigns: %{app: app}} = conn,
1749 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1757 "captcha_answer_data",
1761 |> Map.put("nickname", nickname)
1762 |> Map.put("fullname", params["fullname"] || nickname)
1763 |> Map.put("bio", params["bio"] || "")
1764 |> Map.put("confirm", params["password"])
1766 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1767 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1769 token_type: "Bearer",
1770 access_token: token.token,
1772 created_at: Token.Utils.format_created_at(token)
1777 |> put_status(:bad_request)
1782 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1783 render_error(conn, :bad_request, "Missing parameters")
1786 def account_register(conn, _) do
1787 render_error(conn, :forbidden, "Invalid credentials")
1790 def conversations(%{assigns: %{user: user}} = conn, params) do
1791 participations = Participation.for_user_with_last_activity_id(user, params)
1794 Enum.map(participations, fn participation ->
1795 ConversationView.render("participation.json", %{participation: participation, user: user})
1799 |> add_link_headers(:conversations, participations)
1800 |> json(conversations)
1803 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1804 with %Participation{} = participation <-
1805 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1806 {:ok, participation} <- Participation.mark_as_read(participation) do
1807 participation_view =
1808 ConversationView.render("participation.json", %{participation: participation, user: user})
1811 |> json(participation_view)
1815 def try_render(conn, target, params)
1816 when is_binary(target) do
1817 case render(conn, target, params) do
1818 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1823 def try_render(conn, _, _) do
1824 render_error(conn, :not_implemented, "Can't display this activity")
1827 defp present?(nil), do: false
1828 defp present?(false), do: false
1829 defp present?(_), do: true