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 |> Map.put("user", user)
375 |> ActivityPub.fetch_public_activities()
379 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
380 |> put_view(StatusView)
381 |> render("index.json", %{activities: activities, for: user, as: :activity})
384 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
385 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
388 |> Map.put("tag", params["tagged"])
390 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
393 |> add_link_headers(:user_statuses, activities, params["id"])
394 |> put_view(StatusView)
395 |> render("index.json", %{
396 activities: activities,
403 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
406 |> Map.put("type", "Create")
407 |> Map.put("blocking_user", user)
408 |> Map.put("user", user)
409 |> Map.put(:visibility, "direct")
413 |> ActivityPub.fetch_activities_query(params)
414 |> Pagination.fetch_paginated(params)
417 |> add_link_headers(:dm_timeline, activities)
418 |> put_view(StatusView)
419 |> render("index.json", %{activities: activities, for: user, as: :activity})
422 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
423 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
424 true <- Visibility.visible_for_user?(activity, user) do
426 |> put_view(StatusView)
427 |> try_render("status.json", %{activity: activity, for: user})
431 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
432 with %Activity{} = activity <- Activity.get_by_id(id),
434 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
435 "blocking_user" => user,
437 "exclude_id" => activity.id
439 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
445 activities: grouped_activities[true] || [],
449 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
454 activities: grouped_activities[false] || [],
458 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
465 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
466 with %Object{} = object <- Object.get_by_id(id),
467 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
468 true <- Visibility.visible_for_user?(activity, user) do
470 |> put_view(StatusView)
471 |> try_render("poll.json", %{object: object, for: user})
473 error when is_nil(error) or error == false ->
474 render_error(conn, :not_found, "Record not found")
478 defp get_cached_vote_or_vote(user, object, choices) do
479 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
482 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
483 case CommonAPI.vote(user, object, choices) do
484 {:error, _message} = res -> {:ignore, res}
485 res -> {:commit, res}
492 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
493 with %Object{} = object <- Object.get_by_id(id),
494 true <- object.data["type"] == "Question",
495 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
496 true <- Visibility.visible_for_user?(activity, user),
497 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
499 |> put_view(StatusView)
500 |> try_render("poll.json", %{object: object, for: user})
503 render_error(conn, :not_found, "Record not found")
506 render_error(conn, :not_found, "Record not found")
510 |> put_status(:unprocessable_entity)
511 |> json(%{error: message})
515 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
516 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
518 |> add_link_headers(:scheduled_statuses, scheduled_activities)
519 |> put_view(ScheduledActivityView)
520 |> render("index.json", %{scheduled_activities: scheduled_activities})
524 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
525 with %ScheduledActivity{} = scheduled_activity <-
526 ScheduledActivity.get(user, scheduled_activity_id) do
528 |> put_view(ScheduledActivityView)
529 |> render("show.json", %{scheduled_activity: scheduled_activity})
531 _ -> {:error, :not_found}
535 def update_scheduled_status(
536 %{assigns: %{user: user}} = conn,
537 %{"id" => scheduled_activity_id} = params
539 with %ScheduledActivity{} = scheduled_activity <-
540 ScheduledActivity.get(user, scheduled_activity_id),
541 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
543 |> put_view(ScheduledActivityView)
544 |> render("show.json", %{scheduled_activity: scheduled_activity})
546 nil -> {:error, :not_found}
551 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
552 with %ScheduledActivity{} = scheduled_activity <-
553 ScheduledActivity.get(user, scheduled_activity_id),
554 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
556 |> put_view(ScheduledActivityView)
557 |> render("show.json", %{scheduled_activity: scheduled_activity})
559 nil -> {:error, :not_found}
564 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
567 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
569 scheduled_at = params["scheduled_at"]
571 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
572 with {:ok, scheduled_activity} <-
573 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
575 |> put_view(ScheduledActivityView)
576 |> render("show.json", %{scheduled_activity: scheduled_activity})
579 params = Map.drop(params, ["scheduled_at"])
581 case CommonAPI.post(user, params) do
584 |> put_status(:unprocessable_entity)
585 |> json(%{error: message})
589 |> put_view(StatusView)
590 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
595 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
596 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
599 _e -> render_error(conn, :forbidden, "Can't delete this post")
603 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
604 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
605 %Activity{} = announce <- Activity.normalize(announce.data) do
607 |> put_view(StatusView)
608 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
612 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
613 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
614 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
616 |> put_view(StatusView)
617 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
621 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
622 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
623 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
625 |> put_view(StatusView)
626 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
630 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
631 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
632 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
634 |> put_view(StatusView)
635 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
639 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
640 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
642 |> put_view(StatusView)
643 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
647 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
648 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
650 |> put_view(StatusView)
651 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
655 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
656 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
657 %User{} = user <- User.get_cached_by_nickname(user.nickname),
658 true <- Visibility.visible_for_user?(activity, user),
659 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
661 |> put_view(StatusView)
662 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
666 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
667 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
668 %User{} = user <- User.get_cached_by_nickname(user.nickname),
669 true <- Visibility.visible_for_user?(activity, user),
670 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
672 |> put_view(StatusView)
673 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
677 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
678 activity = Activity.get_by_id(id)
680 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
682 |> put_view(StatusView)
683 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
687 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
688 activity = Activity.get_by_id(id)
690 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
692 |> put_view(StatusView)
693 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
697 def notifications(%{assigns: %{user: user}} = conn, params) do
698 notifications = MastodonAPI.get_notifications(user, params)
701 |> add_link_headers(:notifications, notifications)
702 |> put_view(NotificationView)
703 |> render("index.json", %{notifications: notifications, for: user})
706 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
707 with {:ok, notification} <- Notification.get(user, id) do
709 |> put_view(NotificationView)
710 |> render("show.json", %{notification: notification, for: user})
714 |> put_status(:forbidden)
715 |> json(%{"error" => reason})
719 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
720 Notification.clear(user)
724 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
725 with {:ok, _notif} <- Notification.dismiss(user, id) do
730 |> put_status(:forbidden)
731 |> json(%{"error" => reason})
735 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
736 Notification.destroy_multiple(user, ids)
740 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
742 q = from(u in User, where: u.id in ^id)
743 targets = Repo.all(q)
746 |> put_view(AccountView)
747 |> render("relationships.json", %{user: user, targets: targets})
750 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
751 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
753 def update_media(%{assigns: %{user: user}} = conn, data) do
754 with %Object{} = object <- Repo.get(Object, data["id"]),
755 true <- Object.authorize_mutation(object, user),
756 true <- is_binary(data["description"]),
757 description <- data["description"] do
758 new_data = %{object.data | "name" => description}
762 |> Object.change(%{data: new_data})
765 attachment_data = Map.put(new_data, "id", object.id)
768 |> put_view(StatusView)
769 |> render("attachment.json", %{attachment: attachment_data})
773 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
774 with {:ok, object} <-
777 actor: User.ap_id(user),
778 description: Map.get(data, "description")
780 attachment_data = Map.put(object.data, "id", object.id)
783 |> put_view(StatusView)
784 |> render("attachment.json", %{attachment: attachment_data})
788 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
789 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
790 %{} = attachment_data <- Map.put(object.data, "id", object.id),
791 %{type: type} = rendered <-
792 StatusView.render("attachment.json", %{attachment: attachment_data}) do
793 # Reject if not an image
794 if type == "image" do
796 # Save to the user's info
797 info_changeset = User.Info.mascot_update(user.info, rendered)
801 |> Ecto.Changeset.change()
802 |> Ecto.Changeset.put_embed(:info, info_changeset)
804 {:ok, _user} = User.update_and_set_cache(user_changeset)
809 render_error(conn, :unsupported_media_type, "mascots can only be images")
814 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
815 mascot = User.get_mascot(user)
821 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
822 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
823 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
824 q = from(u in User, where: u.ap_id in ^likes)
828 |> Enum.filter(&(not User.blocks?(user, &1)))
831 |> put_view(AccountView)
832 |> render("accounts.json", %{for: user, users: users, as: :user})
838 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
839 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
840 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
841 q = from(u in User, where: u.ap_id in ^announces)
845 |> Enum.filter(&(not User.blocks?(user, &1)))
848 |> put_view(AccountView)
849 |> render("accounts.json", %{for: user, users: users, as: :user})
855 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
856 local_only = params["local"] in [true, "True", "true", "1"]
859 [params["tag"], params["any"]]
863 |> Enum.map(&String.downcase(&1))
868 |> Enum.map(&String.downcase(&1))
873 |> Enum.map(&String.downcase(&1))
877 |> Map.put("type", "Create")
878 |> Map.put("local_only", local_only)
879 |> Map.put("blocking_user", user)
880 |> Map.put("muting_user", user)
881 |> Map.put("user", user)
882 |> Map.put("tag", tags)
883 |> Map.put("tag_all", tag_all)
884 |> Map.put("tag_reject", tag_reject)
885 |> ActivityPub.fetch_public_activities()
889 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
890 |> put_view(StatusView)
891 |> render("index.json", %{activities: activities, for: user, as: :activity})
894 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
895 with %User{} = user <- User.get_cached_by_id(id),
896 followers <- MastodonAPI.get_followers(user, params) do
899 for_user && user.id == for_user.id -> followers
900 user.info.hide_followers -> []
905 |> add_link_headers(:followers, followers, user)
906 |> put_view(AccountView)
907 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
911 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
912 with %User{} = user <- User.get_cached_by_id(id),
913 followers <- MastodonAPI.get_friends(user, params) do
916 for_user && user.id == for_user.id -> followers
917 user.info.hide_follows -> []
922 |> add_link_headers(:following, followers, user)
923 |> put_view(AccountView)
924 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
928 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
929 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
931 |> put_view(AccountView)
932 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
936 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
937 with %User{} = follower <- User.get_cached_by_id(id),
938 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
940 |> put_view(AccountView)
941 |> render("relationship.json", %{user: followed, target: follower})
945 |> put_status(:forbidden)
946 |> json(%{error: message})
950 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
951 with %User{} = follower <- User.get_cached_by_id(id),
952 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
954 |> put_view(AccountView)
955 |> render("relationship.json", %{user: followed, target: follower})
959 |> put_status(:forbidden)
960 |> json(%{error: message})
964 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
965 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
966 {_, true} <- {:followed, follower.id != followed.id},
967 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
969 |> put_view(AccountView)
970 |> render("relationship.json", %{user: follower, target: followed})
977 |> put_status(:forbidden)
978 |> json(%{error: message})
982 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
983 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
984 {_, true} <- {:followed, follower.id != followed.id},
985 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
987 |> put_view(AccountView)
988 |> render("account.json", %{user: followed, for: follower})
995 |> put_status(:forbidden)
996 |> json(%{error: message})
1000 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1001 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1002 {_, true} <- {:followed, follower.id != followed.id},
1003 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1005 |> put_view(AccountView)
1006 |> render("relationship.json", %{user: follower, target: followed})
1009 {:error, :not_found}
1016 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1018 if Map.has_key?(params, "notifications"),
1019 do: params["notifications"] in [true, "True", "true", "1"],
1022 with %User{} = muted <- User.get_cached_by_id(id),
1023 {:ok, muter} <- User.mute(muter, muted, notifications) do
1025 |> put_view(AccountView)
1026 |> render("relationship.json", %{user: muter, target: muted})
1028 {:error, message} ->
1030 |> put_status(:forbidden)
1031 |> json(%{error: message})
1035 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1036 with %User{} = muted <- User.get_cached_by_id(id),
1037 {:ok, muter} <- User.unmute(muter, muted) do
1039 |> put_view(AccountView)
1040 |> render("relationship.json", %{user: muter, target: muted})
1042 {:error, message} ->
1044 |> put_status(:forbidden)
1045 |> json(%{error: message})
1049 def mutes(%{assigns: %{user: user}} = conn, _) do
1050 with muted_accounts <- User.muted_users(user) do
1051 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1056 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1057 with %User{} = blocked <- User.get_cached_by_id(id),
1058 {:ok, blocker} <- User.block(blocker, blocked),
1059 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1061 |> put_view(AccountView)
1062 |> render("relationship.json", %{user: blocker, target: blocked})
1064 {:error, message} ->
1066 |> put_status(:forbidden)
1067 |> json(%{error: message})
1071 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1072 with %User{} = blocked <- User.get_cached_by_id(id),
1073 {:ok, blocker} <- User.unblock(blocker, blocked),
1074 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1076 |> put_view(AccountView)
1077 |> render("relationship.json", %{user: blocker, target: blocked})
1079 {:error, message} ->
1081 |> put_status(:forbidden)
1082 |> json(%{error: message})
1086 def blocks(%{assigns: %{user: user}} = conn, _) do
1087 with blocked_accounts <- User.blocked_users(user) do
1088 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1093 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1094 json(conn, info.domain_blocks || [])
1097 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1098 User.block_domain(blocker, domain)
1102 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1103 User.unblock_domain(blocker, domain)
1107 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1108 with %User{} = subscription_target <- User.get_cached_by_id(id),
1109 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1111 |> put_view(AccountView)
1112 |> render("relationship.json", %{user: user, target: subscription_target})
1114 {:error, message} ->
1116 |> put_status(:forbidden)
1117 |> json(%{error: message})
1121 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1122 with %User{} = subscription_target <- User.get_cached_by_id(id),
1123 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1125 |> put_view(AccountView)
1126 |> render("relationship.json", %{user: user, target: subscription_target})
1128 {:error, message} ->
1130 |> put_status(:forbidden)
1131 |> json(%{error: message})
1135 def favourites(%{assigns: %{user: user}} = conn, params) do
1138 |> Map.put("type", "Create")
1139 |> Map.put("favorited_by", user.ap_id)
1140 |> Map.put("blocking_user", user)
1143 ActivityPub.fetch_activities([], params)
1147 |> add_link_headers(:favourites, activities)
1148 |> put_view(StatusView)
1149 |> render("index.json", %{activities: activities, for: user, as: :activity})
1152 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1153 with %User{} = user <- User.get_by_id(id),
1154 false <- user.info.hide_favorites do
1157 |> Map.put("type", "Create")
1158 |> Map.put("favorited_by", user.ap_id)
1159 |> Map.put("blocking_user", for_user)
1163 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1165 [Pleroma.Constants.as_public()]
1170 |> ActivityPub.fetch_activities(params)
1174 |> add_link_headers(:favourites, activities)
1175 |> put_view(StatusView)
1176 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1178 nil -> {:error, :not_found}
1179 true -> render_error(conn, :forbidden, "Can't get favorites")
1183 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1184 user = User.get_cached_by_id(user.id)
1187 Bookmark.for_user_query(user.id)
1188 |> Pagination.fetch_paginated(params)
1192 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1195 |> add_link_headers(:bookmarks, bookmarks)
1196 |> put_view(StatusView)
1197 |> render("index.json", %{activities: activities, for: user, as: :activity})
1200 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1201 lists = Pleroma.List.for_user(user, opts)
1202 res = ListView.render("lists.json", lists: lists)
1206 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1207 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1208 res = ListView.render("list.json", list: list)
1211 _e -> render_error(conn, :not_found, "Record not found")
1215 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1216 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1217 res = ListView.render("lists.json", lists: lists)
1221 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1222 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1223 {:ok, _list} <- Pleroma.List.delete(list) do
1227 json(conn, dgettext("errors", "error"))
1231 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1232 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1233 res = ListView.render("list.json", list: list)
1238 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1240 |> Enum.each(fn account_id ->
1241 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1242 %User{} = followed <- User.get_cached_by_id(account_id) do
1243 Pleroma.List.follow(list, followed)
1250 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1252 |> Enum.each(fn account_id ->
1253 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1254 %User{} = followed <- User.get_cached_by_id(account_id) do
1255 Pleroma.List.unfollow(list, followed)
1262 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1263 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1264 {:ok, users} = Pleroma.List.get_following(list) do
1266 |> put_view(AccountView)
1267 |> render("accounts.json", %{for: user, users: users, as: :user})
1271 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1272 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1273 {:ok, list} <- Pleroma.List.rename(list, title) do
1274 res = ListView.render("list.json", list: list)
1278 json(conn, dgettext("errors", "error"))
1282 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1283 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1286 |> Map.put("type", "Create")
1287 |> Map.put("blocking_user", user)
1288 |> Map.put("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}} <-
1629 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1630 {:ok, data} <- Jason.decode(body) do
1633 |> Enum.slice(0, limit)
1636 |> Map.put("id", fetch_suggestion_id(x))
1637 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1638 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1644 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1651 defp fetch_suggestion_id(attrs) do
1652 case User.get_or_fetch(attrs["acct"]) do
1653 {:ok, %User{id: id}} -> id
1658 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1659 with %Activity{} = activity <- Activity.get_by_id(status_id),
1660 true <- Visibility.visible_for_user?(activity, user) do
1664 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1674 def reports(%{assigns: %{user: user}} = conn, params) do
1675 case CommonAPI.report(user, params) do
1678 |> put_view(ReportView)
1679 |> try_render("report.json", %{activity: activity})
1683 |> put_status(:bad_request)
1684 |> json(%{error: err})
1688 def account_register(
1689 %{assigns: %{app: app}} = conn,
1690 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1698 "captcha_answer_data",
1702 |> Map.put("nickname", nickname)
1703 |> Map.put("fullname", params["fullname"] || nickname)
1704 |> Map.put("bio", params["bio"] || "")
1705 |> Map.put("confirm", params["password"])
1707 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1708 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1710 token_type: "Bearer",
1711 access_token: token.token,
1713 created_at: Token.Utils.format_created_at(token)
1718 |> put_status(:bad_request)
1723 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1724 render_error(conn, :bad_request, "Missing parameters")
1727 def account_register(conn, _) do
1728 render_error(conn, :forbidden, "Invalid credentials")
1731 def conversations(%{assigns: %{user: user}} = conn, params) do
1732 participations = Participation.for_user_with_last_activity_id(user, params)
1735 Enum.map(participations, fn participation ->
1736 ConversationView.render("participation.json", %{participation: participation, for: user})
1740 |> add_link_headers(:conversations, participations)
1741 |> json(conversations)
1744 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1745 with %Participation{} = participation <-
1746 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1747 {:ok, participation} <- Participation.mark_as_read(participation) do
1748 participation_view =
1749 ConversationView.render("participation.json", %{participation: participation, for: user})
1752 |> json(participation_view)
1756 def password_reset(conn, params) do
1757 nickname_or_email = params["email"] || params["nickname"]
1759 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1761 |> put_status(:no_content)
1764 {:error, "unknown user"} ->
1765 send_resp(conn, :not_found, "")
1768 send_resp(conn, :bad_request, "")
1772 def account_confirmation_resend(conn, params) do
1773 nickname_or_email = params["email"] || params["nickname"]
1775 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1776 {:ok, _} <- User.try_send_confirmation_email(user) do
1778 |> json_response(:no_content, "")
1782 def try_render(conn, target, params)
1783 when is_binary(target) do
1784 case render(conn, target, params) do
1785 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1790 def try_render(conn, _, _) do
1791 render_error(conn, :not_implemented, "Can't display this activity")
1794 defp present?(nil), do: false
1795 defp present?(false), do: false
1796 defp present?(_), do: true