Commit 03a71eba authored by hakabahitoyo's avatar hakabahitoyo
Browse files

Merge remote-tracking branch 'official/develop' into deploy/distsn/clean-config

parents 05793250 005b4194
...@@ -39,7 +39,7 @@ While we don't provide docker files, other people have written very good ones. T ...@@ -39,7 +39,7 @@ While we don't provide docker files, other people have written very good ones. T
* Run `mix pleroma.instance gen`. This will ask you questions about your instance and generate a configuration file in `config/generated_config.exs`. Check that and copy it to either `config/dev.secret.exs` or `config/prod.secret.exs`. It will also create a `config/setup_db.psql`, which you should run as the PostgreSQL superuser (i.e., `sudo -u postgres psql -f config/setup_db.psql`). It will create the database, user, and password you gave `mix pleroma.gen.instance` earlier, as well as set up the necessary extensions in the database. PostgreSQL superuser privileges are only needed for this step. * Run `mix pleroma.instance gen`. This will ask you questions about your instance and generate a configuration file in `config/generated_config.exs`. Check that and copy it to either `config/dev.secret.exs` or `config/prod.secret.exs`. It will also create a `config/setup_db.psql`, which you should run as the PostgreSQL superuser (i.e., `sudo -u postgres psql -f config/setup_db.psql`). It will create the database, user, and password you gave `mix pleroma.gen.instance` earlier, as well as set up the necessary extensions in the database. PostgreSQL superuser privileges are only needed for this step.
* For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`. Documentation for the config can be found at [``config/config.md``](config/config.md) * For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`. Documentation for the config can be found at [``docs/config.md``](docs/config.md)
* Run `mix ecto.migrate` to run the database migrations. You will have to do this again after certain updates. * Run `mix ecto.migrate` to run the database migrations. You will have to do this again after certain updates.
......
...@@ -163,6 +163,8 @@ ...@@ -163,6 +163,8 @@
allow_followersonly: false, allow_followersonly: false,
allow_direct: false allow_direct: false
config :pleroma, :mrf_hellthread, threshold: 10
config :pleroma, :mrf_simple, config :pleroma, :mrf_simple,
media_removal: [], media_removal: [],
media_nsfw: [], media_nsfw: [],
......
...@@ -69,6 +69,7 @@ config :pleroma, Pleroma.Mailer, ...@@ -69,6 +69,7 @@ config :pleroma, Pleroma.Mailer,
* `banner_upload_limit`: File size limit of user’s profile banners * `banner_upload_limit`: File size limit of user’s profile banners
* `registrations_open`: Enable registrations for anyone, invitations can be enabled when false. * `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
* `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`). * `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
* `account_activation_required`: Require users to confirm their emails before signing in.
* `federating`: Enable federation with other instances * `federating`: Enable federation with other instances
* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance * `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance
* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default: * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
...@@ -120,6 +121,9 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i ...@@ -120,6 +121,9 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
* `allow_followersonly`: whether to allow followers-only posts * `allow_followersonly`: whether to allow followers-only posts
* `allow_direct`: whether to allow direct messages * `allow_direct`: whether to allow direct messages
## :mrf_hellthread
* `threshold`: Number of mentioned users after which the message gets discarded as spam
## :media_proxy ## :media_proxy
* `enabled`: Enables proxying of remote media to the instance’s proxy * `enabled`: Enables proxying of remote media to the instance’s proxy
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts. * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
......
...@@ -103,8 +103,8 @@ def run(["new", nickname, email | rest]) do ...@@ -103,8 +103,8 @@ def run(["new", nickname, email | rest]) do
bio: bio bio: bio
} }
user = User.register_changeset(%User{}, params) changeset = User.register_changeset(%User{}, params, confirmed: true)
Repo.insert!(user) {:ok, _user} = User.register(changeset)
Mix.shell().info("User #{nickname} created") Mix.shell().info("User #{nickname} created")
......
...@@ -15,6 +15,7 @@ defp sender do ...@@ -15,6 +15,7 @@ defp sender do
defp recipient(email, nil), do: email defp recipient(email, nil), do: email
defp recipient(email, name), do: {name, email} defp recipient(email, name), do: {name, email}
defp recipient(%Pleroma.User{} = user), do: recipient(user.email, user.name)
def password_reset_email(user, password_reset_token) when is_binary(password_reset_token) do def password_reset_email(user, password_reset_token) when is_binary(password_reset_token) do
password_reset_url = password_reset_url =
...@@ -32,7 +33,7 @@ def password_reset_email(user, password_reset_token) when is_binary(password_res ...@@ -32,7 +33,7 @@ def password_reset_email(user, password_reset_token) when is_binary(password_res
""" """
new() new()
|> to(recipient(user.email, user.name)) |> to(recipient(user))
|> from(sender()) |> from(sender())
|> subject("Password reset") |> subject("Password reset")
|> html_body(html_body) |> html_body(html_body)
...@@ -63,4 +64,26 @@ def user_invitation_email( ...@@ -63,4 +64,26 @@ def user_invitation_email(
|> subject("Invitation to #{instance_name()}") |> subject("Invitation to #{instance_name()}")
|> html_body(html_body) |> html_body(html_body)
end end
def account_confirmation_email(user) do
confirmation_url =
Router.Helpers.confirm_email_url(
Endpoint,
:confirm_email,
user.id,
to_string(user.info.confirmation_token)
)
html_body = """
<h3>Welcome to #{instance_name()}!</h3>
<p>Email confirmation is required to activate the account.</p>
<p>Click the following link to proceed: <a href="#{confirmation_url}">activate your account</a>.</p>
"""
new()
|> to(recipient(user))
|> from(sender())
|> subject("#{instance_name()} account confirmation")
|> html_body(html_body)
end
end end
...@@ -7,6 +7,9 @@ defmodule Pleroma.Formatter do ...@@ -7,6 +7,9 @@ defmodule Pleroma.Formatter do
@tag_regex ~r/((?<=[^&])|\A)(\#)(\w+)/u @tag_regex ~r/((?<=[^&])|\A)(\#)(\w+)/u
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/ @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
# Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
@mentions_regex ~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9_-](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u
def parse_tags(text, data \\ %{}) do def parse_tags(text, data \\ %{}) do
Regex.scan(@tag_regex, text) Regex.scan(@tag_regex, text)
|> Enum.map(fn ["#" <> tag = full_tag | _] -> {full_tag, String.downcase(tag)} end) |> Enum.map(fn ["#" <> tag = full_tag | _] -> {full_tag, String.downcase(tag)} end)
...@@ -17,16 +20,15 @@ def parse_tags(text, data \\ %{}) do ...@@ -17,16 +20,15 @@ def parse_tags(text, data \\ %{}) do
end).() end).()
end end
@doc "Parses mentions text and returns list {nickname, user}."
@spec parse_mentions(binary()) :: list({binary(), User.t()})
def parse_mentions(text) do def parse_mentions(text) do
# Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address Regex.scan(@mentions_regex, text)
regex =
~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9_-](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u
Regex.scan(regex, text)
|> List.flatten() |> List.flatten()
|> Enum.uniq() |> Enum.uniq()
|> Enum.map(fn "@" <> match = full_match -> |> Enum.map(fn nickname ->
{full_match, User.get_cached_by_nickname(match)} with nickname <- String.trim_leading(nickname, "@"),
do: {"@" <> nickname, User.get_cached_by_nickname(nickname)}
end) end)
|> Enum.filter(fn {_match, user} -> user end) |> Enum.filter(fn {_match, user} -> user end)
end end
......
...@@ -38,6 +38,13 @@ defmodule Pleroma.User do ...@@ -38,6 +38,13 @@ defmodule Pleroma.User do
timestamps() timestamps()
end end
def auth_active?(%User{} = user) do
(user.info && !user.info.confirmation_pending) ||
!Pleroma.Config.get([:instance, :account_activation_required])
end
def superuser?(%User{} = user), do: user.info && User.Info.superuser?(user.info)
def avatar_url(user) do def avatar_url(user) do
case user.avatar do case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href %{"url" => [%{"href" => href} | _]} -> href
...@@ -78,6 +85,7 @@ def user_info(%User{} = user) do ...@@ -78,6 +85,7 @@ def user_info(%User{} = user) do
note_count: user.info.note_count, note_count: user.info.note_count,
follower_count: user.info.follower_count, follower_count: user.info.follower_count,
locked: user.info.locked, locked: user.info.locked,
confirmation_pending: user.info.confirmation_pending,
default_scope: user.info.default_scope default_scope: user.info.default_scope
} }
end end
...@@ -168,7 +176,16 @@ def reset_password(user, data) do ...@@ -168,7 +176,16 @@ def reset_password(user, data) do
update_and_set_cache(password_update_changeset(user, data)) update_and_set_cache(password_update_changeset(user, data))
end end
def register_changeset(struct, params \\ %{}) do def register_changeset(struct, params \\ %{}, opts \\ []) do
confirmation_status =
if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
:confirmed
else
:unconfirmed
end
info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
changeset = changeset =
struct struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation]) |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
...@@ -180,7 +197,7 @@ def register_changeset(struct, params \\ %{}) do ...@@ -180,7 +197,7 @@ def register_changeset(struct, params \\ %{}) do
|> validate_format(:email, @email_regex) |> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000) |> validate_length(:bio, max: 1000)
|> validate_length(:name, min: 1, max: 100) |> validate_length(:name, min: 1, max: 100)
|> put_change(:info, %Pleroma.User.Info{}) |> put_change(:info, info_change)
if changeset.valid? do if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password]) hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
...@@ -197,6 +214,25 @@ def register_changeset(struct, params \\ %{}) do ...@@ -197,6 +214,25 @@ def register_changeset(struct, params \\ %{}) do
end end
end end
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
{:ok, _} = try_send_confirmation_email(user) do
{:ok, user}
end
end
def try_send_confirmation_email(%User{} = user) do
if user.info.confirmation_pending &&
Pleroma.Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.UserEmail.account_confirmation_email()
|> Pleroma.Mailer.deliver()
else
{:ok, :noop}
end
end
def needs_update?(%User{local: true}), do: false def needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
......
...@@ -9,6 +9,8 @@ defmodule Pleroma.User.Info do ...@@ -9,6 +9,8 @@ defmodule Pleroma.User.Info do
field(:note_count, :integer, default: 0) field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0) field(:follower_count, :integer, default: 0)
field(:locked, :boolean, default: false) field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public") field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: []) field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: []) field(:domain_blocks, {:array, :string}, default: [])
...@@ -35,6 +37,8 @@ defmodule Pleroma.User.Info do ...@@ -35,6 +37,8 @@ defmodule Pleroma.User.Info do
# subject _> Where is this used? # subject _> Where is this used?
end end
def superuser?(info), do: info.is_admin || info.is_moderator
def set_activation_status(info, deactivated) do def set_activation_status(info, deactivated) do
params = %{deactivated: deactivated} params = %{deactivated: deactivated}
...@@ -141,6 +145,24 @@ def profile_update(info, params) do ...@@ -141,6 +145,24 @@ def profile_update(info, params) do
]) ])
end end
def confirmation_changeset(info, :confirmed) do
confirmation_changeset(info, %{
confirmation_pending: false,
confirmation_token: nil
})
end
def confirmation_changeset(info, :unconfirmed) do
confirmation_changeset(info, %{
confirmation_pending: true,
confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
})
end
def confirmation_changeset(info, params) do
cast(info, params, [:confirmation_pending, :confirmation_token])
end
def mastodon_profile_update(info, params) do def mastodon_profile_update(info, params) do
info info
|> cast(params, [ |> cast(params, [
......
defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF
@impl true
def filter(%{"type" => "Create"} = object) do
threshold = Pleroma.Config.get([:mrf_hellthread, :threshold])
recipients = (object["to"] || []) ++ (object["cc"] || [])
if length(recipients) > threshold do
{:reject, nil}
else
{:ok, object}
end
end
@impl true
def filter(object), do: {:ok, object}
end
...@@ -69,8 +69,8 @@ def contain_origin_from_id(id, %{"id" => other_id} = _params) do ...@@ -69,8 +69,8 @@ def contain_origin_from_id(id, %{"id" => other_id} = _params) do
def fix_object(object) do def fix_object(object) do
object object
|> fix_actor |> fix_actor
|> fix_attachments
|> fix_url |> fix_url
|> fix_attachments
|> fix_context |> fix_context
|> fix_in_reply_to |> fix_in_reply_to
|> fix_emoji |> fix_emoji
...@@ -170,8 +170,14 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm ...@@ -170,8 +170,14 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
attachments = attachments =
attachment attachment
|> Enum.map(fn data -> |> Enum.map(fn data ->
url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}] media_type = data["mediaType"] || data["mimeType"]
Map.put(data, "url", url) href = data["url"] || data["href"]
url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
data
|> Map.put("mediaType", media_type)
|> Map.put("url", url)
end) end)
object object
...@@ -190,7 +196,22 @@ def fix_url(%{"url" => url} = object) when is_map(url) do ...@@ -190,7 +196,22 @@ def fix_url(%{"url" => url} = object) when is_map(url) do
|> Map.put("url", url["href"]) |> Map.put("url", url["href"])
end end
def fix_url(%{"url" => url} = object) when is_list(url) do def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0)
link_element =
url
|> Enum.filter(fn x -> is_map(x) end)
|> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
|> Enum.at(0)
object
|> Map.put("attachment", [first_element])
|> Map.put("url", link_element["href"])
end
def fix_url(%{"type" => object_type, "url" => url} = object)
when object_type != "Video" and is_list(url) do
first_element = Enum.at(url, 0) first_element = Enum.at(url, 0)
url_string = url_string =
......
defmodule Pleroma.Web.AdminAPI.AdminAPIController do defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.{User, Repo} alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper, only: [json_response: 3]
...@@ -26,7 +26,7 @@ def user_create( ...@@ -26,7 +26,7 @@ def user_create(
conn, conn,
%{"nickname" => nickname, "email" => email, "password" => password} %{"nickname" => nickname, "email" => email, "password" => password}
) do ) do
new_user = %{ user_data = %{
nickname: nickname, nickname: nickname,
name: nickname, name: nickname,
email: email, email: email,
...@@ -35,11 +35,11 @@ def user_create( ...@@ -35,11 +35,11 @@ def user_create(
bio: "." bio: "."
} }
User.register_changeset(%User{}, new_user) changeset = User.register_changeset(%User{}, user_data, confirmed: true)
|> Repo.insert!() {:ok, user} = User.register(changeset)
conn conn
|> json(new_user.nickname) |> json(user.nickname)
end end
def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
......
...@@ -110,7 +110,8 @@ def verify_credentials(%{assigns: %{user: user}} = conn, _) do ...@@ -110,7 +110,8 @@ def verify_credentials(%{assigns: %{user: user}} = conn, _) do
end end
def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id) do with %User{} = user <- Repo.get(User, id),
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
account = AccountView.render("account.json", %{user: user, for: for_user}) account = AccountView.render("account.json", %{user: user, for: for_user})
json(conn, account) json(conn, account)
else else
......
...@@ -62,6 +62,7 @@ def render("account.json", %{user: user} = opts) do ...@@ -62,6 +62,7 @@ def render("account.json", %{user: user} = opts) do
# Pleroma extension # Pleroma extension
pleroma: %{ pleroma: %{
confirmation_pending: user_info.confirmation_pending,
tags: user.tags tags: user.tags
} }
} }
......
...@@ -106,7 +106,6 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity} ...@@ -106,7 +106,6 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || []) favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
attachment_data = object["attachment"] || [] attachment_data = object["attachment"] || []
attachment_data = attachment_data ++ if object["type"] == "Video", do: [object], else: []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object["published"]) created_at = Utils.to_masto_date(object["published"])
......
...@@ -132,6 +132,7 @@ def nodeinfo(conn, %{"version" => "2.0"}) do ...@@ -132,6 +132,7 @@ def nodeinfo(conn, %{"version" => "2.0"}) do
banner: Keyword.get(instance, :banner_upload_limit), banner: Keyword.get(instance, :banner_upload_limit),
background: Keyword.get(instance, :background_upload_limit) background: Keyword.get(instance, :background_upload_limit)
}, },
accountActivationRequired: Keyword.get(instance, :account_activation_required, false),
invitesEnabled: Keyword.get(instance, :invites_enabled, false), invitesEnabled: Keyword.get(instance, :invites_enabled, false),
features: features features: features
} }
......
...@@ -31,6 +31,7 @@ def create_authorization(conn, %{ ...@@ -31,6 +31,7 @@ def create_authorization(conn, %{
}) do }) do
with %User{} = user <- User.get_by_nickname_or_email(name), with %User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash), true <- Pbkdf2.checkpw(password, user.password_hash),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
%App{} = app <- Repo.get_by(App, client_id: client_id), %App{} = app <- Repo.get_by(App, client_id: client_id),
{:ok, auth} <- Authorization.create_authorization(app, user) do {:ok, auth} <- Authorization.create_authorization(app, user) do
# Special case: Local MastodonFE. # Special case: Local MastodonFE.
...@@ -63,6 +64,15 @@ def create_authorization(conn, %{ ...@@ -63,6 +64,15 @@ def create_authorization(conn, %{
redirect(conn, external: url) redirect(conn, external: url)
end end
else
{:auth_active, false} ->
conn
|> put_flash(:error, "Account confirmation pending")
|> put_status(:forbidden)
|> authorize(params)
error ->
error
end end
end end
...@@ -101,6 +111,7 @@ def token_exchange( ...@@ -101,6 +111,7 @@ def token_exchange(
with %App{} = app <- get_app_from_request(conn, params), with %App{} = app <- get_app_from_request(conn, params),
%User{} = user <- User.get_by_nickname_or_email(name), %User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash), true <- Pbkdf2.checkpw(password, user.password_hash),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:ok, auth} <- Authorization.create_authorization(app, user), {:ok, auth} <- Authorization.create_authorization(app, user),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
response = %{ response = %{
...@@ -113,6 +124,11 @@ def token_exchange( ...@@ -113,6 +124,11 @@ def token_exchange(
json(conn, response) json(conn, response)
else else
{:auth_active, false} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Account confirmation pending"})
_error -> _error ->
put_status(conn, 400) put_status(conn, 400)
|> json(%{error: "Invalid credentials"}) |> json(%{error: "Invalid credentials"})
......
...@@ -283,6 +283,15 @@ defmodule Pleroma.Web.Router do ...@@ -283,6 +283,15 @@ defmodule Pleroma.Web.Router do
post("/account/register", TwitterAPI.Controller, :register) post("/account/register", TwitterAPI.Controller, :register)
post("/account/password_reset", TwitterAPI.Controller, :password_reset) post("/account/password_reset", TwitterAPI.Controller, :password_reset)
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
:confirm_email,