tests for /web/mastodon_api/mastodon_api.ex
[akkoma] / lib / pleroma / web / mastodon_api / mastodon_api_controller.ex
index 457709578f7fba823573d7f6f9d5495481910144..47e263aaee0bfa9567246b4f6abad086109674ed 100644 (file)
@@ -4,6 +4,9 @@
 
 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   use Pleroma.Web, :controller
+
+  import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
   alias Ecto.Changeset
   alias Pleroma.Activity
   alias Pleroma.Bookmark
@@ -15,6 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Pagination
+  alias Pleroma.Plugs.RateLimiter
   alias Pleroma.Repo
   alias Pleroma.ScheduledActivity
   alias Pleroma.Stats
@@ -45,9 +49,36 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   import Ecto.Query
 
   require Logger
+  require Pleroma.Constants
+
+  @rate_limited_relations_actions ~w(follow unfollow)a
+
+  @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
+    post_status delete_status)a
 
-  plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register)
-  plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
+  plug(
+    RateLimiter,
+    {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
+    when action in ~w(reblog_status unreblog_status)a
+  )
+
+  plug(
+    RateLimiter,
+    {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+    when action in ~w(fav_status unfav_status)a
+  )
+
+  plug(
+    RateLimiter,
+    {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
+  )
+
+  plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
+  plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
+  plug(RateLimiter, :app_account_creation when action == :account_register)
+  plug(RateLimiter, :search when action in [:search, :search2, :account_search])
+  plug(RateLimiter, :password_reset when action == :password_reset)
+  plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
 
   @local_mastodon_name "Mastodon-Local"
 
@@ -160,10 +191,70 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
       )
     else
-      _e ->
-        conn
-        |> put_status(403)
-        |> json(%{error: "Invalid request"})
+      _e -> render_error(conn, :forbidden, "Invalid request")
+    end
+  end
+
+  def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
+    change = Changeset.change(user, %{avatar: nil})
+    {:ok, user} = User.update_and_set_cache(change)
+    CommonAPI.update(user)
+
+    json(conn, %{url: nil})
+  end
+
+  def update_avatar(%{assigns: %{user: user}} = conn, params) do
+    {:ok, object} = ActivityPub.upload(params, type: :avatar)
+    change = Changeset.change(user, %{avatar: object.data})
+    {:ok, user} = User.update_and_set_cache(change)
+    CommonAPI.update(user)
+    %{"url" => [%{"href" => href} | _]} = object.data
+
+    json(conn, %{url: href})
+  end
+
+  def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
+    with new_info <- %{"banner" => %{}},
+         info_cng <- User.Info.profile_update(user.info, new_info),
+         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
+         {:ok, user} <- User.update_and_set_cache(changeset) do
+      CommonAPI.update(user)
+
+      json(conn, %{url: nil})
+    end
+  end
+
+  def update_banner(%{assigns: %{user: user}} = conn, params) do
+    with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
+         new_info <- %{"banner" => object.data},
+         info_cng <- User.Info.profile_update(user.info, new_info),
+         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
+         {:ok, user} <- User.update_and_set_cache(changeset) do
+      CommonAPI.update(user)
+      %{"url" => [%{"href" => href} | _]} = object.data
+
+      json(conn, %{url: href})
+    end
+  end
+
+  def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
+    with new_info <- %{"background" => %{}},
+         info_cng <- User.Info.profile_update(user.info, new_info),
+         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
+         {:ok, _user} <- User.update_and_set_cache(changeset) do
+      json(conn, %{url: nil})
+    end
+  end
+
+  def update_background(%{assigns: %{user: user}} = conn, params) do
+    with {:ok, object} <- ActivityPub.upload(params, type: :background),
+         new_info <- %{"background" => object.data},
+         info_cng <- User.Info.profile_update(user.info, new_info),
+         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
+         {:ok, _user} <- User.update_and_set_cache(changeset) do
+      %{"url" => [%{"href" => href} | _]} = object.data
+
+      json(conn, %{url: href})
     end
   end
 
@@ -195,10 +286,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       account = AccountView.render("account.json", %{user: user, for: for_user})
       json(conn, account)
     else
-      _e ->
-        conn
-        |> put_status(404)
-        |> json(%{error: "Can't find user"})
+      _e -> render_error(conn, :not_found, "Can't find user")
     end
   end
 
@@ -242,7 +330,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         "static_url" => url,
         "visible_in_picker" => true,
         "url" => url,
-        "tags" => tags
+        "tags" => tags,
+        # Assuming that a comma is authorized in the category name
+        "category" => (tags -- ["Custom"]) |> Enum.join(",")
       }
     end)
   end
@@ -355,7 +445,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
-    with %User{} = user <- User.get_cached_by_id(params["id"]) do
+    with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
+      params =
+        params
+        |> Map.put("tag", params["tagged"])
+
       activities = ActivityPub.fetch_user_activities(user, reading_user, params)
 
       conn
@@ -442,15 +536,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> put_view(StatusView)
       |> try_render("poll.json", %{object: object, for: user})
     else
-      nil ->
-        conn
-        |> put_status(404)
-        |> json(%{error: "Record not found"})
-
-      false ->
-        conn
-        |> put_status(404)
-        |> json(%{error: "Record not found"})
+      error when is_nil(error) or error == false ->
+        render_error(conn, :not_found, "Record not found")
     end
   end
 
@@ -479,18 +566,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> try_render("poll.json", %{object: object, for: user})
     else
       nil ->
-        conn
-        |> put_status(404)
-        |> json(%{error: "Record not found"})
+        render_error(conn, :not_found, "Record not found")
 
       false ->
-        conn
-        |> put_status(404)
-        |> json(%{error: "Record not found"})
+        render_error(conn, :not_found, "Record not found")
 
       {:error, message} ->
         conn
-        |> put_status(422)
+        |> put_status(:unprocessable_entity)
         |> json(%{error: message})
     end
   end
@@ -544,15 +627,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
-      when length(media_ids) > 0 do
-    params =
-      params
-      |> Map.put("status", ".")
-
-    post_status(conn, params)
-  end
-
   def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
     params =
       params
@@ -570,18 +644,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       params = Map.drop(params, ["scheduled_at"])
 
-      case get_cached_status_or_post(conn, params) do
-        {:ignore, message} ->
-          conn
-          |> put_status(422)
-          |> json(%{error: message})
-
+      case CommonAPI.post(user, params) do
         {:error, message} ->
           conn
-          |> put_status(422)
+          |> put_status(:unprocessable_entity)
           |> json(%{error: message})
 
-        {_, activity} ->
+        {:ok, activity} ->
           conn
           |> put_view(StatusView)
           |> try_render("status.json", %{activity: activity, for: user, as: :activity})
@@ -589,29 +658,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
-    idempotency_key =
-      case get_req_header(conn, "idempotency-key") do
-        [key] -> key
-        _ -> Ecto.UUID.generate()
-      end
-
-    Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
-      case CommonAPI.post(user, params) do
-        {:ok, activity} -> activity
-        {:error, message} -> {:ignore, message}
-      end
-    end)
-  end
-
   def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
     with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
       json(conn, %{})
     else
-      _e ->
-        conn
-        |> put_status(403)
-        |> json(%{error: "Can't delete this post"})
+      _e -> render_error(conn, :forbidden, "Can't delete this post")
     end
   end
 
@@ -656,11 +707,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       conn
       |> put_view(StatusView)
       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-    else
-      {:error, reason} ->
-        conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
     end
   end
 
@@ -701,11 +747,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       conn
       |> put_view(StatusView)
       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-    else
-      {:error, reason} ->
-        conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
     end
   end
 
@@ -736,8 +777,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       {:error, reason} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => reason}))
+        |> put_status(:forbidden)
+        |> json(%{"error" => reason})
     end
   end
 
@@ -752,8 +793,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       {:error, reason} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => reason}))
+        |> put_status(:forbidden)
+        |> json(%{"error" => reason})
     end
   end
 
@@ -831,9 +872,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         conn
         |> json(rendered)
       else
-        conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
+        render_error(conn, :unsupported_media_type, "mascots can only be images")
       end
     end
   end
@@ -846,24 +885,30 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
+    with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
          %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
       q = from(u in User, where: u.ap_id in ^likes)
-      users = Repo.all(q)
+
+      users =
+        Repo.all(q)
+        |> Enum.filter(&(not User.blocks?(user, &1)))
 
       conn
       |> put_view(AccountView)
-      |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
+      |> render("accounts.json", %{for: user, users: users, as: :user})
     else
       _ -> json(conn, [])
     end
   end
 
   def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
+    with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
          %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
       q = from(u in User, where: u.ap_id in ^announces)
-      users = Repo.all(q)
+
+      users =
+        Repo.all(q)
+        |> Enum.filter(&(not User.blocks?(user, &1)))
 
       conn
       |> put_view(AccountView)
@@ -962,8 +1007,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       {:error, message} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => message}))
+        |> put_status(:forbidden)
+        |> json(%{error: message})
     end
   end
 
@@ -976,8 +1021,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       {:error, message} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => message}))
+        |> put_status(:forbidden)
+        |> json(%{error: message})
     end
   end
 
@@ -994,8 +1039,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 
       {:error, message} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => message}))
+        |> put_status(:forbidden)
+        |> json(%{error: message})
     end
   end
 
@@ -1012,8 +1057,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 
       {:error, message} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => message}))
+        |> put_status(:forbidden)
+        |> json(%{error: message})
     end
   end
 
@@ -1033,17 +1078,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
+  def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
+    notifications =
+      if Map.has_key?(params, "notifications"),
+        do: params["notifications"] in [true, "True", "true", "1"],
+        else: true
+
     with %User{} = muted <- User.get_cached_by_id(id),
-         {:ok, muter} <- User.mute(muter, muted) do
+         {:ok, muter} <- User.mute(muter, muted, notifications) do
       conn
       |> put_view(AccountView)
       |> render("relationship.json", %{user: muter, target: muted})
     else
       {:error, message} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => message}))
+        |> put_status(:forbidden)
+        |> json(%{error: message})
     end
   end
 
@@ -1056,8 +1106,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       {:error, message} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => message}))
+        |> put_status(:forbidden)
+        |> json(%{error: message})
     end
   end
 
@@ -1078,8 +1128,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       {:error, message} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => message}))
+        |> put_status(:forbidden)
+        |> json(%{error: message})
     end
   end
 
@@ -1093,8 +1143,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       {:error, message} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => message}))
+        |> put_status(:forbidden)
+        |> json(%{error: message})
     end
   end
 
@@ -1128,8 +1178,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       {:error, message} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => message}))
+        |> put_status(:forbidden)
+        |> json(%{error: message})
     end
   end
 
@@ -1142,8 +1192,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       {:error, message} ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(403, Jason.encode!(%{"error" => message}))
+        |> put_status(:forbidden)
+        |> json(%{error: message})
     end
   end
 
@@ -1175,10 +1225,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 
       recipients =
         if for_user do
-          ["https://www.w3.org/ns/activitystreams#Public"] ++
-            [for_user.ap_id | for_user.following]
+          [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
         else
-          ["https://www.w3.org/ns/activitystreams#Public"]
+          [Pleroma.Constants.as_public()]
         end
 
       activities =
@@ -1191,13 +1240,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> put_view(StatusView)
       |> render("index.json", %{activities: activities, for: for_user, as: :activity})
     else
-      nil ->
-        {:error, :not_found}
-
-      true ->
-        conn
-        |> put_status(403)
-        |> json(%{error: "Can't get favorites"})
+      nil -> {:error, :not_found}
+      true -> render_error(conn, :forbidden, "Can't get favorites")
     end
   end
 
@@ -1229,10 +1273,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       res = ListView.render("list.json", list: list)
       json(conn, res)
     else
-      _e ->
-        conn
-        |> put_status(404)
-        |> json(%{error: "Record not found"})
+      _e -> render_error(conn, :not_found, "Record not found")
     end
   end
 
@@ -1248,7 +1289,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       json(conn, %{})
     else
       _e ->
-        json(conn, "error")
+        json(conn, dgettext("errors", "error"))
     end
   end
 
@@ -1299,7 +1340,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       json(conn, res)
     else
       _e ->
-        json(conn, "error")
+        json(conn, dgettext("errors", "error"))
     end
   end
 
@@ -1323,10 +1364,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> put_view(StatusView)
       |> render("index.json", %{activities: activities, for: user, as: :activity})
     else
-      _e ->
-        conn
-        |> put_status(403)
-        |> json(%{error: "Error."})
+      _e -> render_error(conn, :forbidden, "Error.")
     end
   end
 
@@ -1445,8 +1483,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       e ->
         conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
+        |> put_status(:internal_server_error)
+        |> json(%{error: inspect(e)})
     end
   end
 
@@ -1614,20 +1652,24 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.map_join(", ", fn {_k, v} -> v end)
 
     conn
-    |> put_status(422)
+    |> put_status(:unprocessable_entity)
     |> json(%{error: error_message})
   end
 
   def errors(conn, {:error, :not_found}) do
+    render_error(conn, :not_found, "Record not found")
+  end
+
+  def errors(conn, {:error, error_message}) do
     conn
-    |> put_status(404)
-    |> json(%{error: "Record not found"})
+    |> put_status(:bad_request)
+    |> json(%{error: error_message})
   end
 
   def errors(conn, _) do
     conn
-    |> put_status(500)
-    |> json("Something went wrong")
+    |> put_status(:internal_server_error)
+    |> json(dgettext("errors", "Something went wrong"))
   end
 
   def suggestions(%{assigns: %{user: user}} = conn, _) do
@@ -1648,45 +1690,35 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         |> String.replace("{{user}}", user)
 
       with {:ok, %{status: 200, body: body}} <-
-             HTTP.get(
-               url,
-               [],
-               adapter: [
-                 recv_timeout: timeout,
-                 pool: :default
-               ]
-             ),
+             HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
            {:ok, data} <- Jason.decode(body) do
         data =
           data
           |> Enum.slice(0, limit)
           |> Enum.map(fn x ->
-            Map.put(
-              x,
-              "id",
-              case User.get_or_fetch(x["acct"]) do
-                {:ok, %User{id: id}} -> id
-                _ -> 0
-              end
-            )
-          end)
-          |> Enum.map(fn x ->
-            Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
-          end)
-          |> Enum.map(fn x ->
-            Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
+            x
+            |> Map.put("id", fetch_suggestion_id(x))
+            |> Map.put("avatar", MediaProxy.url(x["avatar"]))
+            |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
           end)
 
-        conn
-        |> json(data)
+        json(conn, data)
       else
-        e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
+        e ->
+          Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
       end
     else
       json(conn, [])
     end
   end
 
+  defp fetch_suggestion_id(attrs) do
+    case User.get_or_fetch(attrs["acct"]) do
+      {:ok, %User{id: id}} -> id
+      _ -> 0
+    end
+  end
+
   def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
     with %Activity{} = activity <- Activity.get_by_id(status_id),
          true <- Visibility.visible_for_user?(activity, user) do
@@ -1747,21 +1779,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     else
       {:error, errors} ->
         conn
-        |> put_status(400)
-        |> json(Jason.encode!(errors))
+        |> put_status(:bad_request)
+        |> json(errors)
     end
   end
 
   def account_register(%{assigns: %{app: _app}} = conn, _params) do
-    conn
-    |> put_status(400)
-    |> json(%{error: "Missing parameters"})
+    render_error(conn, :bad_request, "Missing parameters")
   end
 
   def account_register(conn, _) do
-    conn
-    |> put_status(403)
-    |> json(%{error: "Invalid credentials"})
+    render_error(conn, :forbidden, "Invalid credentials")
   end
 
   def conversations(%{assigns: %{user: user}} = conn, params) do
@@ -1789,23 +1817,42 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def try_render(conn, target, params)
-      when is_binary(target) do
-    res = render(conn, target, params)
+  def password_reset(conn, params) do
+    nickname_or_email = params["email"] || params["nickname"]
 
-    if res == nil do
+    with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
       conn
-      |> put_status(501)
-      |> json(%{error: "Can't display this activity"})
+      |> put_status(:no_content)
+      |> json("")
     else
-      res
+      {:error, "unknown user"} ->
+        send_resp(conn, :not_found, "")
+
+      {:error, _} ->
+        send_resp(conn, :bad_request, "")
+    end
+  end
+
+  def account_confirmation_resend(conn, params) do
+    nickname_or_email = params["email"] || params["nickname"]
+
+    with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
+         {:ok, _} <- User.try_send_confirmation_email(user) do
+      conn
+      |> json_response(:no_content, "")
+    end
+  end
+
+  def try_render(conn, target, params)
+      when is_binary(target) do
+    case render(conn, target, params) do
+      nil -> render_error(conn, :not_implemented, "Can't display this activity")
+      res -> res
     end
   end
 
   def try_render(conn, _, _) do
-    conn
-    |> put_status(501)
-    |> json(%{error: "Can't display this activity"})
+    render_error(conn, :not_implemented, "Can't display this activity")
   end
 
   defp present?(nil), do: false