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.Object.Fetcher
18 alias Pleroma.Pagination
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
51 Pleroma.Plugs.RateLimitPlug,
53 max_requests: Config.get([:app_account_creation, :max_requests]),
54 interval: Config.get([:app_account_creation, :interval])
56 when action in [:account_register]
59 @local_mastodon_name "Mastodon-Local"
61 action_fallback(:errors)
63 def create_app(conn, params) do
64 scopes = Scopes.fetch_scopes(params, ["read"])
68 |> Map.drop(["scope", "scopes"])
69 |> Map.put("scopes", scopes)
71 with cs <- App.register_changeset(%App{}, app_attrs),
72 false <- cs.changes[:client_name] == @local_mastodon_name,
73 {:ok, app} <- Repo.insert(cs) do
76 |> render("show.json", %{app: app})
85 value_function \\ fn x -> {:ok, x} end
87 if Map.has_key?(params, params_field) do
88 case value_function.(params[params_field]) do
89 {:ok, new_value} -> Map.put(map, map_field, new_value)
97 def update_credentials(%{assigns: %{user: user}} = conn, params) do
102 |> add_if_present(params, "display_name", :name)
103 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
104 |> add_if_present(params, "avatar", :avatar, fn value ->
105 with %Plug.Upload{} <- value,
106 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
113 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
116 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
120 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
121 |> Enum.reduce(%{}, fn key, acc ->
122 add_if_present(acc, params, to_string(key), key, fn value ->
123 {:ok, ControllerHelper.truthy_param?(value)}
126 |> add_if_present(params, "default_scope", :default_scope)
127 |> add_if_present(params, "header", :banner, fn value ->
128 with %Plug.Upload{} <- value,
129 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
135 |> Map.put(:emoji, user_info_emojis)
137 info_cng = User.Info.profile_update(user.info, info_params)
139 with changeset <- User.update_changeset(user, user_params),
140 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
141 {:ok, user} <- User.update_and_set_cache(changeset) do
142 if original_user != user do
143 CommonAPI.update(user)
146 json(conn, AccountView.render("account.json", %{user: user, for: user}))
151 |> json(%{error: "Invalid request"})
155 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
156 account = AccountView.render("account.json", %{user: user, for: user})
160 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
161 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
164 |> render("short.json", %{app: app})
168 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
169 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
170 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
171 account = AccountView.render("account.json", %{user: user, for: for_user})
177 |> json(%{error: "Can't find user"})
181 @mastodon_api_level "2.7.2"
183 def masto_instance(conn, _params) do
184 instance = Config.get(:instance)
188 title: Keyword.get(instance, :name),
189 description: Keyword.get(instance, :description),
190 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
191 email: Keyword.get(instance, :email),
193 streaming_api: Pleroma.Web.Endpoint.websocket_url()
195 stats: Stats.get_stats(),
196 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
198 registrations: Pleroma.Config.get([:instance, :registrations_open]),
199 # Extra (not present in Mastodon):
200 max_toot_chars: Keyword.get(instance, :limit),
201 poll_limits: Keyword.get(instance, :poll_limits)
207 def peers(conn, _params) do
208 json(conn, Stats.get_peers())
211 defp mastodonized_emoji do
212 Pleroma.Emoji.get_all()
213 |> Enum.map(fn {shortcode, relative_url, tags} ->
214 url = to_string(URI.merge(Web.base_url(), relative_url))
217 "shortcode" => shortcode,
219 "visible_in_picker" => true,
226 def custom_emojis(conn, _params) do
227 mastodon_emoji = mastodonized_emoji()
228 json(conn, mastodon_emoji)
231 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
234 |> Map.drop(["since_id", "max_id", "min_id"])
237 last = List.last(activities)
244 |> Map.get("limit", "20")
245 |> String.to_integer()
248 if length(activities) <= limit do
254 |> Enum.at(limit * -1)
258 {next_url, prev_url} =
262 Pleroma.Web.Endpoint,
265 Map.merge(params, %{max_id: max_id})
268 Pleroma.Web.Endpoint,
271 Map.merge(params, %{min_id: min_id})
277 Pleroma.Web.Endpoint,
279 Map.merge(params, %{max_id: max_id})
282 Pleroma.Web.Endpoint,
284 Map.merge(params, %{min_id: min_id})
290 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
296 def home_timeline(%{assigns: %{user: user}} = conn, params) do
299 |> Map.put("type", ["Create", "Announce"])
300 |> Map.put("blocking_user", user)
301 |> Map.put("muting_user", user)
302 |> Map.put("user", user)
305 [user.ap_id | user.following]
306 |> ActivityPub.fetch_activities(params)
310 |> add_link_headers(:home_timeline, activities)
311 |> put_view(StatusView)
312 |> render("index.json", %{activities: activities, for: user, as: :activity})
315 def public_timeline(%{assigns: %{user: user}} = conn, params) do
316 local_only = params["local"] in [true, "True", "true", "1"]
320 |> Map.put("type", ["Create", "Announce"])
321 |> Map.put("local_only", local_only)
322 |> Map.put("blocking_user", user)
323 |> Map.put("muting_user", user)
324 |> ActivityPub.fetch_public_activities()
328 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
329 |> put_view(StatusView)
330 |> render("index.json", %{activities: activities, for: user, as: :activity})
333 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
334 with %User{} = user <- User.get_cached_by_id(params["id"]) do
335 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
338 |> add_link_headers(:user_statuses, activities, params["id"])
339 |> put_view(StatusView)
340 |> render("index.json", %{
341 activities: activities,
348 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
351 |> Map.put("type", "Create")
352 |> Map.put("blocking_user", user)
353 |> Map.put("user", user)
354 |> Map.put(:visibility, "direct")
358 |> ActivityPub.fetch_activities_query(params)
359 |> Pagination.fetch_paginated(params)
362 |> add_link_headers(:dm_timeline, activities)
363 |> put_view(StatusView)
364 |> render("index.json", %{activities: activities, for: user, as: :activity})
367 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
368 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
369 true <- Visibility.visible_for_user?(activity, user) do
371 |> put_view(StatusView)
372 |> try_render("status.json", %{activity: activity, for: user})
376 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
377 with %Activity{} = activity <- Activity.get_by_id(id),
379 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
380 "blocking_user" => user,
384 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
386 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
387 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
393 activities: grouped_activities[true] || [],
397 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
402 activities: grouped_activities[false] || [],
406 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
413 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
414 with %Object{} = object <- Object.get_by_id(id),
415 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
416 true <- Visibility.visible_for_user?(activity, user) do
418 |> put_view(StatusView)
419 |> try_render("poll.json", %{object: object, for: user})
424 |> json(%{error: "Record not found"})
429 |> json(%{error: "Record not found"})
433 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
434 with %Object{} = object <- Object.get_by_id(id),
435 true <- object.data["type"] == "Question",
436 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
437 true <- Visibility.visible_for_user?(activity, user),
438 {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do
439 object = Object.get_cached_by_ap_id(object.data["id"])
442 |> put_view(StatusView)
443 |> try_render("poll.json", %{object: object, for: user})
448 |> json(%{error: "Record not found"})
453 |> json(%{error: "Record not found"})
458 |> json(%{error: message})
462 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
463 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
465 |> add_link_headers(:scheduled_statuses, scheduled_activities)
466 |> put_view(ScheduledActivityView)
467 |> render("index.json", %{scheduled_activities: scheduled_activities})
471 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
472 with %ScheduledActivity{} = scheduled_activity <-
473 ScheduledActivity.get(user, scheduled_activity_id) do
475 |> put_view(ScheduledActivityView)
476 |> render("show.json", %{scheduled_activity: scheduled_activity})
478 _ -> {:error, :not_found}
482 def update_scheduled_status(
483 %{assigns: %{user: user}} = conn,
484 %{"id" => scheduled_activity_id} = params
486 with %ScheduledActivity{} = scheduled_activity <-
487 ScheduledActivity.get(user, scheduled_activity_id),
488 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
490 |> put_view(ScheduledActivityView)
491 |> render("show.json", %{scheduled_activity: scheduled_activity})
493 nil -> {:error, :not_found}
498 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
499 with %ScheduledActivity{} = scheduled_activity <-
500 ScheduledActivity.get(user, scheduled_activity_id),
501 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
503 |> put_view(ScheduledActivityView)
504 |> render("show.json", %{scheduled_activity: scheduled_activity})
506 nil -> {:error, :not_found}
511 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
512 when length(media_ids) > 0 do
515 |> Map.put("status", ".")
517 post_status(conn, params)
520 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
523 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
525 scheduled_at = params["scheduled_at"]
527 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
528 with {:ok, scheduled_activity} <-
529 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
531 |> put_view(ScheduledActivityView)
532 |> render("show.json", %{scheduled_activity: scheduled_activity})
535 params = Map.drop(params, ["scheduled_at"])
537 case get_cached_status_or_post(conn, params) do
538 {:ignore, message} ->
541 |> json(%{error: message})
546 |> json(%{error: message})
550 |> put_view(StatusView)
551 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
556 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
558 case get_req_header(conn, "idempotency-key") do
560 _ -> Ecto.UUID.generate()
563 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
564 case CommonAPI.post(user, params) do
565 {:ok, activity} -> activity
566 {:error, message} -> {:ignore, message}
571 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
572 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
578 |> json(%{error: "Can't delete this post"})
582 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
583 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
584 %Activity{} = announce <- Activity.normalize(announce.data) do
586 |> put_view(StatusView)
587 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
591 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
592 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
593 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
595 |> put_view(StatusView)
596 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
600 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
601 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
602 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
604 |> put_view(StatusView)
605 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
609 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
610 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
611 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
613 |> put_view(StatusView)
614 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
618 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
619 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
621 |> put_view(StatusView)
622 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
626 |> put_resp_content_type("application/json")
627 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
631 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
632 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
634 |> put_view(StatusView)
635 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
639 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
640 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
641 %User{} = user <- User.get_cached_by_nickname(user.nickname),
642 true <- Visibility.visible_for_user?(activity, user),
643 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
645 |> put_view(StatusView)
646 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
650 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
651 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
652 %User{} = user <- User.get_cached_by_nickname(user.nickname),
653 true <- Visibility.visible_for_user?(activity, user),
654 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
656 |> put_view(StatusView)
657 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
661 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
662 activity = Activity.get_by_id(id)
664 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
666 |> put_view(StatusView)
667 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
671 |> put_resp_content_type("application/json")
672 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
676 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
677 activity = Activity.get_by_id(id)
679 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
681 |> put_view(StatusView)
682 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
686 def notifications(%{assigns: %{user: user}} = conn, params) do
687 notifications = MastodonAPI.get_notifications(user, params)
690 |> add_link_headers(:notifications, notifications)
691 |> put_view(NotificationView)
692 |> render("index.json", %{notifications: notifications, for: user})
695 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
696 with {:ok, notification} <- Notification.get(user, id) do
698 |> put_view(NotificationView)
699 |> render("show.json", %{notification: notification, for: user})
703 |> put_resp_content_type("application/json")
704 |> send_resp(403, Jason.encode!(%{"error" => reason}))
708 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
709 Notification.clear(user)
713 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
714 with {:ok, _notif} <- Notification.dismiss(user, id) do
719 |> put_resp_content_type("application/json")
720 |> send_resp(403, Jason.encode!(%{"error" => reason}))
724 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
725 Notification.destroy_multiple(user, ids)
729 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
731 q = from(u in User, where: u.id in ^id)
732 targets = Repo.all(q)
735 |> put_view(AccountView)
736 |> render("relationships.json", %{user: user, targets: targets})
739 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
740 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
742 def update_media(%{assigns: %{user: user}} = conn, data) do
743 with %Object{} = object <- Repo.get(Object, data["id"]),
744 true <- Object.authorize_mutation(object, user),
745 true <- is_binary(data["description"]),
746 description <- data["description"] do
747 new_data = %{object.data | "name" => description}
751 |> Object.change(%{data: new_data})
754 attachment_data = Map.put(new_data, "id", object.id)
757 |> put_view(StatusView)
758 |> render("attachment.json", %{attachment: attachment_data})
762 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
763 with {:ok, object} <-
766 actor: User.ap_id(user),
767 description: Map.get(data, "description")
769 attachment_data = Map.put(object.data, "id", object.id)
772 |> put_view(StatusView)
773 |> render("attachment.json", %{attachment: attachment_data})
777 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
778 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
779 %{} = attachment_data <- Map.put(object.data, "id", object.id),
780 %{type: type} = rendered <-
781 StatusView.render("attachment.json", %{attachment: attachment_data}) do
782 # Reject if not an image
783 if type == "image" do
785 # Save to the user's info
786 info_changeset = User.Info.mascot_update(user.info, rendered)
790 |> Ecto.Changeset.change()
791 |> Ecto.Changeset.put_embed(:info, info_changeset)
793 {:ok, _user} = User.update_and_set_cache(user_changeset)
799 |> put_resp_content_type("application/json")
800 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
805 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
806 mascot = User.get_mascot(user)
812 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
813 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
814 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
815 q = from(u in User, where: u.ap_id in ^likes)
819 |> put_view(AccountView)
820 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
826 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
827 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
828 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
829 q = from(u in User, where: u.ap_id in ^announces)
833 |> put_view(AccountView)
834 |> render("accounts.json", %{for: user, users: users, as: :user})
840 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
841 local_only = params["local"] in [true, "True", "true", "1"]
844 [params["tag"], params["any"]]
848 |> Enum.map(&String.downcase(&1))
853 |> Enum.map(&String.downcase(&1))
858 |> Enum.map(&String.downcase(&1))
862 |> Map.put("type", "Create")
863 |> Map.put("local_only", local_only)
864 |> Map.put("blocking_user", user)
865 |> Map.put("muting_user", user)
866 |> Map.put("tag", tags)
867 |> Map.put("tag_all", tag_all)
868 |> Map.put("tag_reject", tag_reject)
869 |> ActivityPub.fetch_public_activities()
873 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
874 |> put_view(StatusView)
875 |> render("index.json", %{activities: activities, for: user, as: :activity})
878 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
879 with %User{} = user <- User.get_cached_by_id(id),
880 followers <- MastodonAPI.get_followers(user, params) do
883 for_user && user.id == for_user.id -> followers
884 user.info.hide_followers -> []
889 |> add_link_headers(:followers, followers, user)
890 |> put_view(AccountView)
891 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
895 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
896 with %User{} = user <- User.get_cached_by_id(id),
897 followers <- MastodonAPI.get_friends(user, params) do
900 for_user && user.id == for_user.id -> followers
901 user.info.hide_follows -> []
906 |> add_link_headers(:following, followers, user)
907 |> put_view(AccountView)
908 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
912 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
913 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
915 |> put_view(AccountView)
916 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
920 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
921 with %User{} = follower <- User.get_cached_by_id(id),
922 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
924 |> put_view(AccountView)
925 |> render("relationship.json", %{user: followed, target: follower})
929 |> put_resp_content_type("application/json")
930 |> send_resp(403, Jason.encode!(%{"error" => message}))
934 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
935 with %User{} = follower <- User.get_cached_by_id(id),
936 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
938 |> put_view(AccountView)
939 |> render("relationship.json", %{user: followed, target: follower})
943 |> put_resp_content_type("application/json")
944 |> send_resp(403, Jason.encode!(%{"error" => message}))
948 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
949 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
950 {_, true} <- {:followed, follower.id != followed.id},
951 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
953 |> put_view(AccountView)
954 |> render("relationship.json", %{user: follower, target: followed})
961 |> put_resp_content_type("application/json")
962 |> send_resp(403, Jason.encode!(%{"error" => message}))
966 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
967 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
968 {_, true} <- {:followed, follower.id != followed.id},
969 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
971 |> put_view(AccountView)
972 |> render("account.json", %{user: followed, for: follower})
979 |> put_resp_content_type("application/json")
980 |> send_resp(403, Jason.encode!(%{"error" => message}))
984 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
985 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
986 {_, true} <- {:followed, follower.id != followed.id},
987 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
989 |> put_view(AccountView)
990 |> render("relationship.json", %{user: follower, target: followed})
1000 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1001 with %User{} = muted <- User.get_cached_by_id(id),
1002 {:ok, muter} <- User.mute(muter, muted) do
1004 |> put_view(AccountView)
1005 |> render("relationship.json", %{user: muter, target: muted})
1007 {:error, message} ->
1009 |> put_resp_content_type("application/json")
1010 |> send_resp(403, Jason.encode!(%{"error" => message}))
1014 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1015 with %User{} = muted <- User.get_cached_by_id(id),
1016 {:ok, muter} <- User.unmute(muter, muted) do
1018 |> put_view(AccountView)
1019 |> render("relationship.json", %{user: muter, target: muted})
1021 {:error, message} ->
1023 |> put_resp_content_type("application/json")
1024 |> send_resp(403, Jason.encode!(%{"error" => message}))
1028 def mutes(%{assigns: %{user: user}} = conn, _) do
1029 with muted_accounts <- User.muted_users(user) do
1030 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1035 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1036 with %User{} = blocked <- User.get_cached_by_id(id),
1037 {:ok, blocker} <- User.block(blocker, blocked),
1038 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1040 |> put_view(AccountView)
1041 |> render("relationship.json", %{user: blocker, target: blocked})
1043 {:error, message} ->
1045 |> put_resp_content_type("application/json")
1046 |> send_resp(403, Jason.encode!(%{"error" => message}))
1050 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1051 with %User{} = blocked <- User.get_cached_by_id(id),
1052 {:ok, blocker} <- User.unblock(blocker, blocked),
1053 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1055 |> put_view(AccountView)
1056 |> render("relationship.json", %{user: blocker, target: blocked})
1058 {:error, message} ->
1060 |> put_resp_content_type("application/json")
1061 |> send_resp(403, Jason.encode!(%{"error" => message}))
1065 def blocks(%{assigns: %{user: user}} = conn, _) do
1066 with blocked_accounts <- User.blocked_users(user) do
1067 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1072 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1073 json(conn, info.domain_blocks || [])
1076 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1077 User.block_domain(blocker, domain)
1081 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1082 User.unblock_domain(blocker, domain)
1086 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1087 with %User{} = subscription_target <- User.get_cached_by_id(id),
1088 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1090 |> put_view(AccountView)
1091 |> render("relationship.json", %{user: user, target: subscription_target})
1093 {:error, message} ->
1095 |> put_resp_content_type("application/json")
1096 |> send_resp(403, Jason.encode!(%{"error" => message}))
1100 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1101 with %User{} = subscription_target <- User.get_cached_by_id(id),
1102 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1104 |> put_view(AccountView)
1105 |> render("relationship.json", %{user: user, target: subscription_target})
1107 {:error, message} ->
1109 |> put_resp_content_type("application/json")
1110 |> send_resp(403, Jason.encode!(%{"error" => message}))
1114 def status_search_query_with_gin(q, query) do
1118 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1122 order_by: [desc: :id]
1126 def status_search_query_with_rum(q, query) do
1130 "? @@ plainto_tsquery('english', ?)",
1134 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1138 def status_search(user, query) do
1140 if Regex.match?(~r/https?:/, query) do
1141 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1142 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1143 true <- Visibility.visible_for_user?(activity, user) do
1151 from([a, o] in Activity.with_preloaded_object(Activity),
1152 where: fragment("?->>'type' = 'Create'", a.data),
1153 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1158 if Pleroma.Config.get([:database, :rum_enabled]) do
1159 status_search_query_with_rum(q, query)
1161 status_search_query_with_gin(q, query)
1164 Repo.all(q) ++ fetched
1167 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1168 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1170 statuses = status_search(user, query)
1172 tags_path = Web.base_url() <> "/tag/"
1178 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1179 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1180 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1183 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1185 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1192 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1193 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1195 statuses = status_search(user, query)
1201 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1202 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1205 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1207 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1214 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1215 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1217 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1222 def favourites(%{assigns: %{user: user}} = conn, params) do
1225 |> Map.put("type", "Create")
1226 |> Map.put("favorited_by", user.ap_id)
1227 |> Map.put("blocking_user", user)
1230 ActivityPub.fetch_activities([], params)
1234 |> add_link_headers(:favourites, activities)
1235 |> put_view(StatusView)
1236 |> render("index.json", %{activities: activities, for: user, as: :activity})
1239 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1240 with %User{} = user <- User.get_by_id(id),
1241 false <- user.info.hide_favorites do
1244 |> Map.put("type", "Create")
1245 |> Map.put("favorited_by", user.ap_id)
1246 |> Map.put("blocking_user", for_user)
1250 ["https://www.w3.org/ns/activitystreams#Public"] ++
1251 [for_user.ap_id | for_user.following]
1253 ["https://www.w3.org/ns/activitystreams#Public"]
1258 |> ActivityPub.fetch_activities(params)
1262 |> add_link_headers(:favourites, activities)
1263 |> put_view(StatusView)
1264 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1267 {:error, :not_found}
1272 |> json(%{error: "Can't get favorites"})
1276 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1277 user = User.get_cached_by_id(user.id)
1280 Bookmark.for_user_query(user.id)
1281 |> Pagination.fetch_paginated(params)
1285 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1288 |> add_link_headers(:bookmarks, bookmarks)
1289 |> put_view(StatusView)
1290 |> render("index.json", %{activities: activities, for: user, as: :activity})
1293 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1294 lists = Pleroma.List.for_user(user, opts)
1295 res = ListView.render("lists.json", lists: lists)
1299 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1300 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1301 res = ListView.render("list.json", list: list)
1307 |> json(%{error: "Record not found"})
1311 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1312 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1313 res = ListView.render("lists.json", lists: lists)
1317 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1318 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1319 {:ok, _list} <- Pleroma.List.delete(list) do
1327 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1328 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1329 res = ListView.render("list.json", list: list)
1334 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1336 |> Enum.each(fn account_id ->
1337 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1338 %User{} = followed <- User.get_cached_by_id(account_id) do
1339 Pleroma.List.follow(list, followed)
1346 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1348 |> Enum.each(fn account_id ->
1349 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1350 %User{} = followed <- User.get_cached_by_id(account_id) do
1351 Pleroma.List.unfollow(list, followed)
1358 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1359 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1360 {:ok, users} = Pleroma.List.get_following(list) do
1362 |> put_view(AccountView)
1363 |> render("accounts.json", %{for: user, users: users, as: :user})
1367 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1368 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1369 {:ok, list} <- Pleroma.List.rename(list, title) do
1370 res = ListView.render("list.json", list: list)
1378 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1379 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1382 |> Map.put("type", "Create")
1383 |> Map.put("blocking_user", user)
1384 |> Map.put("muting_user", user)
1386 # we must filter the following list for the user to avoid leaking statuses the user
1387 # does not actually have permission to see (for more info, peruse security issue #270).
1390 |> Enum.filter(fn x -> x in user.following end)
1391 |> ActivityPub.fetch_activities_bounded(following, params)
1395 |> put_view(StatusView)
1396 |> render("index.json", %{activities: activities, for: user, as: :activity})
1401 |> json(%{error: "Error."})
1405 def index(%{assigns: %{user: user}} = conn, _params) do
1406 token = get_session(conn, :oauth_token)
1409 mastodon_emoji = mastodonized_emoji()
1411 limit = Config.get([:instance, :limit])
1414 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1416 flavour = get_user_flavour(user)
1421 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1422 access_token: token,
1424 domain: Pleroma.Web.Endpoint.host(),
1427 unfollow_modal: false,
1430 auto_play_gif: false,
1431 display_sensitive_media: false,
1432 reduce_motion: false,
1433 max_toot_chars: limit,
1434 mascot: User.get_mascot(user)["url"]
1436 poll_limits: Config.get([:instance, :poll_limits]),
1438 delete_others_notice: present?(user.info.is_moderator),
1439 admin: present?(user.info.is_admin)
1443 default_privacy: user.info.default_scope,
1444 default_sensitive: false,
1445 allow_content_types: Config.get([:instance, :allowed_post_formats])
1447 media_attachments: %{
1448 accept_content_types: [
1464 user.info.settings ||
1494 push_subscription: nil,
1496 custom_emojis: mastodon_emoji,
1502 |> put_layout(false)
1503 |> put_view(MastodonView)
1504 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1507 |> put_session(:return_to, conn.request_path)
1508 |> redirect(to: "/web/login")
1512 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1513 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1515 with changeset <- Ecto.Changeset.change(user),
1516 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1517 {:ok, _user} <- User.update_and_set_cache(changeset) do
1522 |> put_resp_content_type("application/json")
1523 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1527 @supported_flavours ["glitch", "vanilla"]
1529 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1530 when flavour in @supported_flavours do
1531 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1533 with changeset <- Ecto.Changeset.change(user),
1534 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1535 {:ok, user} <- User.update_and_set_cache(changeset),
1536 flavour <- user.info.flavour do
1541 |> put_resp_content_type("application/json")
1542 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1546 def set_flavour(conn, _params) do
1549 |> json(%{error: "Unsupported flavour"})
1552 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1553 json(conn, get_user_flavour(user))
1556 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1560 defp get_user_flavour(_) do
1564 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1565 redirect(conn, to: local_mastodon_root_path(conn))
1568 @doc "Local Mastodon FE login init action"
1569 def login(conn, %{"code" => auth_token}) do
1570 with {:ok, app} <- get_or_make_app(),
1571 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1572 {:ok, token} <- Token.exchange_token(app, auth) do
1574 |> put_session(:oauth_token, token.token)
1575 |> redirect(to: local_mastodon_root_path(conn))
1579 @doc "Local Mastodon FE callback action"
1580 def login(conn, _) do
1581 with {:ok, app} <- get_or_make_app() do
1586 response_type: "code",
1587 client_id: app.client_id,
1589 scope: Enum.join(app.scopes, " ")
1592 redirect(conn, to: path)
1596 defp local_mastodon_root_path(conn) do
1597 case get_session(conn, :return_to) do
1599 mastodon_api_path(conn, :index, ["getting-started"])
1602 delete_session(conn, :return_to)
1607 defp get_or_make_app do
1608 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1609 scopes = ["read", "write", "follow", "push"]
1611 with %App{} = app <- Repo.get_by(App, find_attrs) do
1613 if app.scopes == scopes do
1617 |> Ecto.Changeset.change(%{scopes: scopes})
1625 App.register_changeset(
1627 Map.put(find_attrs, :scopes, scopes)
1634 def logout(conn, _) do
1637 |> redirect(to: "/")
1640 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1641 Logger.debug("Unimplemented, returning unmodified relationship")
1643 with %User{} = target <- User.get_cached_by_id(id) do
1645 |> put_view(AccountView)
1646 |> render("relationship.json", %{user: user, target: target})
1650 def empty_array(conn, _) do
1651 Logger.debug("Unimplemented, returning an empty array")
1655 def empty_object(conn, _) do
1656 Logger.debug("Unimplemented, returning an empty object")
1660 def get_filters(%{assigns: %{user: user}} = conn, _) do
1661 filters = Filter.get_filters(user)
1662 res = FilterView.render("filters.json", filters: filters)
1667 %{assigns: %{user: user}} = conn,
1668 %{"phrase" => phrase, "context" => context} = params
1674 hide: Map.get(params, "irreversible", false),
1675 whole_word: Map.get(params, "boolean", true)
1679 {:ok, response} = Filter.create(query)
1680 res = FilterView.render("filter.json", filter: response)
1684 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1685 filter = Filter.get(filter_id, user)
1686 res = FilterView.render("filter.json", filter: filter)
1691 %{assigns: %{user: user}} = conn,
1692 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1696 filter_id: filter_id,
1699 hide: Map.get(params, "irreversible", nil),
1700 whole_word: Map.get(params, "boolean", true)
1704 {:ok, response} = Filter.update(query)
1705 res = FilterView.render("filter.json", filter: response)
1709 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1712 filter_id: filter_id
1715 {:ok, _} = Filter.delete(query)
1721 def errors(conn, {:error, %Changeset{} = changeset}) do
1724 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1725 |> Enum.map_join(", ", fn {_k, v} -> v end)
1729 |> json(%{error: error_message})
1732 def errors(conn, {:error, :not_found}) do
1735 |> json(%{error: "Record not found"})
1738 def errors(conn, _) do
1741 |> json("Something went wrong")
1744 def suggestions(%{assigns: %{user: user}} = conn, _) do
1745 suggestions = Config.get(:suggestions)
1747 if Keyword.get(suggestions, :enabled, false) do
1748 api = Keyword.get(suggestions, :third_party_engine, "")
1749 timeout = Keyword.get(suggestions, :timeout, 5000)
1750 limit = Keyword.get(suggestions, :limit, 23)
1752 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1754 user = user.nickname
1758 |> String.replace("{{host}}", host)
1759 |> String.replace("{{user}}", user)
1761 with {:ok, %{status: 200, body: body}} <-
1766 recv_timeout: timeout,
1770 {:ok, data} <- Jason.decode(body) do
1773 |> Enum.slice(0, limit)
1778 case User.get_or_fetch(x["acct"]) do
1779 {:ok, %User{id: id}} -> id
1785 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1788 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1794 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1801 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1802 with %Activity{} = activity <- Activity.get_by_id(status_id),
1803 true <- Visibility.visible_for_user?(activity, user) do
1807 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1817 def reports(%{assigns: %{user: user}} = conn, params) do
1818 case CommonAPI.report(user, params) do
1821 |> put_view(ReportView)
1822 |> try_render("report.json", %{activity: activity})
1826 |> put_status(:bad_request)
1827 |> json(%{error: err})
1831 def account_register(
1832 %{assigns: %{app: app}} = conn,
1833 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1841 "captcha_answer_data",
1845 |> Map.put("nickname", nickname)
1846 |> Map.put("fullname", params["fullname"] || nickname)
1847 |> Map.put("bio", params["bio"] || "")
1848 |> Map.put("confirm", params["password"])
1850 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1851 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1853 token_type: "Bearer",
1854 access_token: token.token,
1856 created_at: Token.Utils.format_created_at(token)
1862 |> json(Jason.encode!(errors))
1866 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1869 |> json(%{error: "Missing parameters"})
1872 def account_register(conn, _) do
1875 |> json(%{error: "Invalid credentials"})
1878 def conversations(%{assigns: %{user: user}} = conn, params) do
1879 participations = Participation.for_user_with_last_activity_id(user, params)
1882 Enum.map(participations, fn participation ->
1883 ConversationView.render("participation.json", %{participation: participation, user: user})
1887 |> add_link_headers(:conversations, participations)
1888 |> json(conversations)
1891 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1892 with %Participation{} = participation <-
1893 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1894 {:ok, participation} <- Participation.mark_as_read(participation) do
1895 participation_view =
1896 ConversationView.render("participation.json", %{participation: participation, user: user})
1899 |> json(participation_view)
1903 def try_render(conn, target, params)
1904 when is_binary(target) do
1905 res = render(conn, target, params)
1910 |> json(%{error: "Can't display this activity"})
1916 def try_render(conn, _, _) do
1919 |> json(%{error: "Can't display this activity"})
1922 defp present?(nil), do: false
1923 defp present?(false), do: false
1924 defp present?(_), do: true