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_crud_actions ~w(post_status delete_status)a
51 @rate_limited_status_reactions ~w(reblog_status unreblog_status fav_status unfav_status)a
52 @rate_limited_status_actions @rate_limited_status_crud_actions ++ @rate_limited_status_reactions
56 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
57 when action in ~w(reblog_status unreblog_status)a
62 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
63 when action in ~w(fav_status unfav_status)a
66 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
67 plug(RateLimiter, :app_account_creation when action == :account_register)
68 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
70 @local_mastodon_name "Mastodon-Local"
72 action_fallback(:errors)
74 def create_app(conn, params) do
75 scopes = Scopes.fetch_scopes(params, ["read"])
79 |> Map.drop(["scope", "scopes"])
80 |> Map.put("scopes", scopes)
82 with cs <- App.register_changeset(%App{}, app_attrs),
83 false <- cs.changes[:client_name] == @local_mastodon_name,
84 {:ok, app} <- Repo.insert(cs) do
87 |> render("show.json", %{app: app})
96 value_function \\ fn x -> {:ok, x} end
98 if Map.has_key?(params, params_field) do
99 case value_function.(params[params_field]) do
100 {:ok, new_value} -> Map.put(map, map_field, new_value)
108 def update_credentials(%{assigns: %{user: user}} = conn, params) do
113 |> add_if_present(params, "display_name", :name)
114 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
115 |> add_if_present(params, "avatar", :avatar, fn value ->
116 with %Plug.Upload{} <- value,
117 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
124 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
127 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
138 :skip_thread_containment
140 |> Enum.reduce(%{}, fn key, acc ->
141 add_if_present(acc, params, to_string(key), key, fn value ->
142 {:ok, ControllerHelper.truthy_param?(value)}
145 |> add_if_present(params, "default_scope", :default_scope)
146 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
147 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
149 |> add_if_present(params, "header", :banner, fn value ->
150 with %Plug.Upload{} <- value,
151 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
157 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
158 with %Plug.Upload{} <- value,
159 {:ok, object} <- ActivityPub.upload(value, type: :background) do
165 |> Map.put(:emoji, user_info_emojis)
167 info_cng = User.Info.profile_update(user.info, info_params)
169 with changeset <- User.update_changeset(user, user_params),
170 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
171 {:ok, user} <- User.update_and_set_cache(changeset) do
172 if original_user != user do
173 CommonAPI.update(user)
178 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
181 _e -> render_error(conn, :forbidden, "Invalid request")
185 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
186 change = Changeset.change(user, %{avatar: nil})
187 {:ok, user} = User.update_and_set_cache(change)
188 CommonAPI.update(user)
190 json(conn, %{url: nil})
193 def update_avatar(%{assigns: %{user: user}} = conn, params) do
194 {:ok, object} = ActivityPub.upload(params, type: :avatar)
195 change = Changeset.change(user, %{avatar: object.data})
196 {:ok, user} = User.update_and_set_cache(change)
197 CommonAPI.update(user)
198 %{"url" => [%{"href" => href} | _]} = object.data
200 json(conn, %{url: href})
203 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
204 with new_info <- %{"banner" => %{}},
205 info_cng <- User.Info.profile_update(user.info, new_info),
206 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
207 {:ok, user} <- User.update_and_set_cache(changeset) do
208 CommonAPI.update(user)
210 json(conn, %{url: nil})
214 def update_banner(%{assigns: %{user: user}} = conn, params) do
215 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
216 new_info <- %{"banner" => object.data},
217 info_cng <- User.Info.profile_update(user.info, new_info),
218 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
219 {:ok, user} <- User.update_and_set_cache(changeset) do
220 CommonAPI.update(user)
221 %{"url" => [%{"href" => href} | _]} = object.data
223 json(conn, %{url: href})
227 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
228 with new_info <- %{"background" => %{}},
229 info_cng <- User.Info.profile_update(user.info, new_info),
230 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
231 {:ok, _user} <- User.update_and_set_cache(changeset) do
232 json(conn, %{url: nil})
236 def update_background(%{assigns: %{user: user}} = conn, params) do
237 with {:ok, object} <- ActivityPub.upload(params, type: :background),
238 new_info <- %{"background" => object.data},
239 info_cng <- User.Info.profile_update(user.info, new_info),
240 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
241 {:ok, _user} <- User.update_and_set_cache(changeset) do
242 %{"url" => [%{"href" => href} | _]} = object.data
244 json(conn, %{url: href})
248 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
249 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
252 AccountView.render("account.json", %{
255 with_pleroma_settings: true,
256 with_chat_token: chat_token
262 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
263 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
266 |> render("short.json", %{app: app})
270 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
271 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
272 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
273 account = AccountView.render("account.json", %{user: user, for: for_user})
276 _e -> render_error(conn, :not_found, "Can't find user")
280 @mastodon_api_level "2.7.2"
282 def masto_instance(conn, _params) do
283 instance = Config.get(:instance)
287 title: Keyword.get(instance, :name),
288 description: Keyword.get(instance, :description),
289 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
290 email: Keyword.get(instance, :email),
292 streaming_api: Pleroma.Web.Endpoint.websocket_url()
294 stats: Stats.get_stats(),
295 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
297 registrations: Pleroma.Config.get([:instance, :registrations_open]),
298 # Extra (not present in Mastodon):
299 max_toot_chars: Keyword.get(instance, :limit),
300 poll_limits: Keyword.get(instance, :poll_limits)
306 def peers(conn, _params) do
307 json(conn, Stats.get_peers())
310 defp mastodonized_emoji do
311 Pleroma.Emoji.get_all()
312 |> Enum.map(fn {shortcode, relative_url, tags} ->
313 url = to_string(URI.merge(Web.base_url(), relative_url))
316 "shortcode" => shortcode,
318 "visible_in_picker" => true,
321 # Assuming that a comma is authorized in the category name
322 "category" => (tags -- ["Custom"]) |> Enum.join(",")
327 def custom_emojis(conn, _params) do
328 mastodon_emoji = mastodonized_emoji()
329 json(conn, mastodon_emoji)
332 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
335 |> Map.drop(["since_id", "max_id", "min_id"])
338 last = List.last(activities)
345 |> Map.get("limit", "20")
346 |> String.to_integer()
349 if length(activities) <= limit do
355 |> Enum.at(limit * -1)
359 {next_url, prev_url} =
363 Pleroma.Web.Endpoint,
366 Map.merge(params, %{max_id: max_id})
369 Pleroma.Web.Endpoint,
372 Map.merge(params, %{min_id: min_id})
378 Pleroma.Web.Endpoint,
380 Map.merge(params, %{max_id: max_id})
383 Pleroma.Web.Endpoint,
385 Map.merge(params, %{min_id: min_id})
391 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
397 def home_timeline(%{assigns: %{user: user}} = conn, params) do
400 |> Map.put("type", ["Create", "Announce"])
401 |> Map.put("blocking_user", user)
402 |> Map.put("muting_user", user)
403 |> Map.put("user", user)
406 [user.ap_id | user.following]
407 |> ActivityPub.fetch_activities(params)
411 |> add_link_headers(:home_timeline, activities)
412 |> put_view(StatusView)
413 |> render("index.json", %{activities: activities, for: user, as: :activity})
416 def public_timeline(%{assigns: %{user: user}} = conn, params) do
417 local_only = params["local"] in [true, "True", "true", "1"]
421 |> Map.put("type", ["Create", "Announce"])
422 |> Map.put("local_only", local_only)
423 |> Map.put("blocking_user", user)
424 |> Map.put("muting_user", user)
425 |> ActivityPub.fetch_public_activities()
429 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
430 |> put_view(StatusView)
431 |> render("index.json", %{activities: activities, for: user, as: :activity})
434 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
435 with %User{} = user <- User.get_cached_by_id(params["id"]) do
438 |> Map.put("tag", params["tagged"])
440 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
443 |> add_link_headers(:user_statuses, activities, params["id"])
444 |> put_view(StatusView)
445 |> render("index.json", %{
446 activities: activities,
453 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
456 |> Map.put("type", "Create")
457 |> Map.put("blocking_user", user)
458 |> Map.put("user", user)
459 |> Map.put(:visibility, "direct")
463 |> ActivityPub.fetch_activities_query(params)
464 |> Pagination.fetch_paginated(params)
467 |> add_link_headers(:dm_timeline, activities)
468 |> put_view(StatusView)
469 |> render("index.json", %{activities: activities, for: user, as: :activity})
472 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
473 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
474 true <- Visibility.visible_for_user?(activity, user) do
476 |> put_view(StatusView)
477 |> try_render("status.json", %{activity: activity, for: user})
481 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
482 with %Activity{} = activity <- Activity.get_by_id(id),
484 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
485 "blocking_user" => user,
489 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
491 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
492 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
498 activities: grouped_activities[true] || [],
502 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
507 activities: grouped_activities[false] || [],
511 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
518 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
519 with %Object{} = object <- Object.get_by_id(id),
520 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
521 true <- Visibility.visible_for_user?(activity, user) do
523 |> put_view(StatusView)
524 |> try_render("poll.json", %{object: object, for: user})
526 nil -> render_error(conn, :not_found, "Record not found")
527 false -> render_error(conn, :not_found, "Record not found")
531 defp get_cached_vote_or_vote(user, object, choices) do
532 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
535 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
536 case CommonAPI.vote(user, object, choices) do
537 {:error, _message} = res -> {:ignore, res}
538 res -> {:commit, res}
545 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
546 with %Object{} = object <- Object.get_by_id(id),
547 true <- object.data["type"] == "Question",
548 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
549 true <- Visibility.visible_for_user?(activity, user),
550 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
552 |> put_view(StatusView)
553 |> try_render("poll.json", %{object: object, for: user})
556 render_error(conn, :not_found, "Record not found")
559 render_error(conn, :not_found, "Record not found")
563 |> put_status(:unprocessable_entity)
564 |> json(%{error: message})
568 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
569 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
571 |> add_link_headers(:scheduled_statuses, scheduled_activities)
572 |> put_view(ScheduledActivityView)
573 |> render("index.json", %{scheduled_activities: scheduled_activities})
577 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
578 with %ScheduledActivity{} = scheduled_activity <-
579 ScheduledActivity.get(user, scheduled_activity_id) do
581 |> put_view(ScheduledActivityView)
582 |> render("show.json", %{scheduled_activity: scheduled_activity})
584 _ -> {:error, :not_found}
588 def update_scheduled_status(
589 %{assigns: %{user: user}} = conn,
590 %{"id" => scheduled_activity_id} = params
592 with %ScheduledActivity{} = scheduled_activity <-
593 ScheduledActivity.get(user, scheduled_activity_id),
594 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
596 |> put_view(ScheduledActivityView)
597 |> render("show.json", %{scheduled_activity: scheduled_activity})
599 nil -> {:error, :not_found}
604 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
605 with %ScheduledActivity{} = scheduled_activity <-
606 ScheduledActivity.get(user, scheduled_activity_id),
607 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
609 |> put_view(ScheduledActivityView)
610 |> render("show.json", %{scheduled_activity: scheduled_activity})
612 nil -> {:error, :not_found}
617 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
620 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
622 scheduled_at = params["scheduled_at"]
624 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
625 with {:ok, scheduled_activity} <-
626 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
628 |> put_view(ScheduledActivityView)
629 |> render("show.json", %{scheduled_activity: scheduled_activity})
632 params = Map.drop(params, ["scheduled_at"])
634 case CommonAPI.post(user, params) do
637 |> put_status(:unprocessable_entity)
638 |> json(%{error: message})
642 |> put_view(StatusView)
643 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
648 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
649 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
652 _e -> render_error(conn, :forbidden, "Can't delete this post")
656 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
657 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
658 %Activity{} = announce <- Activity.normalize(announce.data) do
660 |> put_view(StatusView)
661 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
665 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
666 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
667 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
669 |> put_view(StatusView)
670 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
674 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
675 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
676 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
678 |> put_view(StatusView)
679 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
683 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
684 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
685 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
687 |> put_view(StatusView)
688 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
692 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
693 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
695 |> put_view(StatusView)
696 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
700 |> put_status(:bad_request)
701 |> json(%{"error" => reason})
705 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
706 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
708 |> put_view(StatusView)
709 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
713 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
714 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
715 %User{} = user <- User.get_cached_by_nickname(user.nickname),
716 true <- Visibility.visible_for_user?(activity, user),
717 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
719 |> put_view(StatusView)
720 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
724 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
725 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
726 %User{} = user <- User.get_cached_by_nickname(user.nickname),
727 true <- Visibility.visible_for_user?(activity, user),
728 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
730 |> put_view(StatusView)
731 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
735 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
736 activity = Activity.get_by_id(id)
738 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
740 |> put_view(StatusView)
741 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
745 |> put_resp_content_type("application/json")
746 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
750 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
751 activity = Activity.get_by_id(id)
753 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
755 |> put_view(StatusView)
756 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
760 def notifications(%{assigns: %{user: user}} = conn, params) do
761 notifications = MastodonAPI.get_notifications(user, params)
764 |> add_link_headers(:notifications, notifications)
765 |> put_view(NotificationView)
766 |> render("index.json", %{notifications: notifications, for: user})
769 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
770 with {:ok, notification} <- Notification.get(user, id) do
772 |> put_view(NotificationView)
773 |> render("show.json", %{notification: notification, for: user})
777 |> put_status(:forbidden)
778 |> json(%{"error" => reason})
782 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
783 Notification.clear(user)
787 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
788 with {:ok, _notif} <- Notification.dismiss(user, id) do
793 |> put_status(:forbidden)
794 |> json(%{"error" => reason})
798 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
799 Notification.destroy_multiple(user, ids)
803 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
805 q = from(u in User, where: u.id in ^id)
806 targets = Repo.all(q)
809 |> put_view(AccountView)
810 |> render("relationships.json", %{user: user, targets: targets})
813 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
814 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
816 def update_media(%{assigns: %{user: user}} = conn, data) do
817 with %Object{} = object <- Repo.get(Object, data["id"]),
818 true <- Object.authorize_mutation(object, user),
819 true <- is_binary(data["description"]),
820 description <- data["description"] do
821 new_data = %{object.data | "name" => description}
825 |> Object.change(%{data: new_data})
828 attachment_data = Map.put(new_data, "id", object.id)
831 |> put_view(StatusView)
832 |> render("attachment.json", %{attachment: attachment_data})
836 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
837 with {:ok, object} <-
840 actor: User.ap_id(user),
841 description: Map.get(data, "description")
843 attachment_data = Map.put(object.data, "id", object.id)
846 |> put_view(StatusView)
847 |> render("attachment.json", %{attachment: attachment_data})
851 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
852 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
853 %{} = attachment_data <- Map.put(object.data, "id", object.id),
854 %{type: type} = rendered <-
855 StatusView.render("attachment.json", %{attachment: attachment_data}) do
856 # Reject if not an image
857 if type == "image" do
859 # Save to the user's info
860 info_changeset = User.Info.mascot_update(user.info, rendered)
864 |> Ecto.Changeset.change()
865 |> Ecto.Changeset.put_embed(:info, info_changeset)
867 {:ok, _user} = User.update_and_set_cache(user_changeset)
872 render_error(conn, :unsupported_media_type, "mascots can only be images")
877 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
878 mascot = User.get_mascot(user)
884 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
885 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
886 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
887 q = from(u in User, where: u.ap_id in ^likes)
891 |> put_view(AccountView)
892 |> render("accounts.json", %{for: user, users: users, as: :user})
898 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
899 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
900 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
901 q = from(u in User, where: u.ap_id in ^announces)
905 |> put_view(AccountView)
906 |> render("accounts.json", %{for: user, users: users, as: :user})
912 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
913 local_only = params["local"] in [true, "True", "true", "1"]
916 [params["tag"], params["any"]]
920 |> Enum.map(&String.downcase(&1))
925 |> Enum.map(&String.downcase(&1))
930 |> Enum.map(&String.downcase(&1))
934 |> Map.put("type", "Create")
935 |> Map.put("local_only", local_only)
936 |> Map.put("blocking_user", user)
937 |> Map.put("muting_user", user)
938 |> Map.put("tag", tags)
939 |> Map.put("tag_all", tag_all)
940 |> Map.put("tag_reject", tag_reject)
941 |> ActivityPub.fetch_public_activities()
945 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
946 |> put_view(StatusView)
947 |> render("index.json", %{activities: activities, for: user, as: :activity})
950 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
951 with %User{} = user <- User.get_cached_by_id(id),
952 followers <- MastodonAPI.get_followers(user, params) do
955 for_user && user.id == for_user.id -> followers
956 user.info.hide_followers -> []
961 |> add_link_headers(:followers, followers, user)
962 |> put_view(AccountView)
963 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
967 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
968 with %User{} = user <- User.get_cached_by_id(id),
969 followers <- MastodonAPI.get_friends(user, params) do
972 for_user && user.id == for_user.id -> followers
973 user.info.hide_follows -> []
978 |> add_link_headers(:following, followers, user)
979 |> put_view(AccountView)
980 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
984 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
985 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
987 |> put_view(AccountView)
988 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
992 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
993 with %User{} = follower <- User.get_cached_by_id(id),
994 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
996 |> put_view(AccountView)
997 |> render("relationship.json", %{user: followed, target: follower})
1001 |> put_status(:forbidden)
1002 |> json(%{error: message})
1006 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1007 with %User{} = follower <- User.get_cached_by_id(id),
1008 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1010 |> put_view(AccountView)
1011 |> render("relationship.json", %{user: followed, target: follower})
1013 {:error, message} ->
1015 |> put_status(:forbidden)
1016 |> json(%{error: message})
1020 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1021 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1022 {_, true} <- {:followed, follower.id != followed.id},
1023 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1025 |> put_view(AccountView)
1026 |> render("relationship.json", %{user: follower, target: followed})
1029 {:error, :not_found}
1031 {:error, message} ->
1033 |> put_status(:forbidden)
1034 |> json(%{error: message})
1038 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1039 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1040 {_, true} <- {:followed, follower.id != followed.id},
1041 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1043 |> put_view(AccountView)
1044 |> render("account.json", %{user: followed, for: follower})
1047 {:error, :not_found}
1049 {:error, message} ->
1051 |> put_status(:forbidden)
1052 |> json(%{error: message})
1056 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1057 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1058 {_, true} <- {:followed, follower.id != followed.id},
1059 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1061 |> put_view(AccountView)
1062 |> render("relationship.json", %{user: follower, target: followed})
1065 {:error, :not_found}
1072 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1073 with %User{} = muted <- User.get_cached_by_id(id),
1074 {:ok, muter} <- User.mute(muter, muted) do
1076 |> put_view(AccountView)
1077 |> render("relationship.json", %{user: muter, target: muted})
1079 {:error, message} ->
1081 |> put_status(:forbidden)
1082 |> json(%{error: message})
1086 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1087 with %User{} = muted <- User.get_cached_by_id(id),
1088 {:ok, muter} <- User.unmute(muter, muted) do
1090 |> put_view(AccountView)
1091 |> render("relationship.json", %{user: muter, target: muted})
1093 {:error, message} ->
1095 |> put_status(:forbidden)
1096 |> json(%{error: message})
1100 def mutes(%{assigns: %{user: user}} = conn, _) do
1101 with muted_accounts <- User.muted_users(user) do
1102 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1107 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1108 with %User{} = blocked <- User.get_cached_by_id(id),
1109 {:ok, blocker} <- User.block(blocker, blocked),
1110 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1112 |> put_view(AccountView)
1113 |> render("relationship.json", %{user: blocker, target: blocked})
1115 {:error, message} ->
1117 |> put_status(:forbidden)
1118 |> json(%{error: message})
1122 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1123 with %User{} = blocked <- User.get_cached_by_id(id),
1124 {:ok, blocker} <- User.unblock(blocker, blocked),
1125 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1127 |> put_view(AccountView)
1128 |> render("relationship.json", %{user: blocker, target: blocked})
1130 {:error, message} ->
1132 |> put_status(:forbidden)
1133 |> json(%{error: message})
1137 def blocks(%{assigns: %{user: user}} = conn, _) do
1138 with blocked_accounts <- User.blocked_users(user) do
1139 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1144 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1145 json(conn, info.domain_blocks || [])
1148 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1149 User.block_domain(blocker, domain)
1153 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1154 User.unblock_domain(blocker, domain)
1158 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1159 with %User{} = subscription_target <- User.get_cached_by_id(id),
1160 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1162 |> put_view(AccountView)
1163 |> render("relationship.json", %{user: user, target: subscription_target})
1165 {:error, message} ->
1167 |> put_status(:forbidden)
1168 |> json(%{error: message})
1172 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1173 with %User{} = subscription_target <- User.get_cached_by_id(id),
1174 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1176 |> put_view(AccountView)
1177 |> render("relationship.json", %{user: user, target: subscription_target})
1179 {:error, message} ->
1181 |> put_status(:forbidden)
1182 |> json(%{error: message})
1186 def favourites(%{assigns: %{user: user}} = conn, params) do
1189 |> Map.put("type", "Create")
1190 |> Map.put("favorited_by", user.ap_id)
1191 |> Map.put("blocking_user", user)
1194 ActivityPub.fetch_activities([], params)
1198 |> add_link_headers(:favourites, activities)
1199 |> put_view(StatusView)
1200 |> render("index.json", %{activities: activities, for: user, as: :activity})
1203 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1204 with %User{} = user <- User.get_by_id(id),
1205 false <- user.info.hide_favorites do
1208 |> Map.put("type", "Create")
1209 |> Map.put("favorited_by", user.ap_id)
1210 |> Map.put("blocking_user", for_user)
1214 ["https://www.w3.org/ns/activitystreams#Public"] ++
1215 [for_user.ap_id | for_user.following]
1217 ["https://www.w3.org/ns/activitystreams#Public"]
1222 |> ActivityPub.fetch_activities(params)
1226 |> add_link_headers(:favourites, activities)
1227 |> put_view(StatusView)
1228 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1230 nil -> {:error, :not_found}
1231 true -> render_error(conn, :forbidden, "Can't get favorites")
1235 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1236 user = User.get_cached_by_id(user.id)
1239 Bookmark.for_user_query(user.id)
1240 |> Pagination.fetch_paginated(params)
1244 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1247 |> add_link_headers(:bookmarks, bookmarks)
1248 |> put_view(StatusView)
1249 |> render("index.json", %{activities: activities, for: user, as: :activity})
1252 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1253 lists = Pleroma.List.for_user(user, opts)
1254 res = ListView.render("lists.json", lists: lists)
1258 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1259 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1260 res = ListView.render("list.json", list: list)
1263 _e -> render_error(conn, :not_found, "Record not found")
1267 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1268 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1269 res = ListView.render("lists.json", lists: lists)
1273 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1274 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1275 {:ok, _list} <- Pleroma.List.delete(list) do
1279 json(conn, dgettext("errors", "error"))
1283 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1284 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1285 res = ListView.render("list.json", list: list)
1290 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1292 |> Enum.each(fn account_id ->
1293 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1294 %User{} = followed <- User.get_cached_by_id(account_id) do
1295 Pleroma.List.follow(list, followed)
1302 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1304 |> Enum.each(fn account_id ->
1305 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1306 %User{} = followed <- User.get_cached_by_id(account_id) do
1307 Pleroma.List.unfollow(list, followed)
1314 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1315 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1316 {:ok, users} = Pleroma.List.get_following(list) do
1318 |> put_view(AccountView)
1319 |> render("accounts.json", %{for: user, users: users, as: :user})
1323 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1324 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1325 {:ok, list} <- Pleroma.List.rename(list, title) do
1326 res = ListView.render("list.json", list: list)
1330 json(conn, dgettext("errors", "error"))
1334 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1335 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1338 |> Map.put("type", "Create")
1339 |> Map.put("blocking_user", user)
1340 |> Map.put("muting_user", user)
1342 # we must filter the following list for the user to avoid leaking statuses the user
1343 # does not actually have permission to see (for more info, peruse security issue #270).
1346 |> Enum.filter(fn x -> x in user.following end)
1347 |> ActivityPub.fetch_activities_bounded(following, params)
1351 |> put_view(StatusView)
1352 |> render("index.json", %{activities: activities, for: user, as: :activity})
1354 _e -> render_error(conn, :forbidden, "Error.")
1358 def index(%{assigns: %{user: user}} = conn, _params) do
1359 token = get_session(conn, :oauth_token)
1362 mastodon_emoji = mastodonized_emoji()
1364 limit = Config.get([:instance, :limit])
1367 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1372 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1373 access_token: token,
1375 domain: Pleroma.Web.Endpoint.host(),
1378 unfollow_modal: false,
1381 auto_play_gif: false,
1382 display_sensitive_media: false,
1383 reduce_motion: false,
1384 max_toot_chars: limit,
1385 mascot: User.get_mascot(user)["url"]
1387 poll_limits: Config.get([:instance, :poll_limits]),
1389 delete_others_notice: present?(user.info.is_moderator),
1390 admin: present?(user.info.is_admin)
1394 default_privacy: user.info.default_scope,
1395 default_sensitive: false,
1396 allow_content_types: Config.get([:instance, :allowed_post_formats])
1398 media_attachments: %{
1399 accept_content_types: [
1415 user.info.settings ||
1445 push_subscription: nil,
1447 custom_emojis: mastodon_emoji,
1453 |> put_layout(false)
1454 |> put_view(MastodonView)
1455 |> render("index.html", %{initial_state: initial_state})
1458 |> put_session(:return_to, conn.request_path)
1459 |> redirect(to: "/web/login")
1463 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1464 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1466 with changeset <- Ecto.Changeset.change(user),
1467 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1468 {:ok, _user} <- User.update_and_set_cache(changeset) do
1473 |> put_status(:internal_server_error)
1474 |> json(%{error: inspect(e)})
1478 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1479 redirect(conn, to: local_mastodon_root_path(conn))
1482 @doc "Local Mastodon FE login init action"
1483 def login(conn, %{"code" => auth_token}) do
1484 with {:ok, app} <- get_or_make_app(),
1485 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1486 {:ok, token} <- Token.exchange_token(app, auth) do
1488 |> put_session(:oauth_token, token.token)
1489 |> redirect(to: local_mastodon_root_path(conn))
1493 @doc "Local Mastodon FE callback action"
1494 def login(conn, _) do
1495 with {:ok, app} <- get_or_make_app() do
1500 response_type: "code",
1501 client_id: app.client_id,
1503 scope: Enum.join(app.scopes, " ")
1506 redirect(conn, to: path)
1510 defp local_mastodon_root_path(conn) do
1511 case get_session(conn, :return_to) do
1513 mastodon_api_path(conn, :index, ["getting-started"])
1516 delete_session(conn, :return_to)
1521 defp get_or_make_app do
1522 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1523 scopes = ["read", "write", "follow", "push"]
1525 with %App{} = app <- Repo.get_by(App, find_attrs) do
1527 if app.scopes == scopes do
1531 |> Ecto.Changeset.change(%{scopes: scopes})
1539 App.register_changeset(
1541 Map.put(find_attrs, :scopes, scopes)
1548 def logout(conn, _) do
1551 |> redirect(to: "/")
1554 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1555 Logger.debug("Unimplemented, returning unmodified relationship")
1557 with %User{} = target <- User.get_cached_by_id(id) do
1559 |> put_view(AccountView)
1560 |> render("relationship.json", %{user: user, target: target})
1564 def empty_array(conn, _) do
1565 Logger.debug("Unimplemented, returning an empty array")
1569 def empty_object(conn, _) do
1570 Logger.debug("Unimplemented, returning an empty object")
1574 def get_filters(%{assigns: %{user: user}} = conn, _) do
1575 filters = Filter.get_filters(user)
1576 res = FilterView.render("filters.json", filters: filters)
1581 %{assigns: %{user: user}} = conn,
1582 %{"phrase" => phrase, "context" => context} = params
1588 hide: Map.get(params, "irreversible", false),
1589 whole_word: Map.get(params, "boolean", true)
1593 {:ok, response} = Filter.create(query)
1594 res = FilterView.render("filter.json", filter: response)
1598 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1599 filter = Filter.get(filter_id, user)
1600 res = FilterView.render("filter.json", filter: filter)
1605 %{assigns: %{user: user}} = conn,
1606 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1610 filter_id: filter_id,
1613 hide: Map.get(params, "irreversible", nil),
1614 whole_word: Map.get(params, "boolean", true)
1618 {:ok, response} = Filter.update(query)
1619 res = FilterView.render("filter.json", filter: response)
1623 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1626 filter_id: filter_id
1629 {:ok, _} = Filter.delete(query)
1635 def errors(conn, {:error, %Changeset{} = changeset}) do
1638 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1639 |> Enum.map_join(", ", fn {_k, v} -> v end)
1642 |> put_status(:unprocessable_entity)
1643 |> json(%{error: error_message})
1646 def errors(conn, {:error, :not_found}) do
1647 render_error(conn, :not_found, "Record not found")
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