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: 2, truthy_param?: 1]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
18 alias Pleroma.Pagination
19 alias Pleroma.Plugs.RateLimiter
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Visibility
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.MastodonAPI.AccountView
28 alias Pleroma.Web.MastodonAPI.AppView
29 alias Pleroma.Web.MastodonAPI.ListView
30 alias Pleroma.Web.MastodonAPI.MastodonAPI
31 alias Pleroma.Web.MastodonAPI.MastodonView
32 alias Pleroma.Web.MastodonAPI.StatusView
33 alias Pleroma.Web.MediaProxy
34 alias Pleroma.Web.OAuth.App
35 alias Pleroma.Web.OAuth.Authorization
36 alias Pleroma.Web.OAuth.Scopes
37 alias Pleroma.Web.OAuth.Token
38 alias Pleroma.Web.TwitterAPI.TwitterAPI
41 require Pleroma.Constants
43 @rate_limited_relations_actions ~w(follow unfollow)a
47 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
50 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
51 plug(RateLimiter, :app_account_creation when action == :account_register)
52 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
53 plug(RateLimiter, :password_reset when action == :password_reset)
54 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
56 @local_mastodon_name "Mastodon-Local"
58 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
60 def create_app(conn, params) do
61 scopes = Scopes.fetch_scopes(params, ["read"])
65 |> Map.drop(["scope", "scopes"])
66 |> Map.put("scopes", scopes)
68 with cs <- App.register_changeset(%App{}, app_attrs),
69 false <- cs.changes[:client_name] == @local_mastodon_name,
70 {:ok, app} <- Repo.insert(cs) do
73 |> render("show.json", %{app: app})
82 value_function \\ fn x -> {:ok, x} end
84 if Map.has_key?(params, params_field) do
85 case value_function.(params[params_field]) do
86 {:ok, new_value} -> Map.put(map, map_field, new_value)
94 def update_credentials(%{assigns: %{user: user}} = conn, params) do
99 |> add_if_present(params, "display_name", :name)
100 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
101 |> add_if_present(params, "avatar", :avatar, fn value ->
102 with %Plug.Upload{} <- value,
103 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
110 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
114 |> Map.get(:emoji, [])
115 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
122 :hide_followers_count,
128 :skip_thread_containment,
131 |> Enum.reduce(%{}, fn key, acc ->
132 add_if_present(acc, params, to_string(key), key, fn value ->
133 {:ok, truthy_param?(value)}
136 |> add_if_present(params, "default_scope", :default_scope)
137 |> add_if_present(params, "fields", :fields, fn fields ->
138 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
142 |> add_if_present(params, "fields", :raw_fields)
143 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
144 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
146 |> add_if_present(params, "header", :banner, fn value ->
147 with %Plug.Upload{} <- value,
148 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
154 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
155 with %Plug.Upload{} <- value,
156 {:ok, object} <- ActivityPub.upload(value, type: :background) do
162 |> Map.put(:emoji, user_info_emojis)
166 |> User.update_changeset(user_params)
167 |> User.change_info(&User.Info.profile_update(&1, info_params))
169 with {:ok, user} <- User.update_and_set_cache(changeset) do
170 if original_user != user, do: CommonAPI.update(user)
174 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
177 _e -> render_error(conn, :forbidden, "Invalid request")
181 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
182 change = Changeset.change(user, %{avatar: nil})
183 {:ok, user} = User.update_and_set_cache(change)
184 CommonAPI.update(user)
186 json(conn, %{url: nil})
189 def update_avatar(%{assigns: %{user: user}} = conn, params) do
190 {:ok, object} = ActivityPub.upload(params, type: :avatar)
191 change = Changeset.change(user, %{avatar: object.data})
192 {:ok, user} = User.update_and_set_cache(change)
193 CommonAPI.update(user)
194 %{"url" => [%{"href" => href} | _]} = object.data
196 json(conn, %{url: href})
199 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
200 new_info = %{"banner" => %{}}
202 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
203 CommonAPI.update(user)
204 json(conn, %{url: nil})
208 def update_banner(%{assigns: %{user: user}} = conn, params) do
209 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
210 new_info <- %{"banner" => object.data},
211 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
212 CommonAPI.update(user)
213 %{"url" => [%{"href" => href} | _]} = object.data
215 json(conn, %{url: href})
219 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
220 new_info = %{"background" => %{}}
222 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
223 json(conn, %{url: nil})
227 def update_background(%{assigns: %{user: user}} = conn, params) do
228 with {:ok, object} <- ActivityPub.upload(params, type: :background),
229 new_info <- %{"background" => object.data},
230 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
231 %{"url" => [%{"href" => href} | _]} = object.data
233 json(conn, %{url: href})
237 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
238 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
241 AccountView.render("account.json", %{
244 with_pleroma_settings: true,
245 with_chat_token: chat_token
251 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
252 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
255 |> render("short.json", %{app: app})
259 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
260 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
261 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
262 account = AccountView.render("account.json", %{user: user, for: for_user})
265 _e -> render_error(conn, :not_found, "Can't find user")
269 @mastodon_api_level "2.7.2"
271 def masto_instance(conn, _params) do
272 instance = Config.get(:instance)
276 title: Keyword.get(instance, :name),
277 description: Keyword.get(instance, :description),
278 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
279 email: Keyword.get(instance, :email),
281 streaming_api: Pleroma.Web.Endpoint.websocket_url()
283 stats: Stats.get_stats(),
284 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
286 registrations: Pleroma.Config.get([:instance, :registrations_open]),
287 # Extra (not present in Mastodon):
288 max_toot_chars: Keyword.get(instance, :limit),
289 poll_limits: Keyword.get(instance, :poll_limits)
295 def peers(conn, _params) do
296 json(conn, Stats.get_peers())
299 defp mastodonized_emoji do
300 Pleroma.Emoji.get_all()
301 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
302 url = to_string(URI.merge(Web.base_url(), relative_url))
305 "shortcode" => shortcode,
307 "visible_in_picker" => true,
310 # Assuming that a comma is authorized in the category name
311 "category" => (tags -- ["Custom"]) |> Enum.join(",")
316 def custom_emojis(conn, _params) do
317 mastodon_emoji = mastodonized_emoji()
318 json(conn, mastodon_emoji)
321 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
322 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
325 |> Map.put("tag", params["tagged"])
327 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
330 |> add_link_headers(activities)
331 |> put_view(StatusView)
332 |> render("index.json", %{
333 activities: activities,
340 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
341 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
342 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
343 true <- Visibility.visible_for_user?(activity, user) do
345 |> put_view(StatusView)
346 |> try_render("poll.json", %{object: object, for: user})
348 error when is_nil(error) or error == false ->
349 render_error(conn, :not_found, "Record not found")
353 defp get_cached_vote_or_vote(user, object, choices) do
354 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
357 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
358 case CommonAPI.vote(user, object, choices) do
359 {:error, _message} = res -> {:ignore, res}
360 res -> {:commit, res}
367 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
368 with %Object{} = object <- Object.get_by_id(id),
369 true <- object.data["type"] == "Question",
370 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
371 true <- Visibility.visible_for_user?(activity, user),
372 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
374 |> put_view(StatusView)
375 |> try_render("poll.json", %{object: object, for: user})
378 render_error(conn, :not_found, "Record not found")
381 render_error(conn, :not_found, "Record not found")
385 |> put_status(:unprocessable_entity)
386 |> json(%{error: message})
390 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
391 targets = User.get_all_by_ids(List.wrap(id))
394 |> put_view(AccountView)
395 |> render("relationships.json", %{user: user, targets: targets})
398 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
399 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
402 %{assigns: %{user: user}} = conn,
403 %{"id" => id, "description" => description} = _
405 when is_binary(description) do
406 with %Object{} = object <- Repo.get(Object, id),
407 true <- Object.authorize_mutation(object, user),
408 {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
409 attachment_data = Map.put(data, "id", object.id)
412 |> put_view(StatusView)
413 |> render("attachment.json", %{attachment: attachment_data})
417 def update_media(_conn, _data), do: {:error, :bad_request}
419 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
420 with {:ok, object} <-
423 actor: User.ap_id(user),
424 description: Map.get(data, "description")
426 attachment_data = Map.put(object.data, "id", object.id)
429 |> put_view(StatusView)
430 |> render("attachment.json", %{attachment: attachment_data})
434 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
435 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
436 %{} = attachment_data <- Map.put(object.data, "id", object.id),
437 # Reject if not an image
438 %{type: "image"} = rendered <-
439 StatusView.render("attachment.json", %{attachment: attachment_data}) do
441 # Save to the user's info
442 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
446 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
450 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
451 mascot = User.get_mascot(user)
456 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
457 with %User{} = user <- User.get_cached_by_id(id),
458 followers <- MastodonAPI.get_followers(user, params) do
461 for_user && user.id == for_user.id -> followers
462 user.info.hide_followers -> []
467 |> add_link_headers(followers)
468 |> put_view(AccountView)
469 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
473 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
474 with %User{} = user <- User.get_cached_by_id(id),
475 followers <- MastodonAPI.get_friends(user, params) do
478 for_user && user.id == for_user.id -> followers
479 user.info.hide_follows -> []
484 |> add_link_headers(followers)
485 |> put_view(AccountView)
486 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
490 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
491 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
492 {_, true} <- {:followed, follower.id != followed.id},
493 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
495 |> put_view(AccountView)
496 |> render("relationship.json", %{user: follower, target: followed})
503 |> put_status(:forbidden)
504 |> json(%{error: message})
508 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
509 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
510 {_, true} <- {:followed, follower.id != followed.id},
511 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
513 |> put_view(AccountView)
514 |> render("account.json", %{user: followed, for: follower})
521 |> put_status(:forbidden)
522 |> json(%{error: message})
526 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
527 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
528 {_, true} <- {:followed, follower.id != followed.id},
529 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
531 |> put_view(AccountView)
532 |> render("relationship.json", %{user: follower, target: followed})
542 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
544 if Map.has_key?(params, "notifications"),
545 do: params["notifications"] in [true, "True", "true", "1"],
548 with %User{} = muted <- User.get_cached_by_id(id),
549 {:ok, muter} <- User.mute(muter, muted, notifications) do
551 |> put_view(AccountView)
552 |> render("relationship.json", %{user: muter, target: muted})
556 |> put_status(:forbidden)
557 |> json(%{error: message})
561 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
562 with %User{} = muted <- User.get_cached_by_id(id),
563 {:ok, muter} <- User.unmute(muter, muted) do
565 |> put_view(AccountView)
566 |> render("relationship.json", %{user: muter, target: muted})
570 |> put_status(:forbidden)
571 |> json(%{error: message})
575 def mutes(%{assigns: %{user: user}} = conn, _) do
576 with muted_accounts <- User.muted_users(user) do
577 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
582 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
583 with %User{} = blocked <- User.get_cached_by_id(id),
584 {:ok, blocker} <- User.block(blocker, blocked),
585 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
587 |> put_view(AccountView)
588 |> render("relationship.json", %{user: blocker, target: blocked})
592 |> put_status(:forbidden)
593 |> json(%{error: message})
597 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
598 with %User{} = blocked <- User.get_cached_by_id(id),
599 {:ok, blocker} <- User.unblock(blocker, blocked),
600 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
602 |> put_view(AccountView)
603 |> render("relationship.json", %{user: blocker, target: blocked})
607 |> put_status(:forbidden)
608 |> json(%{error: message})
612 def blocks(%{assigns: %{user: user}} = conn, _) do
613 with blocked_accounts <- User.blocked_users(user) do
614 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
619 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
620 with %User{} = subscription_target <- User.get_cached_by_id(id),
621 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
623 |> put_view(AccountView)
624 |> render("relationship.json", %{user: user, target: subscription_target})
626 nil -> {:error, :not_found}
631 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
632 with %User{} = subscription_target <- User.get_cached_by_id(id),
633 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
635 |> put_view(AccountView)
636 |> render("relationship.json", %{user: user, target: subscription_target})
638 nil -> {:error, :not_found}
643 def favourites(%{assigns: %{user: user}} = conn, params) do
646 |> Map.put("type", "Create")
647 |> Map.put("favorited_by", user.ap_id)
648 |> Map.put("blocking_user", user)
651 ActivityPub.fetch_activities([], params)
655 |> add_link_headers(activities)
656 |> put_view(StatusView)
657 |> render("index.json", %{activities: activities, for: user, as: :activity})
660 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
661 with %User{} = user <- User.get_by_id(id),
662 false <- user.info.hide_favorites do
665 |> Map.put("type", "Create")
666 |> Map.put("favorited_by", user.ap_id)
667 |> Map.put("blocking_user", for_user)
671 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
673 [Pleroma.Constants.as_public()]
678 |> ActivityPub.fetch_activities(params)
682 |> add_link_headers(activities)
683 |> put_view(StatusView)
684 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
686 nil -> {:error, :not_found}
687 true -> render_error(conn, :forbidden, "Can't get favorites")
691 def bookmarks(%{assigns: %{user: user}} = conn, params) do
692 user = User.get_cached_by_id(user.id)
695 Bookmark.for_user_query(user.id)
696 |> Pagination.fetch_paginated(params)
700 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
703 |> add_link_headers(bookmarks)
704 |> put_view(StatusView)
705 |> render("index.json", %{activities: activities, for: user, as: :activity})
708 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
709 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
712 |> put_view(ListView)
713 |> render("index.json", %{lists: lists})
716 def index(%{assigns: %{user: user}} = conn, _params) do
717 token = get_session(conn, :oauth_token)
720 mastodon_emoji = mastodonized_emoji()
722 limit = Config.get([:instance, :limit])
725 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
730 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
733 domain: Pleroma.Web.Endpoint.host(),
736 unfollow_modal: false,
739 auto_play_gif: false,
740 display_sensitive_media: false,
741 reduce_motion: false,
742 max_toot_chars: limit,
743 mascot: User.get_mascot(user)["url"]
745 poll_limits: Config.get([:instance, :poll_limits]),
747 delete_others_notice: present?(user.info.is_moderator),
748 admin: present?(user.info.is_admin)
752 default_privacy: user.info.default_scope,
753 default_sensitive: false,
754 allow_content_types: Config.get([:instance, :allowed_post_formats])
756 media_attachments: %{
757 accept_content_types: [
773 user.info.settings ||
803 push_subscription: nil,
805 custom_emojis: mastodon_emoji,
812 |> put_view(MastodonView)
813 |> render("index.html", %{initial_state: initial_state})
816 |> put_session(:return_to, conn.request_path)
817 |> redirect(to: "/web/login")
821 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
822 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
827 |> put_status(:internal_server_error)
828 |> json(%{error: inspect(e)})
832 def login(%{assigns: %{user: %User{}}} = conn, _params) do
833 redirect(conn, to: local_mastodon_root_path(conn))
836 @doc "Local Mastodon FE login init action"
837 def login(conn, %{"code" => auth_token}) do
838 with {:ok, app} <- get_or_make_app(),
839 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
840 {:ok, token} <- Token.exchange_token(app, auth) do
842 |> put_session(:oauth_token, token.token)
843 |> redirect(to: local_mastodon_root_path(conn))
847 @doc "Local Mastodon FE callback action"
848 def login(conn, _) do
849 with {:ok, app} <- get_or_make_app() do
851 o_auth_path(conn, :authorize,
852 response_type: "code",
853 client_id: app.client_id,
855 scope: Enum.join(app.scopes, " ")
858 redirect(conn, to: path)
862 defp local_mastodon_root_path(conn) do
863 case get_session(conn, :return_to) do
865 mastodon_api_path(conn, :index, ["getting-started"])
868 delete_session(conn, :return_to)
873 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
874 defp get_or_make_app do
876 %{client_name: @local_mastodon_name, redirect_uris: "."},
877 ["read", "write", "follow", "push"]
881 def logout(conn, _) do
887 # Stubs for unimplemented mastodon api
889 def empty_array(conn, _) do
890 Logger.debug("Unimplemented, returning an empty array")
894 def empty_object(conn, _) do
895 Logger.debug("Unimplemented, returning an empty object")
899 def suggestions(%{assigns: %{user: user}} = conn, _) do
900 suggestions = Config.get(:suggestions)
902 if Keyword.get(suggestions, :enabled, false) do
903 api = Keyword.get(suggestions, :third_party_engine, "")
904 timeout = Keyword.get(suggestions, :timeout, 5000)
905 limit = Keyword.get(suggestions, :limit, 23)
907 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
913 |> String.replace("{{host}}", host)
914 |> String.replace("{{user}}", user)
916 with {:ok, %{status: 200, body: body}} <-
917 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
918 {:ok, data} <- Jason.decode(body) do
921 |> Enum.slice(0, limit)
924 |> Map.put("id", fetch_suggestion_id(x))
925 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
926 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
932 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
939 defp fetch_suggestion_id(attrs) do
940 case User.get_or_fetch(attrs["acct"]) do
941 {:ok, %User{id: id}} -> id
946 def account_register(
947 %{assigns: %{app: app}} = conn,
948 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
956 "captcha_answer_data",
960 |> Map.put("nickname", nickname)
961 |> Map.put("fullname", params["fullname"] || nickname)
962 |> Map.put("bio", params["bio"] || "")
963 |> Map.put("confirm", params["password"])
965 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
966 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
968 token_type: "Bearer",
969 access_token: token.token,
971 created_at: Token.Utils.format_created_at(token)
976 |> put_status(:bad_request)
981 def account_register(%{assigns: %{app: _app}} = conn, _) do
982 render_error(conn, :bad_request, "Missing parameters")
985 def account_register(conn, _) do
986 render_error(conn, :forbidden, "Invalid credentials")
989 def password_reset(conn, params) do
990 nickname_or_email = params["email"] || params["nickname"]
992 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
994 |> put_status(:no_content)
997 {:error, "unknown user"} ->
998 send_resp(conn, :not_found, "")
1001 send_resp(conn, :bad_request, "")
1005 def account_confirmation_resend(conn, params) do
1006 nickname_or_email = params["email"] || params["nickname"]
1008 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1009 {:ok, _} <- User.try_send_confirmation_email(user) do
1011 |> json_response(:no_content, "")
1015 def try_render(conn, target, params)
1016 when is_binary(target) do
1017 case render(conn, target, params) do
1018 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1023 def try_render(conn, _, _) do
1024 render_error(conn, :not_implemented, "Can't display this activity")
1027 defp present?(nil), do: false
1028 defp present?(false), do: false
1029 defp present?(_), do: true