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"] || "")
142 |> Map.get(:emoji, [])
143 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
154 :skip_thread_containment
156 |> Enum.reduce(%{}, fn key, acc ->
157 add_if_present(acc, params, to_string(key), key, fn value ->
158 {:ok, ControllerHelper.truthy_param?(value)}
161 |> add_if_present(params, "default_scope", :default_scope)
162 |> add_if_present(params, "fields", :fields, fn fields ->
163 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
167 |> add_if_present(params, "fields", :raw_fields)
168 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
169 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
171 |> add_if_present(params, "header", :banner, fn value ->
172 with %Plug.Upload{} <- value,
173 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
179 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
180 with %Plug.Upload{} <- value,
181 {:ok, object} <- ActivityPub.upload(value, type: :background) do
187 |> Map.put(:emoji, user_info_emojis)
189 info_cng = User.Info.profile_update(user.info, info_params)
191 with changeset <- User.update_changeset(user, user_params),
192 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
193 {:ok, user} <- User.update_and_set_cache(changeset) do
194 if original_user != user do
195 CommonAPI.update(user)
200 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
203 _e -> render_error(conn, :forbidden, "Invalid request")
207 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
208 change = Changeset.change(user, %{avatar: nil})
209 {:ok, user} = User.update_and_set_cache(change)
210 CommonAPI.update(user)
212 json(conn, %{url: nil})
215 def update_avatar(%{assigns: %{user: user}} = conn, params) do
216 {:ok, object} = ActivityPub.upload(params, type: :avatar)
217 change = Changeset.change(user, %{avatar: object.data})
218 {:ok, user} = User.update_and_set_cache(change)
219 CommonAPI.update(user)
220 %{"url" => [%{"href" => href} | _]} = object.data
222 json(conn, %{url: href})
225 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
226 with new_info <- %{"banner" => %{}},
227 info_cng <- User.Info.profile_update(user.info, new_info),
228 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
229 {:ok, user} <- User.update_and_set_cache(changeset) do
230 CommonAPI.update(user)
232 json(conn, %{url: nil})
236 def update_banner(%{assigns: %{user: user}} = conn, params) do
237 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
238 new_info <- %{"banner" => object.data},
239 info_cng <- User.Info.profile_update(user.info, new_info),
240 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
241 {:ok, user} <- User.update_and_set_cache(changeset) do
242 CommonAPI.update(user)
243 %{"url" => [%{"href" => href} | _]} = object.data
245 json(conn, %{url: href})
249 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
250 with new_info <- %{"background" => %{}},
251 info_cng <- User.Info.profile_update(user.info, new_info),
252 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
253 {:ok, _user} <- User.update_and_set_cache(changeset) do
254 json(conn, %{url: nil})
258 def update_background(%{assigns: %{user: user}} = conn, params) do
259 with {:ok, object} <- ActivityPub.upload(params, type: :background),
260 new_info <- %{"background" => object.data},
261 info_cng <- User.Info.profile_update(user.info, new_info),
262 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
263 {:ok, _user} <- User.update_and_set_cache(changeset) do
264 %{"url" => [%{"href" => href} | _]} = object.data
266 json(conn, %{url: href})
270 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
271 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
274 AccountView.render("account.json", %{
277 with_pleroma_settings: true,
278 with_chat_token: chat_token
284 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
285 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
288 |> render("short.json", %{app: app})
292 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
293 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
294 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
295 account = AccountView.render("account.json", %{user: user, for: for_user})
298 _e -> render_error(conn, :not_found, "Can't find user")
302 @mastodon_api_level "2.7.2"
304 def masto_instance(conn, _params) do
305 instance = Config.get(:instance)
309 title: Keyword.get(instance, :name),
310 description: Keyword.get(instance, :description),
311 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
312 email: Keyword.get(instance, :email),
314 streaming_api: Pleroma.Web.Endpoint.websocket_url()
316 stats: Stats.get_stats(),
317 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
319 registrations: Pleroma.Config.get([:instance, :registrations_open]),
320 # Extra (not present in Mastodon):
321 max_toot_chars: Keyword.get(instance, :limit),
322 poll_limits: Keyword.get(instance, :poll_limits)
328 def peers(conn, _params) do
329 json(conn, Stats.get_peers())
332 defp mastodonized_emoji do
333 Pleroma.Emoji.get_all()
334 |> Enum.map(fn {shortcode, relative_url, tags} ->
335 url = to_string(URI.merge(Web.base_url(), relative_url))
338 "shortcode" => shortcode,
340 "visible_in_picker" => true,
343 # Assuming that a comma is authorized in the category name
344 "category" => (tags -- ["Custom"]) |> Enum.join(",")
349 def custom_emojis(conn, _params) do
350 mastodon_emoji = mastodonized_emoji()
351 json(conn, mastodon_emoji)
354 def home_timeline(%{assigns: %{user: user}} = conn, params) do
357 |> Map.put("type", ["Create", "Announce"])
358 |> Map.put("blocking_user", user)
359 |> Map.put("muting_user", user)
360 |> Map.put("user", user)
363 [user.ap_id | user.following]
364 |> ActivityPub.fetch_activities(params)
368 |> add_link_headers(:home_timeline, activities)
369 |> put_view(StatusView)
370 |> render("index.json", %{activities: activities, for: user, as: :activity})
373 def public_timeline(%{assigns: %{user: user}} = conn, params) do
374 local_only = params["local"] in [true, "True", "true", "1"]
378 |> Map.put("type", ["Create", "Announce"])
379 |> Map.put("local_only", local_only)
380 |> Map.put("blocking_user", user)
381 |> Map.put("muting_user", user)
382 |> Map.put("user", user)
383 |> ActivityPub.fetch_public_activities()
387 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
388 |> put_view(StatusView)
389 |> render("index.json", %{activities: activities, for: user, as: :activity})
392 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
393 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
396 |> Map.put("tag", params["tagged"])
398 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
401 |> add_link_headers(:user_statuses, activities, params["id"])
402 |> put_view(StatusView)
403 |> render("index.json", %{
404 activities: activities,
411 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
414 |> Map.put("type", "Create")
415 |> Map.put("blocking_user", user)
416 |> Map.put("user", user)
417 |> Map.put(:visibility, "direct")
421 |> ActivityPub.fetch_activities_query(params)
422 |> Pagination.fetch_paginated(params)
425 |> add_link_headers(:dm_timeline, activities)
426 |> put_view(StatusView)
427 |> render("index.json", %{activities: activities, for: user, as: :activity})
430 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
431 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
432 true <- Visibility.visible_for_user?(activity, user) do
434 |> put_view(StatusView)
435 |> try_render("status.json", %{activity: activity, for: user})
439 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
440 with %Activity{} = activity <- Activity.get_by_id(id),
442 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
443 "blocking_user" => user,
445 "exclude_id" => activity.id
447 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
453 activities: grouped_activities[true] || [],
457 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
462 activities: grouped_activities[false] || [],
466 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
473 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
474 with %Object{} = object <- Object.get_by_id(id),
475 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
476 true <- Visibility.visible_for_user?(activity, user) do
478 |> put_view(StatusView)
479 |> try_render("poll.json", %{object: object, for: user})
481 error when is_nil(error) or error == false ->
482 render_error(conn, :not_found, "Record not found")
486 defp get_cached_vote_or_vote(user, object, choices) do
487 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
490 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
491 case CommonAPI.vote(user, object, choices) do
492 {:error, _message} = res -> {:ignore, res}
493 res -> {:commit, res}
500 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
501 with %Object{} = object <- Object.get_by_id(id),
502 true <- object.data["type"] == "Question",
503 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
504 true <- Visibility.visible_for_user?(activity, user),
505 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
507 |> put_view(StatusView)
508 |> try_render("poll.json", %{object: object, for: user})
511 render_error(conn, :not_found, "Record not found")
514 render_error(conn, :not_found, "Record not found")
518 |> put_status(:unprocessable_entity)
519 |> json(%{error: message})
523 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
524 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
526 |> add_link_headers(:scheduled_statuses, scheduled_activities)
527 |> put_view(ScheduledActivityView)
528 |> render("index.json", %{scheduled_activities: scheduled_activities})
532 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
533 with %ScheduledActivity{} = scheduled_activity <-
534 ScheduledActivity.get(user, scheduled_activity_id) do
536 |> put_view(ScheduledActivityView)
537 |> render("show.json", %{scheduled_activity: scheduled_activity})
539 _ -> {:error, :not_found}
543 def update_scheduled_status(
544 %{assigns: %{user: user}} = conn,
545 %{"id" => scheduled_activity_id} = params
547 with %ScheduledActivity{} = scheduled_activity <-
548 ScheduledActivity.get(user, scheduled_activity_id),
549 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
551 |> put_view(ScheduledActivityView)
552 |> render("show.json", %{scheduled_activity: scheduled_activity})
554 nil -> {:error, :not_found}
559 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
560 with %ScheduledActivity{} = scheduled_activity <-
561 ScheduledActivity.get(user, scheduled_activity_id),
562 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
564 |> put_view(ScheduledActivityView)
565 |> render("show.json", %{scheduled_activity: scheduled_activity})
567 nil -> {:error, :not_found}
572 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
575 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
577 scheduled_at = params["scheduled_at"]
579 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
580 with {:ok, scheduled_activity} <-
581 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
583 |> put_view(ScheduledActivityView)
584 |> render("show.json", %{scheduled_activity: scheduled_activity})
587 params = Map.drop(params, ["scheduled_at"])
589 case CommonAPI.post(user, params) do
592 |> put_status(:unprocessable_entity)
593 |> json(%{error: message})
597 |> put_view(StatusView)
598 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
603 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
604 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
607 _e -> render_error(conn, :forbidden, "Can't delete this post")
611 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
612 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
613 %Activity{} = announce <- Activity.normalize(announce.data) do
615 |> put_view(StatusView)
616 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
620 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
621 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
622 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
624 |> put_view(StatusView)
625 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
629 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
630 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
631 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
633 |> put_view(StatusView)
634 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
638 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
639 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
640 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
642 |> put_view(StatusView)
643 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
647 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
648 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
650 |> put_view(StatusView)
651 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
655 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
656 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
658 |> put_view(StatusView)
659 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
663 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
664 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
665 %User{} = user <- User.get_cached_by_nickname(user.nickname),
666 true <- Visibility.visible_for_user?(activity, user),
667 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
669 |> put_view(StatusView)
670 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
674 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
675 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
676 %User{} = user <- User.get_cached_by_nickname(user.nickname),
677 true <- Visibility.visible_for_user?(activity, user),
678 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
680 |> put_view(StatusView)
681 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
685 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
686 activity = Activity.get_by_id(id)
688 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
690 |> put_view(StatusView)
691 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
695 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
696 activity = Activity.get_by_id(id)
698 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
700 |> put_view(StatusView)
701 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
705 def notifications(%{assigns: %{user: user}} = conn, params) do
706 notifications = MastodonAPI.get_notifications(user, params)
709 |> add_link_headers(:notifications, notifications)
710 |> put_view(NotificationView)
711 |> render("index.json", %{notifications: notifications, for: user})
714 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
715 with {:ok, notification} <- Notification.get(user, id) do
717 |> put_view(NotificationView)
718 |> render("show.json", %{notification: notification, for: user})
722 |> put_status(:forbidden)
723 |> json(%{"error" => reason})
727 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
728 Notification.clear(user)
732 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
733 with {:ok, _notif} <- Notification.dismiss(user, id) do
738 |> put_status(:forbidden)
739 |> json(%{"error" => reason})
743 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
744 Notification.destroy_multiple(user, ids)
748 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
750 q = from(u in User, where: u.id in ^id)
751 targets = Repo.all(q)
754 |> put_view(AccountView)
755 |> render("relationships.json", %{user: user, targets: targets})
758 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
759 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
761 def update_media(%{assigns: %{user: user}} = conn, data) do
762 with %Object{} = object <- Repo.get(Object, data["id"]),
763 true <- Object.authorize_mutation(object, user),
764 true <- is_binary(data["description"]),
765 description <- data["description"] do
766 new_data = %{object.data | "name" => description}
770 |> Object.change(%{data: new_data})
773 attachment_data = Map.put(new_data, "id", object.id)
776 |> put_view(StatusView)
777 |> render("attachment.json", %{attachment: attachment_data})
781 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
782 with {:ok, object} <-
785 actor: User.ap_id(user),
786 description: Map.get(data, "description")
788 attachment_data = Map.put(object.data, "id", object.id)
791 |> put_view(StatusView)
792 |> render("attachment.json", %{attachment: attachment_data})
796 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
797 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
798 %{} = attachment_data <- Map.put(object.data, "id", object.id),
799 %{type: type} = rendered <-
800 StatusView.render("attachment.json", %{attachment: attachment_data}) do
801 # Reject if not an image
802 if type == "image" do
804 # Save to the user's info
805 info_changeset = User.Info.mascot_update(user.info, rendered)
809 |> Ecto.Changeset.change()
810 |> Ecto.Changeset.put_embed(:info, info_changeset)
812 {:ok, _user} = User.update_and_set_cache(user_changeset)
817 render_error(conn, :unsupported_media_type, "mascots can only be images")
822 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
823 mascot = User.get_mascot(user)
829 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
830 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
831 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
832 q = from(u in User, where: u.ap_id in ^likes)
836 |> Enum.filter(&(not User.blocks?(user, &1)))
839 |> put_view(AccountView)
840 |> render("accounts.json", %{for: user, users: users, as: :user})
846 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
847 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
848 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
849 q = from(u in User, where: u.ap_id in ^announces)
853 |> Enum.filter(&(not User.blocks?(user, &1)))
856 |> put_view(AccountView)
857 |> render("accounts.json", %{for: user, users: users, as: :user})
863 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
864 local_only = params["local"] in [true, "True", "true", "1"]
867 [params["tag"], params["any"]]
871 |> Enum.map(&String.downcase(&1))
876 |> Enum.map(&String.downcase(&1))
881 |> Enum.map(&String.downcase(&1))
885 |> Map.put("type", "Create")
886 |> Map.put("local_only", local_only)
887 |> Map.put("blocking_user", user)
888 |> Map.put("muting_user", user)
889 |> Map.put("user", user)
890 |> Map.put("tag", tags)
891 |> Map.put("tag_all", tag_all)
892 |> Map.put("tag_reject", tag_reject)
893 |> ActivityPub.fetch_public_activities()
897 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
898 |> put_view(StatusView)
899 |> render("index.json", %{activities: activities, for: user, as: :activity})
902 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
903 with %User{} = user <- User.get_cached_by_id(id),
904 followers <- MastodonAPI.get_followers(user, params) do
907 for_user && user.id == for_user.id -> followers
908 user.info.hide_followers -> []
913 |> add_link_headers(:followers, followers, user)
914 |> put_view(AccountView)
915 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
919 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
920 with %User{} = user <- User.get_cached_by_id(id),
921 followers <- MastodonAPI.get_friends(user, params) do
924 for_user && user.id == for_user.id -> followers
925 user.info.hide_follows -> []
930 |> add_link_headers(:following, followers, user)
931 |> put_view(AccountView)
932 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
936 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
937 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
939 |> put_view(AccountView)
940 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
944 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
945 with %User{} = follower <- User.get_cached_by_id(id),
946 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
948 |> put_view(AccountView)
949 |> render("relationship.json", %{user: followed, target: follower})
953 |> put_status(:forbidden)
954 |> json(%{error: message})
958 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
959 with %User{} = follower <- User.get_cached_by_id(id),
960 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
962 |> put_view(AccountView)
963 |> render("relationship.json", %{user: followed, target: follower})
967 |> put_status(:forbidden)
968 |> json(%{error: message})
972 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
973 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
974 {_, true} <- {:followed, follower.id != followed.id},
975 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
977 |> put_view(AccountView)
978 |> render("relationship.json", %{user: follower, target: followed})
985 |> put_status(:forbidden)
986 |> json(%{error: message})
990 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
991 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
992 {_, true} <- {:followed, follower.id != followed.id},
993 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
995 |> put_view(AccountView)
996 |> render("account.json", %{user: followed, for: follower})
1001 {:error, message} ->
1003 |> put_status(:forbidden)
1004 |> json(%{error: message})
1008 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1009 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1010 {_, true} <- {:followed, follower.id != followed.id},
1011 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1013 |> put_view(AccountView)
1014 |> render("relationship.json", %{user: follower, target: followed})
1017 {:error, :not_found}
1024 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1026 if Map.has_key?(params, "notifications"),
1027 do: params["notifications"] in [true, "True", "true", "1"],
1030 with %User{} = muted <- User.get_cached_by_id(id),
1031 {:ok, muter} <- User.mute(muter, muted, notifications) do
1033 |> put_view(AccountView)
1034 |> render("relationship.json", %{user: muter, target: muted})
1036 {:error, message} ->
1038 |> put_status(:forbidden)
1039 |> json(%{error: message})
1043 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1044 with %User{} = muted <- User.get_cached_by_id(id),
1045 {:ok, muter} <- User.unmute(muter, muted) do
1047 |> put_view(AccountView)
1048 |> render("relationship.json", %{user: muter, target: muted})
1050 {:error, message} ->
1052 |> put_status(:forbidden)
1053 |> json(%{error: message})
1057 def mutes(%{assigns: %{user: user}} = conn, _) do
1058 with muted_accounts <- User.muted_users(user) do
1059 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1064 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1065 with %User{} = blocked <- User.get_cached_by_id(id),
1066 {:ok, blocker} <- User.block(blocker, blocked),
1067 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1069 |> put_view(AccountView)
1070 |> render("relationship.json", %{user: blocker, target: blocked})
1072 {:error, message} ->
1074 |> put_status(:forbidden)
1075 |> json(%{error: message})
1079 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1080 with %User{} = blocked <- User.get_cached_by_id(id),
1081 {:ok, blocker} <- User.unblock(blocker, blocked),
1082 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1084 |> put_view(AccountView)
1085 |> render("relationship.json", %{user: blocker, target: blocked})
1087 {:error, message} ->
1089 |> put_status(:forbidden)
1090 |> json(%{error: message})
1094 def blocks(%{assigns: %{user: user}} = conn, _) do
1095 with blocked_accounts <- User.blocked_users(user) do
1096 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1101 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1102 json(conn, info.domain_blocks || [])
1105 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1106 User.block_domain(blocker, domain)
1110 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1111 User.unblock_domain(blocker, domain)
1115 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1116 with %User{} = subscription_target <- User.get_cached_by_id(id),
1117 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1119 |> put_view(AccountView)
1120 |> render("relationship.json", %{user: user, target: subscription_target})
1122 {:error, message} ->
1124 |> put_status(:forbidden)
1125 |> json(%{error: message})
1129 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1130 with %User{} = subscription_target <- User.get_cached_by_id(id),
1131 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1133 |> put_view(AccountView)
1134 |> render("relationship.json", %{user: user, target: subscription_target})
1136 {:error, message} ->
1138 |> put_status(:forbidden)
1139 |> json(%{error: message})
1143 def favourites(%{assigns: %{user: user}} = conn, params) do
1146 |> Map.put("type", "Create")
1147 |> Map.put("favorited_by", user.ap_id)
1148 |> Map.put("blocking_user", user)
1151 ActivityPub.fetch_activities([], params)
1155 |> add_link_headers(:favourites, activities)
1156 |> put_view(StatusView)
1157 |> render("index.json", %{activities: activities, for: user, as: :activity})
1160 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1161 with %User{} = user <- User.get_by_id(id),
1162 false <- user.info.hide_favorites do
1165 |> Map.put("type", "Create")
1166 |> Map.put("favorited_by", user.ap_id)
1167 |> Map.put("blocking_user", for_user)
1171 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1173 [Pleroma.Constants.as_public()]
1178 |> ActivityPub.fetch_activities(params)
1182 |> add_link_headers(:favourites, activities)
1183 |> put_view(StatusView)
1184 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1186 nil -> {:error, :not_found}
1187 true -> render_error(conn, :forbidden, "Can't get favorites")
1191 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1192 user = User.get_cached_by_id(user.id)
1195 Bookmark.for_user_query(user.id)
1196 |> Pagination.fetch_paginated(params)
1200 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1203 |> add_link_headers(:bookmarks, bookmarks)
1204 |> put_view(StatusView)
1205 |> render("index.json", %{activities: activities, for: user, as: :activity})
1208 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1209 lists = Pleroma.List.for_user(user, opts)
1210 res = ListView.render("lists.json", lists: lists)
1214 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1215 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1216 res = ListView.render("list.json", list: list)
1219 _e -> render_error(conn, :not_found, "Record not found")
1223 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1224 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1225 res = ListView.render("lists.json", lists: lists)
1229 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1230 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1231 {:ok, _list} <- Pleroma.List.delete(list) do
1235 json(conn, dgettext("errors", "error"))
1239 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1240 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1241 res = ListView.render("list.json", list: list)
1246 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1248 |> Enum.each(fn account_id ->
1249 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1250 %User{} = followed <- User.get_cached_by_id(account_id) do
1251 Pleroma.List.follow(list, followed)
1258 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1260 |> Enum.each(fn account_id ->
1261 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1262 %User{} = followed <- User.get_cached_by_id(account_id) do
1263 Pleroma.List.unfollow(list, followed)
1270 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1271 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1272 {:ok, users} = Pleroma.List.get_following(list) do
1274 |> put_view(AccountView)
1275 |> render("accounts.json", %{for: user, users: users, as: :user})
1279 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1280 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1281 {:ok, list} <- Pleroma.List.rename(list, title) do
1282 res = ListView.render("list.json", list: list)
1286 json(conn, dgettext("errors", "error"))
1290 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1291 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1294 |> Map.put("type", "Create")
1295 |> Map.put("blocking_user", user)
1296 |> Map.put("user", user)
1297 |> Map.put("muting_user", user)
1299 # we must filter the following list for the user to avoid leaking statuses the user
1300 # does not actually have permission to see (for more info, peruse security issue #270).
1303 |> Enum.filter(fn x -> x in user.following end)
1304 |> ActivityPub.fetch_activities_bounded(following, params)
1308 |> put_view(StatusView)
1309 |> render("index.json", %{activities: activities, for: user, as: :activity})
1311 _e -> render_error(conn, :forbidden, "Error.")
1315 def index(%{assigns: %{user: user}} = conn, _params) do
1316 token = get_session(conn, :oauth_token)
1319 mastodon_emoji = mastodonized_emoji()
1321 limit = Config.get([:instance, :limit])
1324 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1329 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1330 access_token: token,
1332 domain: Pleroma.Web.Endpoint.host(),
1335 unfollow_modal: false,
1338 auto_play_gif: false,
1339 display_sensitive_media: false,
1340 reduce_motion: false,
1341 max_toot_chars: limit,
1342 mascot: User.get_mascot(user)["url"]
1344 poll_limits: Config.get([:instance, :poll_limits]),
1346 delete_others_notice: present?(user.info.is_moderator),
1347 admin: present?(user.info.is_admin)
1351 default_privacy: user.info.default_scope,
1352 default_sensitive: false,
1353 allow_content_types: Config.get([:instance, :allowed_post_formats])
1355 media_attachments: %{
1356 accept_content_types: [
1372 user.info.settings ||
1402 push_subscription: nil,
1404 custom_emojis: mastodon_emoji,
1410 |> put_layout(false)
1411 |> put_view(MastodonView)
1412 |> render("index.html", %{initial_state: initial_state})
1415 |> put_session(:return_to, conn.request_path)
1416 |> redirect(to: "/web/login")
1420 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1421 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1423 with changeset <- Ecto.Changeset.change(user),
1424 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1425 {:ok, _user} <- User.update_and_set_cache(changeset) do
1430 |> put_status(:internal_server_error)
1431 |> json(%{error: inspect(e)})
1435 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1436 redirect(conn, to: local_mastodon_root_path(conn))
1439 @doc "Local Mastodon FE login init action"
1440 def login(conn, %{"code" => auth_token}) do
1441 with {:ok, app} <- get_or_make_app(),
1442 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1443 {:ok, token} <- Token.exchange_token(app, auth) do
1445 |> put_session(:oauth_token, token.token)
1446 |> redirect(to: local_mastodon_root_path(conn))
1450 @doc "Local Mastodon FE callback action"
1451 def login(conn, _) do
1452 with {:ok, app} <- get_or_make_app() do
1457 response_type: "code",
1458 client_id: app.client_id,
1460 scope: Enum.join(app.scopes, " ")
1463 redirect(conn, to: path)
1467 defp local_mastodon_root_path(conn) do
1468 case get_session(conn, :return_to) do
1470 mastodon_api_path(conn, :index, ["getting-started"])
1473 delete_session(conn, :return_to)
1478 defp get_or_make_app do
1479 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1480 scopes = ["read", "write", "follow", "push"]
1482 with %App{} = app <- Repo.get_by(App, find_attrs) do
1484 if app.scopes == scopes do
1488 |> Ecto.Changeset.change(%{scopes: scopes})
1496 App.register_changeset(
1498 Map.put(find_attrs, :scopes, scopes)
1505 def logout(conn, _) do
1508 |> redirect(to: "/")
1511 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1512 Logger.debug("Unimplemented, returning unmodified relationship")
1514 with %User{} = target <- User.get_cached_by_id(id) do
1516 |> put_view(AccountView)
1517 |> render("relationship.json", %{user: user, target: target})
1521 def empty_array(conn, _) do
1522 Logger.debug("Unimplemented, returning an empty array")
1526 def empty_object(conn, _) do
1527 Logger.debug("Unimplemented, returning an empty object")
1531 def get_filters(%{assigns: %{user: user}} = conn, _) do
1532 filters = Filter.get_filters(user)
1533 res = FilterView.render("filters.json", filters: filters)
1538 %{assigns: %{user: user}} = conn,
1539 %{"phrase" => phrase, "context" => context} = params
1545 hide: Map.get(params, "irreversible", false),
1546 whole_word: Map.get(params, "boolean", true)
1550 {:ok, response} = Filter.create(query)
1551 res = FilterView.render("filter.json", filter: response)
1555 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1556 filter = Filter.get(filter_id, user)
1557 res = FilterView.render("filter.json", filter: filter)
1562 %{assigns: %{user: user}} = conn,
1563 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1567 filter_id: filter_id,
1570 hide: Map.get(params, "irreversible", nil),
1571 whole_word: Map.get(params, "boolean", true)
1575 {:ok, response} = Filter.update(query)
1576 res = FilterView.render("filter.json", filter: response)
1580 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1583 filter_id: filter_id
1586 {:ok, _} = Filter.delete(query)
1592 def errors(conn, {:error, %Changeset{} = changeset}) do
1595 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1596 |> Enum.map_join(", ", fn {_k, v} -> v end)
1599 |> put_status(:unprocessable_entity)
1600 |> json(%{error: error_message})
1603 def errors(conn, {:error, :not_found}) do
1604 render_error(conn, :not_found, "Record not found")
1607 def errors(conn, {:error, error_message}) do
1609 |> put_status(:bad_request)
1610 |> json(%{error: error_message})
1613 def errors(conn, _) do
1615 |> put_status(:internal_server_error)
1616 |> json(dgettext("errors", "Something went wrong"))
1619 def suggestions(%{assigns: %{user: user}} = conn, _) do
1620 suggestions = Config.get(:suggestions)
1622 if Keyword.get(suggestions, :enabled, false) do
1623 api = Keyword.get(suggestions, :third_party_engine, "")
1624 timeout = Keyword.get(suggestions, :timeout, 5000)
1625 limit = Keyword.get(suggestions, :limit, 23)
1627 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1629 user = user.nickname
1633 |> String.replace("{{host}}", host)
1634 |> String.replace("{{user}}", user)
1636 with {:ok, %{status: 200, body: body}} <-
1637 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1638 {:ok, data} <- Jason.decode(body) do
1641 |> Enum.slice(0, limit)
1644 |> Map.put("id", fetch_suggestion_id(x))
1645 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1646 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1652 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1659 defp fetch_suggestion_id(attrs) do
1660 case User.get_or_fetch(attrs["acct"]) do
1661 {:ok, %User{id: id}} -> id
1666 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1667 with %Activity{} = activity <- Activity.get_by_id(status_id),
1668 true <- Visibility.visible_for_user?(activity, user) do
1672 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1682 def reports(%{assigns: %{user: user}} = conn, params) do
1683 case CommonAPI.report(user, params) do
1686 |> put_view(ReportView)
1687 |> try_render("report.json", %{activity: activity})
1691 |> put_status(:bad_request)
1692 |> json(%{error: err})
1696 def account_register(
1697 %{assigns: %{app: app}} = conn,
1698 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1706 "captcha_answer_data",
1710 |> Map.put("nickname", nickname)
1711 |> Map.put("fullname", params["fullname"] || nickname)
1712 |> Map.put("bio", params["bio"] || "")
1713 |> Map.put("confirm", params["password"])
1715 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1716 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1718 token_type: "Bearer",
1719 access_token: token.token,
1721 created_at: Token.Utils.format_created_at(token)
1726 |> put_status(:bad_request)
1731 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1732 render_error(conn, :bad_request, "Missing parameters")
1735 def account_register(conn, _) do
1736 render_error(conn, :forbidden, "Invalid credentials")
1739 def conversations(%{assigns: %{user: user}} = conn, params) do
1740 participations = Participation.for_user_with_last_activity_id(user, params)
1743 Enum.map(participations, fn participation ->
1744 ConversationView.render("participation.json", %{participation: participation, for: user})
1748 |> add_link_headers(:conversations, participations)
1749 |> json(conversations)
1752 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1753 with %Participation{} = participation <-
1754 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1755 {:ok, participation} <- Participation.mark_as_read(participation) do
1756 participation_view =
1757 ConversationView.render("participation.json", %{participation: participation, for: user})
1760 |> json(participation_view)
1764 def password_reset(conn, params) do
1765 nickname_or_email = params["email"] || params["nickname"]
1767 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1769 |> put_status(:no_content)
1772 {:error, "unknown user"} ->
1773 send_resp(conn, :not_found, "")
1776 send_resp(conn, :bad_request, "")
1780 def account_confirmation_resend(conn, params) do
1781 nickname_or_email = params["email"] || params["nickname"]
1783 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1784 {:ok, _} <- User.try_send_confirmation_email(user) do
1786 |> json_response(:no_content, "")
1790 def try_render(conn, target, params)
1791 when is_binary(target) do
1792 case render(conn, target, params) do
1793 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1798 def try_render(conn, _, _) do
1799 render_error(conn, :not_implemented, "Can't display this activity")
1802 defp present?(nil), do: false
1803 defp present?(false), do: false
1804 defp present?(_), do: true