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.ReportView
33 alias Pleroma.Web.MastodonAPI.StatusView
34 alias Pleroma.Web.MediaProxy
35 alias Pleroma.Web.OAuth.App
36 alias Pleroma.Web.OAuth.Authorization
37 alias Pleroma.Web.OAuth.Scopes
38 alias Pleroma.Web.OAuth.Token
39 alias Pleroma.Web.TwitterAPI.TwitterAPI
42 require Pleroma.Constants
44 @rate_limited_relations_actions ~w(follow unfollow)a
48 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
51 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
52 plug(RateLimiter, :app_account_creation when action == :account_register)
53 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
54 plug(RateLimiter, :password_reset when action == :password_reset)
55 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
57 @local_mastodon_name "Mastodon-Local"
59 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
61 def create_app(conn, params) do
62 scopes = Scopes.fetch_scopes(params, ["read"])
66 |> Map.drop(["scope", "scopes"])
67 |> Map.put("scopes", scopes)
69 with cs <- App.register_changeset(%App{}, app_attrs),
70 false <- cs.changes[:client_name] == @local_mastodon_name,
71 {:ok, app} <- Repo.insert(cs) do
74 |> render("show.json", %{app: app})
83 value_function \\ fn x -> {:ok, x} end
85 if Map.has_key?(params, params_field) do
86 case value_function.(params[params_field]) do
87 {:ok, new_value} -> Map.put(map, map_field, new_value)
95 def update_credentials(%{assigns: %{user: user}} = conn, params) do
100 |> add_if_present(params, "display_name", :name)
101 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
102 |> add_if_present(params, "avatar", :avatar, fn value ->
103 with %Plug.Upload{} <- value,
104 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
111 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
115 |> Map.get(:emoji, [])
116 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
123 :hide_followers_count,
129 :skip_thread_containment,
132 |> Enum.reduce(%{}, fn key, acc ->
133 add_if_present(acc, params, to_string(key), key, fn value ->
134 {:ok, truthy_param?(value)}
137 |> add_if_present(params, "default_scope", :default_scope)
138 |> add_if_present(params, "fields", :fields, fn fields ->
139 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
143 |> add_if_present(params, "fields", :raw_fields)
144 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
145 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
147 |> add_if_present(params, "header", :banner, fn value ->
148 with %Plug.Upload{} <- value,
149 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
155 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
156 with %Plug.Upload{} <- value,
157 {:ok, object} <- ActivityPub.upload(value, type: :background) do
163 |> Map.put(:emoji, user_info_emojis)
167 |> User.update_changeset(user_params)
168 |> User.change_info(&User.Info.profile_update(&1, info_params))
170 with {:ok, user} <- User.update_and_set_cache(changeset) do
171 if original_user != user, do: CommonAPI.update(user)
175 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
178 _e -> render_error(conn, :forbidden, "Invalid request")
182 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
183 change = Changeset.change(user, %{avatar: nil})
184 {:ok, user} = User.update_and_set_cache(change)
185 CommonAPI.update(user)
187 json(conn, %{url: nil})
190 def update_avatar(%{assigns: %{user: user}} = conn, params) do
191 {:ok, object} = ActivityPub.upload(params, type: :avatar)
192 change = Changeset.change(user, %{avatar: object.data})
193 {:ok, user} = User.update_and_set_cache(change)
194 CommonAPI.update(user)
195 %{"url" => [%{"href" => href} | _]} = object.data
197 json(conn, %{url: href})
200 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
201 new_info = %{"banner" => %{}}
203 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
204 CommonAPI.update(user)
205 json(conn, %{url: nil})
209 def update_banner(%{assigns: %{user: user}} = conn, params) do
210 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
211 new_info <- %{"banner" => object.data},
212 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
213 CommonAPI.update(user)
214 %{"url" => [%{"href" => href} | _]} = object.data
216 json(conn, %{url: href})
220 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
221 new_info = %{"background" => %{}}
223 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
224 json(conn, %{url: nil})
228 def update_background(%{assigns: %{user: user}} = conn, params) do
229 with {:ok, object} <- ActivityPub.upload(params, type: :background),
230 new_info <- %{"background" => object.data},
231 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
232 %{"url" => [%{"href" => href} | _]} = object.data
234 json(conn, %{url: href})
238 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
239 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
242 AccountView.render("account.json", %{
245 with_pleroma_settings: true,
246 with_chat_token: chat_token
252 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
253 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
256 |> render("short.json", %{app: app})
260 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
261 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
262 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
263 account = AccountView.render("account.json", %{user: user, for: for_user})
266 _e -> render_error(conn, :not_found, "Can't find user")
270 @mastodon_api_level "2.7.2"
272 def masto_instance(conn, _params) do
273 instance = Config.get(:instance)
277 title: Keyword.get(instance, :name),
278 description: Keyword.get(instance, :description),
279 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
280 email: Keyword.get(instance, :email),
282 streaming_api: Pleroma.Web.Endpoint.websocket_url()
284 stats: Stats.get_stats(),
285 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
287 registrations: Pleroma.Config.get([:instance, :registrations_open]),
288 # Extra (not present in Mastodon):
289 max_toot_chars: Keyword.get(instance, :limit),
290 poll_limits: Keyword.get(instance, :poll_limits)
296 def peers(conn, _params) do
297 json(conn, Stats.get_peers())
300 defp mastodonized_emoji do
301 Pleroma.Emoji.get_all()
302 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
303 url = to_string(URI.merge(Web.base_url(), relative_url))
306 "shortcode" => shortcode,
308 "visible_in_picker" => true,
311 # Assuming that a comma is authorized in the category name
312 "category" => (tags -- ["Custom"]) |> Enum.join(",")
317 def custom_emojis(conn, _params) do
318 mastodon_emoji = mastodonized_emoji()
319 json(conn, mastodon_emoji)
322 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
323 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
326 |> Map.put("tag", params["tagged"])
328 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
331 |> add_link_headers(activities)
332 |> put_view(StatusView)
333 |> render("index.json", %{
334 activities: activities,
341 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
342 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
343 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
344 true <- Visibility.visible_for_user?(activity, user) do
346 |> put_view(StatusView)
347 |> try_render("poll.json", %{object: object, for: user})
349 error when is_nil(error) or error == false ->
350 render_error(conn, :not_found, "Record not found")
354 defp get_cached_vote_or_vote(user, object, choices) do
355 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
358 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
359 case CommonAPI.vote(user, object, choices) do
360 {:error, _message} = res -> {:ignore, res}
361 res -> {:commit, res}
368 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
369 with %Object{} = object <- Object.get_by_id(id),
370 true <- object.data["type"] == "Question",
371 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
372 true <- Visibility.visible_for_user?(activity, user),
373 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
375 |> put_view(StatusView)
376 |> try_render("poll.json", %{object: object, for: user})
379 render_error(conn, :not_found, "Record not found")
382 render_error(conn, :not_found, "Record not found")
386 |> put_status(:unprocessable_entity)
387 |> json(%{error: message})
391 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
392 targets = User.get_all_by_ids(List.wrap(id))
395 |> put_view(AccountView)
396 |> render("relationships.json", %{user: user, targets: targets})
399 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
400 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
403 %{assigns: %{user: user}} = conn,
404 %{"id" => id, "description" => description} = _
406 when is_binary(description) do
407 with %Object{} = object <- Repo.get(Object, id),
408 true <- Object.authorize_mutation(object, user),
409 {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
410 attachment_data = Map.put(data, "id", object.id)
413 |> put_view(StatusView)
414 |> render("attachment.json", %{attachment: attachment_data})
418 def update_media(_conn, _data), do: {:error, :bad_request}
420 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
421 with {:ok, object} <-
424 actor: User.ap_id(user),
425 description: Map.get(data, "description")
427 attachment_data = Map.put(object.data, "id", object.id)
430 |> put_view(StatusView)
431 |> render("attachment.json", %{attachment: attachment_data})
435 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
436 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
437 %{} = attachment_data <- Map.put(object.data, "id", object.id),
438 # Reject if not an image
439 %{type: "image"} = rendered <-
440 StatusView.render("attachment.json", %{attachment: attachment_data}) do
442 # Save to the user's info
443 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
447 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
451 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
452 mascot = User.get_mascot(user)
457 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
458 with %User{} = user <- User.get_cached_by_id(id),
459 followers <- MastodonAPI.get_followers(user, params) do
462 for_user && user.id == for_user.id -> followers
463 user.info.hide_followers -> []
468 |> add_link_headers(followers)
469 |> put_view(AccountView)
470 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
474 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
475 with %User{} = user <- User.get_cached_by_id(id),
476 followers <- MastodonAPI.get_friends(user, params) do
479 for_user && user.id == for_user.id -> followers
480 user.info.hide_follows -> []
485 |> add_link_headers(followers)
486 |> put_view(AccountView)
487 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
491 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
492 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
493 {_, true} <- {:followed, follower.id != followed.id},
494 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
496 |> put_view(AccountView)
497 |> render("relationship.json", %{user: follower, target: followed})
504 |> put_status(:forbidden)
505 |> json(%{error: message})
509 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
510 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
511 {_, true} <- {:followed, follower.id != followed.id},
512 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
514 |> put_view(AccountView)
515 |> render("account.json", %{user: followed, for: follower})
522 |> put_status(:forbidden)
523 |> json(%{error: message})
527 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
528 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
529 {_, true} <- {:followed, follower.id != followed.id},
530 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
532 |> put_view(AccountView)
533 |> render("relationship.json", %{user: follower, target: followed})
543 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
545 if Map.has_key?(params, "notifications"),
546 do: params["notifications"] in [true, "True", "true", "1"],
549 with %User{} = muted <- User.get_cached_by_id(id),
550 {:ok, muter} <- User.mute(muter, muted, notifications) do
552 |> put_view(AccountView)
553 |> render("relationship.json", %{user: muter, target: muted})
557 |> put_status(:forbidden)
558 |> json(%{error: message})
562 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
563 with %User{} = muted <- User.get_cached_by_id(id),
564 {:ok, muter} <- User.unmute(muter, muted) do
566 |> put_view(AccountView)
567 |> render("relationship.json", %{user: muter, target: muted})
571 |> put_status(:forbidden)
572 |> json(%{error: message})
576 def mutes(%{assigns: %{user: user}} = conn, _) do
577 with muted_accounts <- User.muted_users(user) do
578 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
583 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
584 with %User{} = blocked <- User.get_cached_by_id(id),
585 {:ok, blocker} <- User.block(blocker, blocked),
586 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
588 |> put_view(AccountView)
589 |> render("relationship.json", %{user: blocker, target: blocked})
593 |> put_status(:forbidden)
594 |> json(%{error: message})
598 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
599 with %User{} = blocked <- User.get_cached_by_id(id),
600 {:ok, blocker} <- User.unblock(blocker, blocked),
601 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
603 |> put_view(AccountView)
604 |> render("relationship.json", %{user: blocker, target: blocked})
608 |> put_status(:forbidden)
609 |> json(%{error: message})
613 def blocks(%{assigns: %{user: user}} = conn, _) do
614 with blocked_accounts <- User.blocked_users(user) do
615 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
620 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
621 with %User{} = subscription_target <- User.get_cached_by_id(id),
622 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
624 |> put_view(AccountView)
625 |> render("relationship.json", %{user: user, target: subscription_target})
627 nil -> {:error, :not_found}
632 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
633 with %User{} = subscription_target <- User.get_cached_by_id(id),
634 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
636 |> put_view(AccountView)
637 |> render("relationship.json", %{user: user, target: subscription_target})
639 nil -> {:error, :not_found}
644 def favourites(%{assigns: %{user: user}} = conn, params) do
647 |> Map.put("type", "Create")
648 |> Map.put("favorited_by", user.ap_id)
649 |> Map.put("blocking_user", user)
652 ActivityPub.fetch_activities([], params)
656 |> add_link_headers(activities)
657 |> put_view(StatusView)
658 |> render("index.json", %{activities: activities, for: user, as: :activity})
661 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
662 with %User{} = user <- User.get_by_id(id),
663 false <- user.info.hide_favorites do
666 |> Map.put("type", "Create")
667 |> Map.put("favorited_by", user.ap_id)
668 |> Map.put("blocking_user", for_user)
672 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
674 [Pleroma.Constants.as_public()]
679 |> ActivityPub.fetch_activities(params)
683 |> add_link_headers(activities)
684 |> put_view(StatusView)
685 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
687 nil -> {:error, :not_found}
688 true -> render_error(conn, :forbidden, "Can't get favorites")
692 def bookmarks(%{assigns: %{user: user}} = conn, params) do
693 user = User.get_cached_by_id(user.id)
696 Bookmark.for_user_query(user.id)
697 |> Pagination.fetch_paginated(params)
701 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
704 |> add_link_headers(bookmarks)
705 |> put_view(StatusView)
706 |> render("index.json", %{activities: activities, for: user, as: :activity})
709 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
710 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
713 |> put_view(ListView)
714 |> render("index.json", %{lists: lists})
717 def index(%{assigns: %{user: user}} = conn, _params) do
718 token = get_session(conn, :oauth_token)
721 mastodon_emoji = mastodonized_emoji()
723 limit = Config.get([:instance, :limit])
726 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
731 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
734 domain: Pleroma.Web.Endpoint.host(),
737 unfollow_modal: false,
740 auto_play_gif: false,
741 display_sensitive_media: false,
742 reduce_motion: false,
743 max_toot_chars: limit,
744 mascot: User.get_mascot(user)["url"]
746 poll_limits: Config.get([:instance, :poll_limits]),
748 delete_others_notice: present?(user.info.is_moderator),
749 admin: present?(user.info.is_admin)
753 default_privacy: user.info.default_scope,
754 default_sensitive: false,
755 allow_content_types: Config.get([:instance, :allowed_post_formats])
757 media_attachments: %{
758 accept_content_types: [
774 user.info.settings ||
804 push_subscription: nil,
806 custom_emojis: mastodon_emoji,
813 |> put_view(MastodonView)
814 |> render("index.html", %{initial_state: initial_state})
817 |> put_session(:return_to, conn.request_path)
818 |> redirect(to: "/web/login")
822 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
823 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
828 |> put_status(:internal_server_error)
829 |> json(%{error: inspect(e)})
833 def login(%{assigns: %{user: %User{}}} = conn, _params) do
834 redirect(conn, to: local_mastodon_root_path(conn))
837 @doc "Local Mastodon FE login init action"
838 def login(conn, %{"code" => auth_token}) do
839 with {:ok, app} <- get_or_make_app(),
840 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
841 {:ok, token} <- Token.exchange_token(app, auth) do
843 |> put_session(:oauth_token, token.token)
844 |> redirect(to: local_mastodon_root_path(conn))
848 @doc "Local Mastodon FE callback action"
849 def login(conn, _) do
850 with {:ok, app} <- get_or_make_app() do
852 o_auth_path(conn, :authorize,
853 response_type: "code",
854 client_id: app.client_id,
856 scope: Enum.join(app.scopes, " ")
859 redirect(conn, to: path)
863 defp local_mastodon_root_path(conn) do
864 case get_session(conn, :return_to) do
866 mastodon_api_path(conn, :index, ["getting-started"])
869 delete_session(conn, :return_to)
874 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
875 defp get_or_make_app do
877 %{client_name: @local_mastodon_name, redirect_uris: "."},
878 ["read", "write", "follow", "push"]
882 def logout(conn, _) do
888 # Stubs for unimplemented mastodon api
890 def empty_array(conn, _) do
891 Logger.debug("Unimplemented, returning an empty array")
895 def empty_object(conn, _) do
896 Logger.debug("Unimplemented, returning an empty object")
900 def suggestions(%{assigns: %{user: user}} = conn, _) do
901 suggestions = Config.get(:suggestions)
903 if Keyword.get(suggestions, :enabled, false) do
904 api = Keyword.get(suggestions, :third_party_engine, "")
905 timeout = Keyword.get(suggestions, :timeout, 5000)
906 limit = Keyword.get(suggestions, :limit, 23)
908 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
914 |> String.replace("{{host}}", host)
915 |> String.replace("{{user}}", user)
917 with {:ok, %{status: 200, body: body}} <-
918 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
919 {:ok, data} <- Jason.decode(body) do
922 |> Enum.slice(0, limit)
925 |> Map.put("id", fetch_suggestion_id(x))
926 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
927 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
933 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
940 defp fetch_suggestion_id(attrs) do
941 case User.get_or_fetch(attrs["acct"]) do
942 {:ok, %User{id: id}} -> id
947 def reports(%{assigns: %{user: user}} = conn, params) do
948 case CommonAPI.report(user, params) do
951 |> put_view(ReportView)
952 |> try_render("report.json", %{activity: activity})
956 |> put_status(:bad_request)
957 |> json(%{error: err})
961 def account_register(
962 %{assigns: %{app: app}} = conn,
963 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
971 "captcha_answer_data",
975 |> Map.put("nickname", nickname)
976 |> Map.put("fullname", params["fullname"] || nickname)
977 |> Map.put("bio", params["bio"] || "")
978 |> Map.put("confirm", params["password"])
980 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
981 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
983 token_type: "Bearer",
984 access_token: token.token,
986 created_at: Token.Utils.format_created_at(token)
991 |> put_status(:bad_request)
996 def account_register(%{assigns: %{app: _app}} = conn, _) do
997 render_error(conn, :bad_request, "Missing parameters")
1000 def account_register(conn, _) do
1001 render_error(conn, :forbidden, "Invalid credentials")
1004 def password_reset(conn, params) do
1005 nickname_or_email = params["email"] || params["nickname"]
1007 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1009 |> put_status(:no_content)
1012 {:error, "unknown user"} ->
1013 send_resp(conn, :not_found, "")
1016 send_resp(conn, :bad_request, "")
1020 def account_confirmation_resend(conn, params) do
1021 nickname_or_email = params["email"] || params["nickname"]
1023 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1024 {:ok, _} <- User.try_send_confirmation_email(user) do
1026 |> json_response(:no_content, "")
1030 def try_render(conn, target, params)
1031 when is_binary(target) do
1032 case render(conn, target, params) do
1033 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1038 def try_render(conn, _, _) do
1039 render_error(conn, :not_implemented, "Can't display this activity")
1042 defp present?(nil), do: false
1043 defp present?(false), do: false
1044 defp present?(_), do: true