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
8 import Pleroma.Web.ControllerHelper,
9 only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
15 alias Pleroma.Conversation.Participation
17 alias Pleroma.Formatter
19 alias Pleroma.Notification
21 alias Pleroma.Pagination
22 alias Pleroma.Plugs.RateLimiter
24 alias Pleroma.ScheduledActivity
28 alias Pleroma.Web.ActivityPub.ActivityPub
29 alias Pleroma.Web.ActivityPub.Visibility
30 alias Pleroma.Web.CommonAPI
31 alias Pleroma.Web.MastodonAPI.AccountView
32 alias Pleroma.Web.MastodonAPI.AppView
33 alias Pleroma.Web.MastodonAPI.ConversationView
34 alias Pleroma.Web.MastodonAPI.FilterView
35 alias Pleroma.Web.MastodonAPI.ListView
36 alias Pleroma.Web.MastodonAPI.MastodonAPI
37 alias Pleroma.Web.MastodonAPI.MastodonView
38 alias Pleroma.Web.MastodonAPI.NotificationView
39 alias Pleroma.Web.MastodonAPI.ReportView
40 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
41 alias Pleroma.Web.MastodonAPI.StatusView
42 alias Pleroma.Web.MediaProxy
43 alias Pleroma.Web.OAuth.App
44 alias Pleroma.Web.OAuth.Authorization
45 alias Pleroma.Web.OAuth.Scopes
46 alias Pleroma.Web.OAuth.Token
47 alias Pleroma.Web.TwitterAPI.TwitterAPI
49 alias Pleroma.Web.ControllerHelper
53 require Pleroma.Constants
55 @rate_limited_relations_actions ~w(follow unfollow)a
57 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
58 post_status delete_status)a
62 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
63 when action in ~w(reblog_status unreblog_status)a
68 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
69 when action in ~w(fav_status unfav_status)a
74 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
77 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
78 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
79 plug(RateLimiter, :app_account_creation when action == :account_register)
80 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
81 plug(RateLimiter, :password_reset when action == :password_reset)
82 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
84 @local_mastodon_name "Mastodon-Local"
86 action_fallback(:errors)
88 def create_app(conn, params) do
89 scopes = Scopes.fetch_scopes(params, ["read"])
93 |> Map.drop(["scope", "scopes"])
94 |> Map.put("scopes", scopes)
96 with cs <- App.register_changeset(%App{}, app_attrs),
97 false <- cs.changes[:client_name] == @local_mastodon_name,
98 {:ok, app} <- Repo.insert(cs) do
101 |> render("show.json", %{app: app})
110 value_function \\ fn x -> {:ok, x} end
112 if Map.has_key?(params, params_field) do
113 case value_function.(params[params_field]) do
114 {:ok, new_value} -> Map.put(map, map_field, new_value)
122 def update_credentials(%{assigns: %{user: user}} = conn, params) do
127 |> add_if_present(params, "display_name", :name)
128 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
129 |> add_if_present(params, "avatar", :avatar, fn value ->
130 with %Plug.Upload{} <- value,
131 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
138 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
141 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
152 :skip_thread_containment
154 |> Enum.reduce(%{}, fn key, acc ->
155 add_if_present(acc, params, to_string(key), key, fn value ->
156 {:ok, ControllerHelper.truthy_param?(value)}
159 |> add_if_present(params, "default_scope", :default_scope)
160 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
161 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
163 |> add_if_present(params, "header", :banner, fn value ->
164 with %Plug.Upload{} <- value,
165 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
171 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
172 with %Plug.Upload{} <- value,
173 {:ok, object} <- ActivityPub.upload(value, type: :background) do
179 |> Map.put(:emoji, user_info_emojis)
181 info_cng = User.Info.profile_update(user.info, info_params)
183 with changeset <- User.update_changeset(user, user_params),
184 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
185 {:ok, user} <- User.update_and_set_cache(changeset) do
186 if original_user != user do
187 CommonAPI.update(user)
192 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
195 _e -> render_error(conn, :forbidden, "Invalid request")
199 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
200 change = Changeset.change(user, %{avatar: nil})
201 {:ok, user} = User.update_and_set_cache(change)
202 CommonAPI.update(user)
204 json(conn, %{url: nil})
207 def update_avatar(%{assigns: %{user: user}} = conn, params) do
208 {:ok, object} = ActivityPub.upload(params, type: :avatar)
209 change = Changeset.change(user, %{avatar: object.data})
210 {:ok, user} = User.update_and_set_cache(change)
211 CommonAPI.update(user)
212 %{"url" => [%{"href" => href} | _]} = object.data
214 json(conn, %{url: href})
217 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
218 with new_info <- %{"banner" => %{}},
219 info_cng <- User.Info.profile_update(user.info, new_info),
220 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
221 {:ok, user} <- User.update_and_set_cache(changeset) do
222 CommonAPI.update(user)
224 json(conn, %{url: nil})
228 def update_banner(%{assigns: %{user: user}} = conn, params) do
229 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
230 new_info <- %{"banner" => object.data},
231 info_cng <- User.Info.profile_update(user.info, new_info),
232 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
233 {:ok, user} <- User.update_and_set_cache(changeset) do
234 CommonAPI.update(user)
235 %{"url" => [%{"href" => href} | _]} = object.data
237 json(conn, %{url: href})
241 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
242 with new_info <- %{"background" => %{}},
243 info_cng <- User.Info.profile_update(user.info, new_info),
244 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
245 {:ok, _user} <- User.update_and_set_cache(changeset) do
246 json(conn, %{url: nil})
250 def update_background(%{assigns: %{user: user}} = conn, params) do
251 with {:ok, object} <- ActivityPub.upload(params, type: :background),
252 new_info <- %{"background" => object.data},
253 info_cng <- User.Info.profile_update(user.info, new_info),
254 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
255 {:ok, _user} <- User.update_and_set_cache(changeset) do
256 %{"url" => [%{"href" => href} | _]} = object.data
258 json(conn, %{url: href})
262 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
263 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
266 AccountView.render("account.json", %{
269 with_pleroma_settings: true,
270 with_chat_token: chat_token
276 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
277 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
280 |> render("short.json", %{app: app})
284 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
285 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
286 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
287 account = AccountView.render("account.json", %{user: user, for: for_user})
290 _e -> render_error(conn, :not_found, "Can't find user")
294 @mastodon_api_level "2.7.2"
296 def masto_instance(conn, _params) do
297 instance = Config.get(:instance)
301 title: Keyword.get(instance, :name),
302 description: Keyword.get(instance, :description),
303 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
304 email: Keyword.get(instance, :email),
306 streaming_api: Pleroma.Web.Endpoint.websocket_url()
308 stats: Stats.get_stats(),
309 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
311 registrations: Pleroma.Config.get([:instance, :registrations_open]),
312 # Extra (not present in Mastodon):
313 max_toot_chars: Keyword.get(instance, :limit),
314 poll_limits: Keyword.get(instance, :poll_limits)
320 def peers(conn, _params) do
321 json(conn, Stats.get_peers())
324 defp mastodonized_emoji do
325 Pleroma.Emoji.get_all()
326 |> Enum.map(fn {shortcode, relative_url, tags} ->
327 url = to_string(URI.merge(Web.base_url(), relative_url))
330 "shortcode" => shortcode,
332 "visible_in_picker" => true,
335 # Assuming that a comma is authorized in the category name
336 "category" => (tags -- ["Custom"]) |> Enum.join(",")
341 def custom_emojis(conn, _params) do
342 mastodon_emoji = mastodonized_emoji()
343 json(conn, mastodon_emoji)
346 def home_timeline(%{assigns: %{user: user}} = conn, params) do
349 |> Map.put("type", ["Create", "Announce"])
350 |> Map.put("blocking_user", user)
351 |> Map.put("muting_user", user)
352 |> Map.put("user", user)
355 [user.ap_id | user.following]
356 |> ActivityPub.fetch_activities(params)
360 |> add_link_headers(:home_timeline, activities)
361 |> put_view(StatusView)
362 |> render("index.json", %{activities: activities, for: user, as: :activity})
365 def public_timeline(%{assigns: %{user: user}} = conn, params) do
366 local_only = params["local"] in [true, "True", "true", "1"]
370 |> Map.put("type", ["Create", "Announce"])
371 |> Map.put("local_only", local_only)
372 |> Map.put("blocking_user", user)
373 |> Map.put("muting_user", user)
374 |> ActivityPub.fetch_public_activities()
378 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
379 |> put_view(StatusView)
380 |> render("index.json", %{activities: activities, for: user, as: :activity})
383 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
384 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
387 |> Map.put("tag", params["tagged"])
389 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
392 |> add_link_headers(:user_statuses, activities, params["id"])
393 |> put_view(StatusView)
394 |> render("index.json", %{
395 activities: activities,
402 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
405 |> Map.put("type", "Create")
406 |> Map.put("blocking_user", user)
407 |> Map.put("user", user)
408 |> Map.put(:visibility, "direct")
412 |> ActivityPub.fetch_activities_query(params)
413 |> Pagination.fetch_paginated(params)
416 |> add_link_headers(:dm_timeline, activities)
417 |> put_view(StatusView)
418 |> render("index.json", %{activities: activities, for: user, as: :activity})
421 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
422 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
423 true <- Visibility.visible_for_user?(activity, user) do
425 |> put_view(StatusView)
426 |> try_render("status.json", %{activity: activity, for: user})
430 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
431 with %Activity{} = activity <- Activity.get_by_id(id),
433 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
434 "blocking_user" => user,
438 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
440 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
441 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
447 activities: grouped_activities[true] || [],
451 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
456 activities: grouped_activities[false] || [],
460 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
467 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
468 with %Object{} = object <- Object.get_by_id(id),
469 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
470 true <- Visibility.visible_for_user?(activity, user) do
472 |> put_view(StatusView)
473 |> try_render("poll.json", %{object: object, for: user})
475 nil -> render_error(conn, :not_found, "Record not found")
476 false -> render_error(conn, :not_found, "Record not found")
480 defp get_cached_vote_or_vote(user, object, choices) do
481 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
484 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
485 case CommonAPI.vote(user, object, choices) do
486 {:error, _message} = res -> {:ignore, res}
487 res -> {:commit, res}
494 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
495 with %Object{} = object <- Object.get_by_id(id),
496 true <- object.data["type"] == "Question",
497 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
498 true <- Visibility.visible_for_user?(activity, user),
499 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
501 |> put_view(StatusView)
502 |> try_render("poll.json", %{object: object, for: user})
505 render_error(conn, :not_found, "Record not found")
508 render_error(conn, :not_found, "Record not found")
512 |> put_status(:unprocessable_entity)
513 |> json(%{error: message})
517 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
518 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
520 |> add_link_headers(:scheduled_statuses, scheduled_activities)
521 |> put_view(ScheduledActivityView)
522 |> render("index.json", %{scheduled_activities: scheduled_activities})
526 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
527 with %ScheduledActivity{} = scheduled_activity <-
528 ScheduledActivity.get(user, scheduled_activity_id) do
530 |> put_view(ScheduledActivityView)
531 |> render("show.json", %{scheduled_activity: scheduled_activity})
533 _ -> {:error, :not_found}
537 def update_scheduled_status(
538 %{assigns: %{user: user}} = conn,
539 %{"id" => scheduled_activity_id} = params
541 with %ScheduledActivity{} = scheduled_activity <-
542 ScheduledActivity.get(user, scheduled_activity_id),
543 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
545 |> put_view(ScheduledActivityView)
546 |> render("show.json", %{scheduled_activity: scheduled_activity})
548 nil -> {:error, :not_found}
553 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
554 with %ScheduledActivity{} = scheduled_activity <-
555 ScheduledActivity.get(user, scheduled_activity_id),
556 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
558 |> put_view(ScheduledActivityView)
559 |> render("show.json", %{scheduled_activity: scheduled_activity})
561 nil -> {:error, :not_found}
566 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
569 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
571 scheduled_at = params["scheduled_at"]
573 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
574 with {:ok, scheduled_activity} <-
575 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
577 |> put_view(ScheduledActivityView)
578 |> render("show.json", %{scheduled_activity: scheduled_activity})
581 params = Map.drop(params, ["scheduled_at"])
583 case CommonAPI.post(user, params) do
586 |> put_status(:unprocessable_entity)
587 |> json(%{error: message})
591 |> put_view(StatusView)
592 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
597 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
598 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
601 _e -> render_error(conn, :forbidden, "Can't delete this post")
605 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
606 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
607 %Activity{} = announce <- Activity.normalize(announce.data) do
609 |> put_view(StatusView)
610 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
614 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
615 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
616 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
618 |> put_view(StatusView)
619 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
623 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
624 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
625 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
627 |> put_view(StatusView)
628 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
632 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
633 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
634 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
636 |> put_view(StatusView)
637 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
641 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
642 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
644 |> put_view(StatusView)
645 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
649 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
650 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
657 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
658 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
659 %User{} = user <- User.get_cached_by_nickname(user.nickname),
660 true <- Visibility.visible_for_user?(activity, user),
661 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
663 |> put_view(StatusView)
664 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
668 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
669 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
670 %User{} = user <- User.get_cached_by_nickname(user.nickname),
671 true <- Visibility.visible_for_user?(activity, user),
672 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
674 |> put_view(StatusView)
675 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
679 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
680 activity = Activity.get_by_id(id)
682 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
684 |> put_view(StatusView)
685 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
689 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
690 activity = Activity.get_by_id(id)
692 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
694 |> put_view(StatusView)
695 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
699 def notifications(%{assigns: %{user: user}} = conn, params) do
700 notifications = MastodonAPI.get_notifications(user, params)
703 |> add_link_headers(:notifications, notifications)
704 |> put_view(NotificationView)
705 |> render("index.json", %{notifications: notifications, for: user})
708 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
709 with {:ok, notification} <- Notification.get(user, id) do
711 |> put_view(NotificationView)
712 |> render("show.json", %{notification: notification, for: user})
716 |> put_status(:forbidden)
717 |> json(%{"error" => reason})
721 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
722 Notification.clear(user)
726 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
727 with {:ok, _notif} <- Notification.dismiss(user, id) do
732 |> put_status(:forbidden)
733 |> json(%{"error" => reason})
737 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
738 Notification.destroy_multiple(user, ids)
742 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
744 q = from(u in User, where: u.id in ^id)
745 targets = Repo.all(q)
748 |> put_view(AccountView)
749 |> render("relationships.json", %{user: user, targets: targets})
752 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
753 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
755 def update_media(%{assigns: %{user: user}} = conn, data) do
756 with %Object{} = object <- Repo.get(Object, data["id"]),
757 true <- Object.authorize_mutation(object, user),
758 true <- is_binary(data["description"]),
759 description <- data["description"] do
760 new_data = %{object.data | "name" => description}
764 |> Object.change(%{data: new_data})
767 attachment_data = Map.put(new_data, "id", object.id)
770 |> put_view(StatusView)
771 |> render("attachment.json", %{attachment: attachment_data})
775 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
776 with {:ok, object} <-
779 actor: User.ap_id(user),
780 description: Map.get(data, "description")
782 attachment_data = Map.put(object.data, "id", object.id)
785 |> put_view(StatusView)
786 |> render("attachment.json", %{attachment: attachment_data})
790 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
791 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
792 %{} = attachment_data <- Map.put(object.data, "id", object.id),
793 %{type: type} = rendered <-
794 StatusView.render("attachment.json", %{attachment: attachment_data}) do
795 # Reject if not an image
796 if type == "image" do
798 # Save to the user's info
799 info_changeset = User.Info.mascot_update(user.info, rendered)
803 |> Ecto.Changeset.change()
804 |> Ecto.Changeset.put_embed(:info, info_changeset)
806 {:ok, _user} = User.update_and_set_cache(user_changeset)
811 render_error(conn, :unsupported_media_type, "mascots can only be images")
816 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
817 mascot = User.get_mascot(user)
823 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
824 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
825 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
826 q = from(u in User, where: u.ap_id in ^likes)
830 |> Enum.filter(&(not User.blocks?(user, &1)))
833 |> put_view(AccountView)
834 |> render("accounts.json", %{for: user, users: users, as: :user})
840 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
841 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
842 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
843 q = from(u in User, where: u.ap_id in ^announces)
847 |> Enum.filter(&(not User.blocks?(user, &1)))
850 |> put_view(AccountView)
851 |> render("accounts.json", %{for: user, users: users, as: :user})
857 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
858 local_only = params["local"] in [true, "True", "true", "1"]
861 [params["tag"], params["any"]]
865 |> Enum.map(&String.downcase(&1))
870 |> Enum.map(&String.downcase(&1))
875 |> Enum.map(&String.downcase(&1))
879 |> Map.put("type", "Create")
880 |> Map.put("local_only", local_only)
881 |> Map.put("blocking_user", user)
882 |> Map.put("muting_user", user)
883 |> Map.put("tag", tags)
884 |> Map.put("tag_all", tag_all)
885 |> Map.put("tag_reject", tag_reject)
886 |> ActivityPub.fetch_public_activities()
890 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
891 |> put_view(StatusView)
892 |> render("index.json", %{activities: activities, for: user, as: :activity})
895 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
896 with %User{} = user <- User.get_cached_by_id(id),
897 followers <- MastodonAPI.get_followers(user, params) do
900 for_user && user.id == for_user.id -> followers
901 user.info.hide_followers -> []
906 |> add_link_headers(:followers, followers, user)
907 |> put_view(AccountView)
908 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
912 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
913 with %User{} = user <- User.get_cached_by_id(id),
914 followers <- MastodonAPI.get_friends(user, params) do
917 for_user && user.id == for_user.id -> followers
918 user.info.hide_follows -> []
923 |> add_link_headers(:following, followers, user)
924 |> put_view(AccountView)
925 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
929 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
930 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
932 |> put_view(AccountView)
933 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
937 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
938 with %User{} = follower <- User.get_cached_by_id(id),
939 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
941 |> put_view(AccountView)
942 |> render("relationship.json", %{user: followed, target: follower})
946 |> put_status(:forbidden)
947 |> json(%{error: message})
951 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
952 with %User{} = follower <- User.get_cached_by_id(id),
953 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
955 |> put_view(AccountView)
956 |> render("relationship.json", %{user: followed, target: follower})
960 |> put_status(:forbidden)
961 |> json(%{error: message})
965 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
966 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
967 {_, true} <- {:followed, follower.id != followed.id},
968 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
970 |> put_view(AccountView)
971 |> render("relationship.json", %{user: follower, target: followed})
978 |> put_status(:forbidden)
979 |> json(%{error: message})
983 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
984 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
985 {_, true} <- {:followed, follower.id != followed.id},
986 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
988 |> put_view(AccountView)
989 |> render("account.json", %{user: followed, for: follower})
996 |> put_status(:forbidden)
997 |> json(%{error: message})
1001 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1002 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1003 {_, true} <- {:followed, follower.id != followed.id},
1004 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1006 |> put_view(AccountView)
1007 |> render("relationship.json", %{user: follower, target: followed})
1010 {:error, :not_found}
1017 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1019 if Map.has_key?(params, "notifications"),
1020 do: params["notifications"] in [true, "True", "true", "1"],
1023 with %User{} = muted <- User.get_cached_by_id(id),
1024 {:ok, muter} <- User.mute(muter, muted, notifications) do
1026 |> put_view(AccountView)
1027 |> render("relationship.json", %{user: muter, target: muted})
1029 {:error, message} ->
1031 |> put_status(:forbidden)
1032 |> json(%{error: message})
1036 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1037 with %User{} = muted <- User.get_cached_by_id(id),
1038 {:ok, muter} <- User.unmute(muter, muted) do
1040 |> put_view(AccountView)
1041 |> render("relationship.json", %{user: muter, target: muted})
1043 {:error, message} ->
1045 |> put_status(:forbidden)
1046 |> json(%{error: message})
1050 def mutes(%{assigns: %{user: user}} = conn, _) do
1051 with muted_accounts <- User.muted_users(user) do
1052 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1057 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1058 with %User{} = blocked <- User.get_cached_by_id(id),
1059 {:ok, blocker} <- User.block(blocker, blocked),
1060 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1062 |> put_view(AccountView)
1063 |> render("relationship.json", %{user: blocker, target: blocked})
1065 {:error, message} ->
1067 |> put_status(:forbidden)
1068 |> json(%{error: message})
1072 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1073 with %User{} = blocked <- User.get_cached_by_id(id),
1074 {:ok, blocker} <- User.unblock(blocker, blocked),
1075 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1077 |> put_view(AccountView)
1078 |> render("relationship.json", %{user: blocker, target: blocked})
1080 {:error, message} ->
1082 |> put_status(:forbidden)
1083 |> json(%{error: message})
1087 def blocks(%{assigns: %{user: user}} = conn, _) do
1088 with blocked_accounts <- User.blocked_users(user) do
1089 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1094 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1095 json(conn, info.domain_blocks || [])
1098 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1099 User.block_domain(blocker, domain)
1103 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1104 User.unblock_domain(blocker, domain)
1108 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1109 with %User{} = subscription_target <- User.get_cached_by_id(id),
1110 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1112 |> put_view(AccountView)
1113 |> render("relationship.json", %{user: user, target: subscription_target})
1115 {:error, message} ->
1117 |> put_status(:forbidden)
1118 |> json(%{error: message})
1122 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1123 with %User{} = subscription_target <- User.get_cached_by_id(id),
1124 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1126 |> put_view(AccountView)
1127 |> render("relationship.json", %{user: user, target: subscription_target})
1129 {:error, message} ->
1131 |> put_status(:forbidden)
1132 |> json(%{error: message})
1136 def favourites(%{assigns: %{user: user}} = conn, params) do
1139 |> Map.put("type", "Create")
1140 |> Map.put("favorited_by", user.ap_id)
1141 |> Map.put("blocking_user", user)
1144 ActivityPub.fetch_activities([], params)
1148 |> add_link_headers(:favourites, activities)
1149 |> put_view(StatusView)
1150 |> render("index.json", %{activities: activities, for: user, as: :activity})
1153 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1154 with %User{} = user <- User.get_by_id(id),
1155 false <- user.info.hide_favorites do
1158 |> Map.put("type", "Create")
1159 |> Map.put("favorited_by", user.ap_id)
1160 |> Map.put("blocking_user", for_user)
1164 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1166 [Pleroma.Constants.as_public()]
1171 |> ActivityPub.fetch_activities(params)
1175 |> add_link_headers(:favourites, activities)
1176 |> put_view(StatusView)
1177 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1179 nil -> {:error, :not_found}
1180 true -> render_error(conn, :forbidden, "Can't get favorites")
1184 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1185 user = User.get_cached_by_id(user.id)
1188 Bookmark.for_user_query(user.id)
1189 |> Pagination.fetch_paginated(params)
1193 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1196 |> add_link_headers(:bookmarks, bookmarks)
1197 |> put_view(StatusView)
1198 |> render("index.json", %{activities: activities, for: user, as: :activity})
1201 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1202 lists = Pleroma.List.for_user(user, opts)
1203 res = ListView.render("lists.json", lists: lists)
1207 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1208 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1209 res = ListView.render("list.json", list: list)
1212 _e -> render_error(conn, :not_found, "Record not found")
1216 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1217 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1218 res = ListView.render("lists.json", lists: lists)
1222 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1223 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1224 {:ok, _list} <- Pleroma.List.delete(list) do
1228 json(conn, dgettext("errors", "error"))
1232 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1233 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1234 res = ListView.render("list.json", list: list)
1239 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1241 |> Enum.each(fn account_id ->
1242 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1243 %User{} = followed <- User.get_cached_by_id(account_id) do
1244 Pleroma.List.follow(list, followed)
1251 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1253 |> Enum.each(fn account_id ->
1254 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1255 %User{} = followed <- User.get_cached_by_id(account_id) do
1256 Pleroma.List.unfollow(list, followed)
1263 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1264 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1265 {:ok, users} = Pleroma.List.get_following(list) do
1267 |> put_view(AccountView)
1268 |> render("accounts.json", %{for: user, users: users, as: :user})
1272 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1273 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1274 {:ok, list} <- Pleroma.List.rename(list, title) do
1275 res = ListView.render("list.json", list: list)
1279 json(conn, dgettext("errors", "error"))
1283 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1284 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1287 |> Map.put("type", "Create")
1288 |> Map.put("blocking_user", user)
1289 |> Map.put("muting_user", user)
1291 # we must filter the following list for the user to avoid leaking statuses the user
1292 # does not actually have permission to see (for more info, peruse security issue #270).
1295 |> Enum.filter(fn x -> x in user.following end)
1296 |> ActivityPub.fetch_activities_bounded(following, params)
1300 |> put_view(StatusView)
1301 |> render("index.json", %{activities: activities, for: user, as: :activity})
1303 _e -> render_error(conn, :forbidden, "Error.")
1307 def index(%{assigns: %{user: user}} = conn, _params) do
1308 token = get_session(conn, :oauth_token)
1311 mastodon_emoji = mastodonized_emoji()
1313 limit = Config.get([:instance, :limit])
1316 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1321 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1322 access_token: token,
1324 domain: Pleroma.Web.Endpoint.host(),
1327 unfollow_modal: false,
1330 auto_play_gif: false,
1331 display_sensitive_media: false,
1332 reduce_motion: false,
1333 max_toot_chars: limit,
1334 mascot: User.get_mascot(user)["url"]
1336 poll_limits: Config.get([:instance, :poll_limits]),
1338 delete_others_notice: present?(user.info.is_moderator),
1339 admin: present?(user.info.is_admin)
1343 default_privacy: user.info.default_scope,
1344 default_sensitive: false,
1345 allow_content_types: Config.get([:instance, :allowed_post_formats])
1347 media_attachments: %{
1348 accept_content_types: [
1364 user.info.settings ||
1394 push_subscription: nil,
1396 custom_emojis: mastodon_emoji,
1402 |> put_layout(false)
1403 |> put_view(MastodonView)
1404 |> render("index.html", %{initial_state: initial_state})
1407 |> put_session(:return_to, conn.request_path)
1408 |> redirect(to: "/web/login")
1412 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1413 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1415 with changeset <- Ecto.Changeset.change(user),
1416 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1417 {:ok, _user} <- User.update_and_set_cache(changeset) do
1422 |> put_status(:internal_server_error)
1423 |> json(%{error: inspect(e)})
1427 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1428 redirect(conn, to: local_mastodon_root_path(conn))
1431 @doc "Local Mastodon FE login init action"
1432 def login(conn, %{"code" => auth_token}) do
1433 with {:ok, app} <- get_or_make_app(),
1434 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1435 {:ok, token} <- Token.exchange_token(app, auth) do
1437 |> put_session(:oauth_token, token.token)
1438 |> redirect(to: local_mastodon_root_path(conn))
1442 @doc "Local Mastodon FE callback action"
1443 def login(conn, _) do
1444 with {:ok, app} <- get_or_make_app() do
1449 response_type: "code",
1450 client_id: app.client_id,
1452 scope: Enum.join(app.scopes, " ")
1455 redirect(conn, to: path)
1459 defp local_mastodon_root_path(conn) do
1460 case get_session(conn, :return_to) do
1462 mastodon_api_path(conn, :index, ["getting-started"])
1465 delete_session(conn, :return_to)
1470 defp get_or_make_app do
1471 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1472 scopes = ["read", "write", "follow", "push"]
1474 with %App{} = app <- Repo.get_by(App, find_attrs) do
1476 if app.scopes == scopes do
1480 |> Ecto.Changeset.change(%{scopes: scopes})
1488 App.register_changeset(
1490 Map.put(find_attrs, :scopes, scopes)
1497 def logout(conn, _) do
1500 |> redirect(to: "/")
1503 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1504 Logger.debug("Unimplemented, returning unmodified relationship")
1506 with %User{} = target <- User.get_cached_by_id(id) do
1508 |> put_view(AccountView)
1509 |> render("relationship.json", %{user: user, target: target})
1513 def empty_array(conn, _) do
1514 Logger.debug("Unimplemented, returning an empty array")
1518 def empty_object(conn, _) do
1519 Logger.debug("Unimplemented, returning an empty object")
1523 def get_filters(%{assigns: %{user: user}} = conn, _) do
1524 filters = Filter.get_filters(user)
1525 res = FilterView.render("filters.json", filters: filters)
1530 %{assigns: %{user: user}} = conn,
1531 %{"phrase" => phrase, "context" => context} = params
1537 hide: Map.get(params, "irreversible", false),
1538 whole_word: Map.get(params, "boolean", true)
1542 {:ok, response} = Filter.create(query)
1543 res = FilterView.render("filter.json", filter: response)
1547 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1548 filter = Filter.get(filter_id, user)
1549 res = FilterView.render("filter.json", filter: filter)
1554 %{assigns: %{user: user}} = conn,
1555 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1559 filter_id: filter_id,
1562 hide: Map.get(params, "irreversible", nil),
1563 whole_word: Map.get(params, "boolean", true)
1567 {:ok, response} = Filter.update(query)
1568 res = FilterView.render("filter.json", filter: response)
1572 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1575 filter_id: filter_id
1578 {:ok, _} = Filter.delete(query)
1584 def errors(conn, {:error, %Changeset{} = changeset}) do
1587 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1588 |> Enum.map_join(", ", fn {_k, v} -> v end)
1591 |> put_status(:unprocessable_entity)
1592 |> json(%{error: error_message})
1595 def errors(conn, {:error, :not_found}) do
1596 render_error(conn, :not_found, "Record not found")
1599 def errors(conn, {:error, error_message}) do
1601 |> put_status(:bad_request)
1602 |> json(%{error: error_message})
1605 def errors(conn, _) do
1607 |> put_status(:internal_server_error)
1608 |> json(dgettext("errors", "Something went wrong"))
1611 def suggestions(%{assigns: %{user: user}} = conn, _) do
1612 suggestions = Config.get(:suggestions)
1614 if Keyword.get(suggestions, :enabled, false) do
1615 api = Keyword.get(suggestions, :third_party_engine, "")
1616 timeout = Keyword.get(suggestions, :timeout, 5000)
1617 limit = Keyword.get(suggestions, :limit, 23)
1619 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1621 user = user.nickname
1625 |> String.replace("{{host}}", host)
1626 |> String.replace("{{user}}", user)
1628 with {:ok, %{status: 200, body: body}} <-
1633 recv_timeout: timeout,
1637 {:ok, data} <- Jason.decode(body) do
1640 |> Enum.slice(0, limit)
1645 case User.get_or_fetch(x["acct"]) do
1646 {:ok, %User{id: id}} -> id
1652 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1655 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1661 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1668 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1669 with %Activity{} = activity <- Activity.get_by_id(status_id),
1670 true <- Visibility.visible_for_user?(activity, user) do
1674 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1684 def reports(%{assigns: %{user: user}} = conn, params) do
1685 case CommonAPI.report(user, params) do
1688 |> put_view(ReportView)
1689 |> try_render("report.json", %{activity: activity})
1693 |> put_status(:bad_request)
1694 |> json(%{error: err})
1698 def account_register(
1699 %{assigns: %{app: app}} = conn,
1700 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1708 "captcha_answer_data",
1712 |> Map.put("nickname", nickname)
1713 |> Map.put("fullname", params["fullname"] || nickname)
1714 |> Map.put("bio", params["bio"] || "")
1715 |> Map.put("confirm", params["password"])
1717 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1718 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1720 token_type: "Bearer",
1721 access_token: token.token,
1723 created_at: Token.Utils.format_created_at(token)
1728 |> put_status(:bad_request)
1733 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1734 render_error(conn, :bad_request, "Missing parameters")
1737 def account_register(conn, _) do
1738 render_error(conn, :forbidden, "Invalid credentials")
1741 def conversations(%{assigns: %{user: user}} = conn, params) do
1742 participations = Participation.for_user_with_last_activity_id(user, params)
1745 Enum.map(participations, fn participation ->
1746 ConversationView.render("participation.json", %{participation: participation, for: user})
1750 |> add_link_headers(:conversations, participations)
1751 |> json(conversations)
1754 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1755 with %Participation{} = participation <-
1756 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1757 {:ok, participation} <- Participation.mark_as_read(participation) do
1758 participation_view =
1759 ConversationView.render("participation.json", %{participation: participation, for: user})
1762 |> json(participation_view)
1766 def password_reset(conn, params) do
1767 nickname_or_email = params["email"] || params["nickname"]
1769 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1771 |> put_status(:no_content)
1774 {:error, "unknown user"} ->
1775 send_resp(conn, :not_found, "")
1778 send_resp(conn, :bad_request, "")
1782 def account_confirmation_resend(conn, params) do
1783 nickname_or_email = params["email"] || params["nickname"]
1785 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1786 {:ok, _} <- User.try_send_confirmation_email(user) do
1788 |> json_response(:no_content, "")
1792 def try_render(conn, target, params)
1793 when is_binary(target) do
1794 case render(conn, target, params) do
1795 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1800 def try_render(conn, _, _) do
1801 render_error(conn, :not_implemented, "Can't display this activity")
1804 defp present?(nil), do: false
1805 defp present?(false), do: false
1806 defp present?(_), do: true