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_relations_actions ~w(follow unfollow)a
52 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
53 post_status delete_status)a
57 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
58 when action in ~w(reblog_status unreblog_status)a
63 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
64 when action in ~w(fav_status unfav_status)a
69 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
72 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
73 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
74 plug(RateLimiter, :app_account_creation when action == :account_register)
75 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
77 @local_mastodon_name "Mastodon-Local"
79 action_fallback(:errors)
81 def create_app(conn, params) do
82 scopes = Scopes.fetch_scopes(params, ["read"])
86 |> Map.drop(["scope", "scopes"])
87 |> Map.put("scopes", scopes)
89 with cs <- App.register_changeset(%App{}, app_attrs),
90 false <- cs.changes[:client_name] == @local_mastodon_name,
91 {:ok, app} <- Repo.insert(cs) do
94 |> render("show.json", %{app: app})
103 value_function \\ fn x -> {:ok, x} end
105 if Map.has_key?(params, params_field) do
106 case value_function.(params[params_field]) do
107 {:ok, new_value} -> Map.put(map, map_field, new_value)
115 def update_credentials(%{assigns: %{user: user}} = conn, params) do
120 |> add_if_present(params, "display_name", :name)
121 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
122 |> add_if_present(params, "avatar", :avatar, fn value ->
123 with %Plug.Upload{} <- value,
124 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
131 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
134 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
145 :skip_thread_containment
147 |> Enum.reduce(%{}, fn key, acc ->
148 add_if_present(acc, params, to_string(key), key, fn value ->
149 {:ok, ControllerHelper.truthy_param?(value)}
152 |> add_if_present(params, "default_scope", :default_scope)
153 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
154 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
156 |> add_if_present(params, "header", :banner, fn value ->
157 with %Plug.Upload{} <- value,
158 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
164 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
165 with %Plug.Upload{} <- value,
166 {:ok, object} <- ActivityPub.upload(value, type: :background) do
172 |> Map.put(:emoji, user_info_emojis)
174 info_cng = User.Info.profile_update(user.info, info_params)
176 with changeset <- User.update_changeset(user, user_params),
177 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
178 {:ok, user} <- User.update_and_set_cache(changeset) do
179 if original_user != user do
180 CommonAPI.update(user)
185 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
188 _e -> render_error(conn, :forbidden, "Invalid request")
192 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
193 change = Changeset.change(user, %{avatar: nil})
194 {:ok, user} = User.update_and_set_cache(change)
195 CommonAPI.update(user)
197 json(conn, %{url: nil})
200 def update_avatar(%{assigns: %{user: user}} = conn, params) do
201 {:ok, object} = ActivityPub.upload(params, type: :avatar)
202 change = Changeset.change(user, %{avatar: object.data})
203 {:ok, user} = User.update_and_set_cache(change)
204 CommonAPI.update(user)
205 %{"url" => [%{"href" => href} | _]} = object.data
207 json(conn, %{url: href})
210 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
211 with new_info <- %{"banner" => %{}},
212 info_cng <- User.Info.profile_update(user.info, new_info),
213 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
214 {:ok, user} <- User.update_and_set_cache(changeset) do
215 CommonAPI.update(user)
217 json(conn, %{url: nil})
221 def update_banner(%{assigns: %{user: user}} = conn, params) do
222 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
223 new_info <- %{"banner" => object.data},
224 info_cng <- User.Info.profile_update(user.info, new_info),
225 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
226 {:ok, user} <- User.update_and_set_cache(changeset) do
227 CommonAPI.update(user)
228 %{"url" => [%{"href" => href} | _]} = object.data
230 json(conn, %{url: href})
234 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
235 with new_info <- %{"background" => %{}},
236 info_cng <- User.Info.profile_update(user.info, new_info),
237 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
238 {:ok, _user} <- User.update_and_set_cache(changeset) do
239 json(conn, %{url: nil})
243 def update_background(%{assigns: %{user: user}} = conn, params) do
244 with {:ok, object} <- ActivityPub.upload(params, type: :background),
245 new_info <- %{"background" => object.data},
246 info_cng <- User.Info.profile_update(user.info, new_info),
247 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
248 {:ok, _user} <- User.update_and_set_cache(changeset) do
249 %{"url" => [%{"href" => href} | _]} = object.data
251 json(conn, %{url: href})
255 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
256 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
259 AccountView.render("account.json", %{
262 with_pleroma_settings: true,
263 with_chat_token: chat_token
269 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
270 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
273 |> render("short.json", %{app: app})
277 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
278 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
279 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
280 account = AccountView.render("account.json", %{user: user, for: for_user})
283 _e -> render_error(conn, :not_found, "Can't find user")
287 @mastodon_api_level "2.7.2"
289 def masto_instance(conn, _params) do
290 instance = Config.get(:instance)
294 title: Keyword.get(instance, :name),
295 description: Keyword.get(instance, :description),
296 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
297 email: Keyword.get(instance, :email),
299 streaming_api: Pleroma.Web.Endpoint.websocket_url()
301 stats: Stats.get_stats(),
302 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
304 registrations: Pleroma.Config.get([:instance, :registrations_open]),
305 # Extra (not present in Mastodon):
306 max_toot_chars: Keyword.get(instance, :limit),
307 poll_limits: Keyword.get(instance, :poll_limits)
313 def peers(conn, _params) do
314 json(conn, Stats.get_peers())
317 defp mastodonized_emoji do
318 Pleroma.Emoji.get_all()
319 |> Enum.map(fn {shortcode, relative_url, tags} ->
320 url = to_string(URI.merge(Web.base_url(), relative_url))
323 "shortcode" => shortcode,
325 "visible_in_picker" => true,
328 # Assuming that a comma is authorized in the category name
329 "category" => (tags -- ["Custom"]) |> Enum.join(",")
334 def custom_emojis(conn, _params) do
335 mastodon_emoji = mastodonized_emoji()
336 json(conn, mastodon_emoji)
339 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
342 |> Map.drop(["since_id", "max_id", "min_id"])
345 last = List.last(activities)
352 |> Map.get("limit", "20")
353 |> String.to_integer()
356 if length(activities) <= limit do
362 |> Enum.at(limit * -1)
366 {next_url, prev_url} =
370 Pleroma.Web.Endpoint,
373 Map.merge(params, %{max_id: max_id})
376 Pleroma.Web.Endpoint,
379 Map.merge(params, %{min_id: min_id})
385 Pleroma.Web.Endpoint,
387 Map.merge(params, %{max_id: max_id})
390 Pleroma.Web.Endpoint,
392 Map.merge(params, %{min_id: min_id})
398 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
404 def home_timeline(%{assigns: %{user: user}} = conn, params) do
407 |> Map.put("type", ["Create", "Announce"])
408 |> Map.put("blocking_user", user)
409 |> Map.put("muting_user", user)
410 |> Map.put("user", user)
413 [user.ap_id | user.following]
414 |> ActivityPub.fetch_activities(params)
418 |> add_link_headers(:home_timeline, activities)
419 |> put_view(StatusView)
420 |> render("index.json", %{activities: activities, for: user, as: :activity})
423 def public_timeline(%{assigns: %{user: user}} = conn, params) do
424 local_only = params["local"] in [true, "True", "true", "1"]
428 |> Map.put("type", ["Create", "Announce"])
429 |> Map.put("local_only", local_only)
430 |> Map.put("blocking_user", user)
431 |> Map.put("muting_user", user)
432 |> ActivityPub.fetch_public_activities()
436 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
437 |> put_view(StatusView)
438 |> render("index.json", %{activities: activities, for: user, as: :activity})
441 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
442 with %User{} = user <- User.get_cached_by_id(params["id"]) do
445 |> Map.put("tag", params["tagged"])
447 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
450 |> add_link_headers(:user_statuses, activities, params["id"])
451 |> put_view(StatusView)
452 |> render("index.json", %{
453 activities: activities,
460 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
463 |> Map.put("type", "Create")
464 |> Map.put("blocking_user", user)
465 |> Map.put("user", user)
466 |> Map.put(:visibility, "direct")
470 |> ActivityPub.fetch_activities_query(params)
471 |> Pagination.fetch_paginated(params)
474 |> add_link_headers(:dm_timeline, activities)
475 |> put_view(StatusView)
476 |> render("index.json", %{activities: activities, for: user, as: :activity})
479 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
480 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
481 true <- Visibility.visible_for_user?(activity, user) do
483 |> put_view(StatusView)
484 |> try_render("status.json", %{activity: activity, for: user})
488 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
489 with %Activity{} = activity <- Activity.get_by_id(id),
491 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
492 "blocking_user" => user,
496 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
498 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
499 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
505 activities: grouped_activities[true] || [],
509 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
514 activities: grouped_activities[false] || [],
518 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
525 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
526 with %Object{} = object <- Object.get_by_id(id),
527 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
528 true <- Visibility.visible_for_user?(activity, user) do
530 |> put_view(StatusView)
531 |> try_render("poll.json", %{object: object, for: user})
533 nil -> render_error(conn, :not_found, "Record not found")
534 false -> render_error(conn, :not_found, "Record not found")
538 defp get_cached_vote_or_vote(user, object, choices) do
539 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
542 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
543 case CommonAPI.vote(user, object, choices) do
544 {:error, _message} = res -> {:ignore, res}
545 res -> {:commit, res}
552 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
553 with %Object{} = object <- Object.get_by_id(id),
554 true <- object.data["type"] == "Question",
555 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
556 true <- Visibility.visible_for_user?(activity, user),
557 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
559 |> put_view(StatusView)
560 |> try_render("poll.json", %{object: object, for: user})
563 render_error(conn, :not_found, "Record not found")
566 render_error(conn, :not_found, "Record not found")
570 |> put_status(:unprocessable_entity)
571 |> json(%{error: message})
575 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
576 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
578 |> add_link_headers(:scheduled_statuses, scheduled_activities)
579 |> put_view(ScheduledActivityView)
580 |> render("index.json", %{scheduled_activities: scheduled_activities})
584 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
585 with %ScheduledActivity{} = scheduled_activity <-
586 ScheduledActivity.get(user, scheduled_activity_id) do
588 |> put_view(ScheduledActivityView)
589 |> render("show.json", %{scheduled_activity: scheduled_activity})
591 _ -> {:error, :not_found}
595 def update_scheduled_status(
596 %{assigns: %{user: user}} = conn,
597 %{"id" => scheduled_activity_id} = params
599 with %ScheduledActivity{} = scheduled_activity <-
600 ScheduledActivity.get(user, scheduled_activity_id),
601 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
603 |> put_view(ScheduledActivityView)
604 |> render("show.json", %{scheduled_activity: scheduled_activity})
606 nil -> {:error, :not_found}
611 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
612 with %ScheduledActivity{} = scheduled_activity <-
613 ScheduledActivity.get(user, scheduled_activity_id),
614 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
616 |> put_view(ScheduledActivityView)
617 |> render("show.json", %{scheduled_activity: scheduled_activity})
619 nil -> {:error, :not_found}
624 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
627 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
629 scheduled_at = params["scheduled_at"]
631 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
632 with {:ok, scheduled_activity} <-
633 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
635 |> put_view(ScheduledActivityView)
636 |> render("show.json", %{scheduled_activity: scheduled_activity})
639 params = Map.drop(params, ["scheduled_at"])
641 case CommonAPI.post(user, params) do
644 |> put_status(:unprocessable_entity)
645 |> json(%{error: message})
649 |> put_view(StatusView)
650 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
655 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
656 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
659 _e -> render_error(conn, :forbidden, "Can't delete this post")
663 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
664 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
665 %Activity{} = announce <- Activity.normalize(announce.data) do
667 |> put_view(StatusView)
668 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
672 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
673 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
674 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
676 |> put_view(StatusView)
677 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
681 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
682 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
683 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
685 |> put_view(StatusView)
686 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
690 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
691 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
692 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
694 |> put_view(StatusView)
695 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
699 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
700 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
702 |> put_view(StatusView)
703 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
707 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
708 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
710 |> put_view(StatusView)
711 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
715 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
716 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
717 %User{} = user <- User.get_cached_by_nickname(user.nickname),
718 true <- Visibility.visible_for_user?(activity, user),
719 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
721 |> put_view(StatusView)
722 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
726 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
727 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
728 %User{} = user <- User.get_cached_by_nickname(user.nickname),
729 true <- Visibility.visible_for_user?(activity, user),
730 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
732 |> put_view(StatusView)
733 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
737 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
738 activity = Activity.get_by_id(id)
740 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
742 |> put_view(StatusView)
743 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
747 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
748 activity = Activity.get_by_id(id)
750 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
752 |> put_view(StatusView)
753 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
757 def notifications(%{assigns: %{user: user}} = conn, params) do
758 notifications = MastodonAPI.get_notifications(user, params)
761 |> add_link_headers(:notifications, notifications)
762 |> put_view(NotificationView)
763 |> render("index.json", %{notifications: notifications, for: user})
766 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
767 with {:ok, notification} <- Notification.get(user, id) do
769 |> put_view(NotificationView)
770 |> render("show.json", %{notification: notification, for: user})
774 |> put_status(:forbidden)
775 |> json(%{"error" => reason})
779 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
780 Notification.clear(user)
784 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
785 with {:ok, _notif} <- Notification.dismiss(user, id) do
790 |> put_status(:forbidden)
791 |> json(%{"error" => reason})
795 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
796 Notification.destroy_multiple(user, ids)
800 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
802 q = from(u in User, where: u.id in ^id)
803 targets = Repo.all(q)
806 |> put_view(AccountView)
807 |> render("relationships.json", %{user: user, targets: targets})
810 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
811 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
813 def update_media(%{assigns: %{user: user}} = conn, data) do
814 with %Object{} = object <- Repo.get(Object, data["id"]),
815 true <- Object.authorize_mutation(object, user),
816 true <- is_binary(data["description"]),
817 description <- data["description"] do
818 new_data = %{object.data | "name" => description}
822 |> Object.change(%{data: new_data})
825 attachment_data = Map.put(new_data, "id", object.id)
828 |> put_view(StatusView)
829 |> render("attachment.json", %{attachment: attachment_data})
833 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
834 with {:ok, object} <-
837 actor: User.ap_id(user),
838 description: Map.get(data, "description")
840 attachment_data = Map.put(object.data, "id", object.id)
843 |> put_view(StatusView)
844 |> render("attachment.json", %{attachment: attachment_data})
848 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
849 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
850 %{} = attachment_data <- Map.put(object.data, "id", object.id),
851 %{type: type} = rendered <-
852 StatusView.render("attachment.json", %{attachment: attachment_data}) do
853 # Reject if not an image
854 if type == "image" do
856 # Save to the user's info
857 info_changeset = User.Info.mascot_update(user.info, rendered)
861 |> Ecto.Changeset.change()
862 |> Ecto.Changeset.put_embed(:info, info_changeset)
864 {:ok, _user} = User.update_and_set_cache(user_changeset)
869 render_error(conn, :unsupported_media_type, "mascots can only be images")
874 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
875 mascot = User.get_mascot(user)
881 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
882 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
883 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
884 q = from(u in User, where: u.ap_id in ^likes)
888 |> put_view(AccountView)
889 |> render("accounts.json", %{for: user, users: users, as: :user})
895 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
896 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
897 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
898 q = from(u in User, where: u.ap_id in ^announces)
902 |> put_view(AccountView)
903 |> render("accounts.json", %{for: user, users: users, as: :user})
909 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
910 local_only = params["local"] in [true, "True", "true", "1"]
913 [params["tag"], params["any"]]
917 |> Enum.map(&String.downcase(&1))
922 |> Enum.map(&String.downcase(&1))
927 |> Enum.map(&String.downcase(&1))
931 |> Map.put("type", "Create")
932 |> Map.put("local_only", local_only)
933 |> Map.put("blocking_user", user)
934 |> Map.put("muting_user", user)
935 |> Map.put("tag", tags)
936 |> Map.put("tag_all", tag_all)
937 |> Map.put("tag_reject", tag_reject)
938 |> ActivityPub.fetch_public_activities()
942 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
943 |> put_view(StatusView)
944 |> render("index.json", %{activities: activities, for: user, as: :activity})
947 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
948 with %User{} = user <- User.get_cached_by_id(id),
949 followers <- MastodonAPI.get_followers(user, params) do
952 for_user && user.id == for_user.id -> followers
953 user.info.hide_followers -> []
958 |> add_link_headers(:followers, followers, user)
959 |> put_view(AccountView)
960 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
964 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
965 with %User{} = user <- User.get_cached_by_id(id),
966 followers <- MastodonAPI.get_friends(user, params) do
969 for_user && user.id == for_user.id -> followers
970 user.info.hide_follows -> []
975 |> add_link_headers(:following, followers, user)
976 |> put_view(AccountView)
977 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
981 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
982 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
984 |> put_view(AccountView)
985 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
989 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
990 with %User{} = follower <- User.get_cached_by_id(id),
991 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
993 |> put_view(AccountView)
994 |> render("relationship.json", %{user: followed, target: follower})
998 |> put_status(:forbidden)
999 |> json(%{error: message})
1003 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1004 with %User{} = follower <- User.get_cached_by_id(id),
1005 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1007 |> put_view(AccountView)
1008 |> render("relationship.json", %{user: followed, target: follower})
1010 {:error, message} ->
1012 |> put_status(:forbidden)
1013 |> json(%{error: message})
1017 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1018 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1019 {_, true} <- {:followed, follower.id != followed.id},
1020 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1022 |> put_view(AccountView)
1023 |> render("relationship.json", %{user: follower, target: followed})
1026 {:error, :not_found}
1028 {:error, message} ->
1030 |> put_status(:forbidden)
1031 |> json(%{error: message})
1035 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1036 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1037 {_, true} <- {:followed, follower.id != followed.id},
1038 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1040 |> put_view(AccountView)
1041 |> render("account.json", %{user: followed, for: follower})
1044 {:error, :not_found}
1046 {:error, message} ->
1048 |> put_status(:forbidden)
1049 |> json(%{error: message})
1053 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1054 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1055 {_, true} <- {:followed, follower.id != followed.id},
1056 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1058 |> put_view(AccountView)
1059 |> render("relationship.json", %{user: follower, target: followed})
1062 {:error, :not_found}
1069 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1071 if Map.has_key?(params, "notifications"),
1072 do: params["notifications"] in [true, "True", "true", "1"],
1075 with %User{} = muted <- User.get_cached_by_id(id),
1076 {:ok, muter} <- User.mute(muter, muted, notifications) do
1078 |> put_view(AccountView)
1079 |> render("relationship.json", %{user: muter, target: muted})
1081 {:error, message} ->
1083 |> put_status(:forbidden)
1084 |> json(%{error: message})
1088 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1089 with %User{} = muted <- User.get_cached_by_id(id),
1090 {:ok, muter} <- User.unmute(muter, muted) do
1092 |> put_view(AccountView)
1093 |> render("relationship.json", %{user: muter, target: muted})
1095 {:error, message} ->
1097 |> put_status(:forbidden)
1098 |> json(%{error: message})
1102 def mutes(%{assigns: %{user: user}} = conn, _) do
1103 with muted_accounts <- User.muted_users(user) do
1104 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1109 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1110 with %User{} = blocked <- User.get_cached_by_id(id),
1111 {:ok, blocker} <- User.block(blocker, blocked),
1112 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1114 |> put_view(AccountView)
1115 |> render("relationship.json", %{user: blocker, target: blocked})
1117 {:error, message} ->
1119 |> put_status(:forbidden)
1120 |> json(%{error: message})
1124 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1125 with %User{} = blocked <- User.get_cached_by_id(id),
1126 {:ok, blocker} <- User.unblock(blocker, blocked),
1127 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1129 |> put_view(AccountView)
1130 |> render("relationship.json", %{user: blocker, target: blocked})
1132 {:error, message} ->
1134 |> put_status(:forbidden)
1135 |> json(%{error: message})
1139 def blocks(%{assigns: %{user: user}} = conn, _) do
1140 with blocked_accounts <- User.blocked_users(user) do
1141 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1146 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1147 json(conn, info.domain_blocks || [])
1150 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1151 User.block_domain(blocker, domain)
1155 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1156 User.unblock_domain(blocker, domain)
1160 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1161 with %User{} = subscription_target <- User.get_cached_by_id(id),
1162 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1164 |> put_view(AccountView)
1165 |> render("relationship.json", %{user: user, target: subscription_target})
1167 {:error, message} ->
1169 |> put_status(:forbidden)
1170 |> json(%{error: message})
1174 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1175 with %User{} = subscription_target <- User.get_cached_by_id(id),
1176 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1178 |> put_view(AccountView)
1179 |> render("relationship.json", %{user: user, target: subscription_target})
1181 {:error, message} ->
1183 |> put_status(:forbidden)
1184 |> json(%{error: message})
1188 def favourites(%{assigns: %{user: user}} = conn, params) do
1191 |> Map.put("type", "Create")
1192 |> Map.put("favorited_by", user.ap_id)
1193 |> Map.put("blocking_user", user)
1196 ActivityPub.fetch_activities([], params)
1200 |> add_link_headers(:favourites, activities)
1201 |> put_view(StatusView)
1202 |> render("index.json", %{activities: activities, for: user, as: :activity})
1205 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1206 with %User{} = user <- User.get_by_id(id),
1207 false <- user.info.hide_favorites do
1210 |> Map.put("type", "Create")
1211 |> Map.put("favorited_by", user.ap_id)
1212 |> Map.put("blocking_user", for_user)
1216 ["https://www.w3.org/ns/activitystreams#Public"] ++
1217 [for_user.ap_id | for_user.following]
1219 ["https://www.w3.org/ns/activitystreams#Public"]
1224 |> ActivityPub.fetch_activities(params)
1228 |> add_link_headers(:favourites, activities)
1229 |> put_view(StatusView)
1230 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1232 nil -> {:error, :not_found}
1233 true -> render_error(conn, :forbidden, "Can't get favorites")
1237 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1238 user = User.get_cached_by_id(user.id)
1241 Bookmark.for_user_query(user.id)
1242 |> Pagination.fetch_paginated(params)
1246 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1249 |> add_link_headers(:bookmarks, bookmarks)
1250 |> put_view(StatusView)
1251 |> render("index.json", %{activities: activities, for: user, as: :activity})
1254 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1255 lists = Pleroma.List.for_user(user, opts)
1256 res = ListView.render("lists.json", lists: lists)
1260 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1261 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1262 res = ListView.render("list.json", list: list)
1265 _e -> render_error(conn, :not_found, "Record not found")
1269 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1270 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1271 res = ListView.render("lists.json", lists: lists)
1275 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1276 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1277 {:ok, _list} <- Pleroma.List.delete(list) do
1281 json(conn, dgettext("errors", "error"))
1285 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1286 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1287 res = ListView.render("list.json", list: list)
1292 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1294 |> Enum.each(fn account_id ->
1295 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1296 %User{} = followed <- User.get_cached_by_id(account_id) do
1297 Pleroma.List.follow(list, followed)
1304 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1306 |> Enum.each(fn account_id ->
1307 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1308 %User{} = followed <- User.get_cached_by_id(account_id) do
1309 Pleroma.List.unfollow(list, followed)
1316 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1317 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1318 {:ok, users} = Pleroma.List.get_following(list) do
1320 |> put_view(AccountView)
1321 |> render("accounts.json", %{for: user, users: users, as: :user})
1325 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1326 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1327 {:ok, list} <- Pleroma.List.rename(list, title) do
1328 res = ListView.render("list.json", list: list)
1332 json(conn, dgettext("errors", "error"))
1336 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1337 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1340 |> Map.put("type", "Create")
1341 |> Map.put("blocking_user", user)
1342 |> Map.put("muting_user", user)
1344 # we must filter the following list for the user to avoid leaking statuses the user
1345 # does not actually have permission to see (for more info, peruse security issue #270).
1348 |> Enum.filter(fn x -> x in user.following end)
1349 |> ActivityPub.fetch_activities_bounded(following, params)
1353 |> put_view(StatusView)
1354 |> render("index.json", %{activities: activities, for: user, as: :activity})
1356 _e -> render_error(conn, :forbidden, "Error.")
1360 def index(%{assigns: %{user: user}} = conn, _params) do
1361 token = get_session(conn, :oauth_token)
1364 mastodon_emoji = mastodonized_emoji()
1366 limit = Config.get([:instance, :limit])
1369 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1374 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1375 access_token: token,
1377 domain: Pleroma.Web.Endpoint.host(),
1380 unfollow_modal: false,
1383 auto_play_gif: false,
1384 display_sensitive_media: false,
1385 reduce_motion: false,
1386 max_toot_chars: limit,
1387 mascot: User.get_mascot(user)["url"]
1389 poll_limits: Config.get([:instance, :poll_limits]),
1391 delete_others_notice: present?(user.info.is_moderator),
1392 admin: present?(user.info.is_admin)
1396 default_privacy: user.info.default_scope,
1397 default_sensitive: false,
1398 allow_content_types: Config.get([:instance, :allowed_post_formats])
1400 media_attachments: %{
1401 accept_content_types: [
1417 user.info.settings ||
1447 push_subscription: nil,
1449 custom_emojis: mastodon_emoji,
1455 |> put_layout(false)
1456 |> put_view(MastodonView)
1457 |> render("index.html", %{initial_state: initial_state})
1460 |> put_session(:return_to, conn.request_path)
1461 |> redirect(to: "/web/login")
1465 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1466 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1468 with changeset <- Ecto.Changeset.change(user),
1469 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1470 {:ok, _user} <- User.update_and_set_cache(changeset) do
1475 |> put_status(:internal_server_error)
1476 |> json(%{error: inspect(e)})
1480 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1481 redirect(conn, to: local_mastodon_root_path(conn))
1484 @doc "Local Mastodon FE login init action"
1485 def login(conn, %{"code" => auth_token}) do
1486 with {:ok, app} <- get_or_make_app(),
1487 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1488 {:ok, token} <- Token.exchange_token(app, auth) do
1490 |> put_session(:oauth_token, token.token)
1491 |> redirect(to: local_mastodon_root_path(conn))
1495 @doc "Local Mastodon FE callback action"
1496 def login(conn, _) do
1497 with {:ok, app} <- get_or_make_app() do
1502 response_type: "code",
1503 client_id: app.client_id,
1505 scope: Enum.join(app.scopes, " ")
1508 redirect(conn, to: path)
1512 defp local_mastodon_root_path(conn) do
1513 case get_session(conn, :return_to) do
1515 mastodon_api_path(conn, :index, ["getting-started"])
1518 delete_session(conn, :return_to)
1523 defp get_or_make_app do
1524 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1525 scopes = ["read", "write", "follow", "push"]
1527 with %App{} = app <- Repo.get_by(App, find_attrs) do
1529 if app.scopes == scopes do
1533 |> Ecto.Changeset.change(%{scopes: scopes})
1541 App.register_changeset(
1543 Map.put(find_attrs, :scopes, scopes)
1550 def logout(conn, _) do
1553 |> redirect(to: "/")
1556 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1557 Logger.debug("Unimplemented, returning unmodified relationship")
1559 with %User{} = target <- User.get_cached_by_id(id) do
1561 |> put_view(AccountView)
1562 |> render("relationship.json", %{user: user, target: target})
1566 def empty_array(conn, _) do
1567 Logger.debug("Unimplemented, returning an empty array")
1571 def empty_object(conn, _) do
1572 Logger.debug("Unimplemented, returning an empty object")
1576 def get_filters(%{assigns: %{user: user}} = conn, _) do
1577 filters = Filter.get_filters(user)
1578 res = FilterView.render("filters.json", filters: filters)
1583 %{assigns: %{user: user}} = conn,
1584 %{"phrase" => phrase, "context" => context} = params
1590 hide: Map.get(params, "irreversible", false),
1591 whole_word: Map.get(params, "boolean", true)
1595 {:ok, response} = Filter.create(query)
1596 res = FilterView.render("filter.json", filter: response)
1600 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1601 filter = Filter.get(filter_id, user)
1602 res = FilterView.render("filter.json", filter: filter)
1607 %{assigns: %{user: user}} = conn,
1608 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1612 filter_id: filter_id,
1615 hide: Map.get(params, "irreversible", nil),
1616 whole_word: Map.get(params, "boolean", true)
1620 {:ok, response} = Filter.update(query)
1621 res = FilterView.render("filter.json", filter: response)
1625 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1628 filter_id: filter_id
1631 {:ok, _} = Filter.delete(query)
1637 def errors(conn, {:error, %Changeset{} = changeset}) do
1640 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1641 |> Enum.map_join(", ", fn {_k, v} -> v end)
1644 |> put_status(:unprocessable_entity)
1645 |> json(%{error: error_message})
1648 def errors(conn, {:error, :not_found}) do
1649 render_error(conn, :not_found, "Record not found")
1652 def errors(conn, {:error, error_message}) do
1654 |> put_status(:bad_request)
1655 |> json(%{error: error_message})
1658 def errors(conn, _) do
1660 |> put_status(:internal_server_error)
1661 |> json(dgettext("errors", "Something went wrong"))
1664 def suggestions(%{assigns: %{user: user}} = conn, _) do
1665 suggestions = Config.get(:suggestions)
1667 if Keyword.get(suggestions, :enabled, false) do
1668 api = Keyword.get(suggestions, :third_party_engine, "")
1669 timeout = Keyword.get(suggestions, :timeout, 5000)
1670 limit = Keyword.get(suggestions, :limit, 23)
1672 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1674 user = user.nickname
1678 |> String.replace("{{host}}", host)
1679 |> String.replace("{{user}}", user)
1681 with {:ok, %{status: 200, body: body}} <-
1686 recv_timeout: timeout,
1690 {:ok, data} <- Jason.decode(body) do
1693 |> Enum.slice(0, limit)
1698 case User.get_or_fetch(x["acct"]) do
1699 {:ok, %User{id: id}} -> id
1705 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1708 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1714 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1721 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1722 with %Activity{} = activity <- Activity.get_by_id(status_id),
1723 true <- Visibility.visible_for_user?(activity, user) do
1727 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1737 def reports(%{assigns: %{user: user}} = conn, params) do
1738 case CommonAPI.report(user, params) do
1741 |> put_view(ReportView)
1742 |> try_render("report.json", %{activity: activity})
1746 |> put_status(:bad_request)
1747 |> json(%{error: err})
1751 def account_register(
1752 %{assigns: %{app: app}} = conn,
1753 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1761 "captcha_answer_data",
1765 |> Map.put("nickname", nickname)
1766 |> Map.put("fullname", params["fullname"] || nickname)
1767 |> Map.put("bio", params["bio"] || "")
1768 |> Map.put("confirm", params["password"])
1770 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1771 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1773 token_type: "Bearer",
1774 access_token: token.token,
1776 created_at: Token.Utils.format_created_at(token)
1781 |> put_status(:bad_request)
1786 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1787 render_error(conn, :bad_request, "Missing parameters")
1790 def account_register(conn, _) do
1791 render_error(conn, :forbidden, "Invalid credentials")
1794 def conversations(%{assigns: %{user: user}} = conn, params) do
1795 participations = Participation.for_user_with_last_activity_id(user, params)
1798 Enum.map(participations, fn participation ->
1799 ConversationView.render("participation.json", %{participation: participation, user: user})
1803 |> add_link_headers(:conversations, participations)
1804 |> json(conversations)
1807 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1808 with %Participation{} = participation <-
1809 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1810 {:ok, participation} <- Participation.mark_as_read(participation) do
1811 participation_view =
1812 ConversationView.render("participation.json", %{participation: participation, user: user})
1815 |> json(participation_view)
1819 def try_render(conn, target, params)
1820 when is_binary(target) do
1821 case render(conn, target, params) do
1822 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1827 def try_render(conn, _, _) do
1828 render_error(conn, :not_implemented, "Can't display this activity")
1831 defp present?(nil), do: false
1832 defp present?(false), do: false
1833 defp present?(_), do: true