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
15 alias Pleroma.Conversation.Participation
19 alias Pleroma.Pagination
20 alias Pleroma.Plugs.RateLimiter
25 alias Pleroma.Web.ActivityPub.ActivityPub
26 alias Pleroma.Web.ActivityPub.Visibility
27 alias Pleroma.Web.CommonAPI
28 alias Pleroma.Web.MastodonAPI.AccountView
29 alias Pleroma.Web.MastodonAPI.AppView
30 alias Pleroma.Web.MastodonAPI.ConversationView
31 alias Pleroma.Web.MastodonAPI.ListView
32 alias Pleroma.Web.MastodonAPI.MastodonAPI
33 alias Pleroma.Web.MastodonAPI.MastodonView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.StatusView
36 alias Pleroma.Web.MediaProxy
37 alias Pleroma.Web.OAuth.App
38 alias Pleroma.Web.OAuth.Authorization
39 alias Pleroma.Web.OAuth.Scopes
40 alias Pleroma.Web.OAuth.Token
41 alias Pleroma.Web.TwitterAPI.TwitterAPI
44 require Pleroma.Constants
46 @rate_limited_relations_actions ~w(follow unfollow)a
50 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
53 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
54 plug(RateLimiter, :app_account_creation when action == :account_register)
55 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
56 plug(RateLimiter, :password_reset when action == :password_reset)
57 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
59 @local_mastodon_name "Mastodon-Local"
61 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
63 def create_app(conn, params) do
64 scopes = Scopes.fetch_scopes(params, ["read"])
68 |> Map.drop(["scope", "scopes"])
69 |> Map.put("scopes", scopes)
71 with cs <- App.register_changeset(%App{}, app_attrs),
72 false <- cs.changes[:client_name] == @local_mastodon_name,
73 {:ok, app} <- Repo.insert(cs) do
76 |> render("show.json", %{app: app})
85 value_function \\ fn x -> {:ok, x} end
87 if Map.has_key?(params, params_field) do
88 case value_function.(params[params_field]) do
89 {:ok, new_value} -> Map.put(map, map_field, new_value)
97 def update_credentials(%{assigns: %{user: user}} = conn, params) do
102 |> add_if_present(params, "display_name", :name)
103 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
104 |> add_if_present(params, "avatar", :avatar, fn value ->
105 with %Plug.Upload{} <- value,
106 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
113 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
117 |> Map.get(:emoji, [])
118 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
125 :hide_followers_count,
131 :skip_thread_containment,
134 |> Enum.reduce(%{}, fn key, acc ->
135 add_if_present(acc, params, to_string(key), key, fn value ->
136 {:ok, truthy_param?(value)}
139 |> add_if_present(params, "default_scope", :default_scope)
140 |> add_if_present(params, "fields", :fields, fn fields ->
141 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
145 |> add_if_present(params, "fields", :raw_fields)
146 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
147 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
149 |> add_if_present(params, "header", :banner, fn value ->
150 with %Plug.Upload{} <- value,
151 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
157 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
158 with %Plug.Upload{} <- value,
159 {:ok, object} <- ActivityPub.upload(value, type: :background) do
165 |> Map.put(:emoji, user_info_emojis)
169 |> User.update_changeset(user_params)
170 |> User.change_info(&User.Info.profile_update(&1, info_params))
172 with {:ok, user} <- User.update_and_set_cache(changeset) do
173 if original_user != user, do: CommonAPI.update(user)
177 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
180 _e -> render_error(conn, :forbidden, "Invalid request")
184 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
185 change = Changeset.change(user, %{avatar: nil})
186 {:ok, user} = User.update_and_set_cache(change)
187 CommonAPI.update(user)
189 json(conn, %{url: nil})
192 def update_avatar(%{assigns: %{user: user}} = conn, params) do
193 {:ok, object} = ActivityPub.upload(params, type: :avatar)
194 change = Changeset.change(user, %{avatar: object.data})
195 {:ok, user} = User.update_and_set_cache(change)
196 CommonAPI.update(user)
197 %{"url" => [%{"href" => href} | _]} = object.data
199 json(conn, %{url: href})
202 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
203 new_info = %{"banner" => %{}}
205 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
206 CommonAPI.update(user)
207 json(conn, %{url: nil})
211 def update_banner(%{assigns: %{user: user}} = conn, params) do
212 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
213 new_info <- %{"banner" => object.data},
214 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
215 CommonAPI.update(user)
216 %{"url" => [%{"href" => href} | _]} = object.data
218 json(conn, %{url: href})
222 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
223 new_info = %{"background" => %{}}
225 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
226 json(conn, %{url: nil})
230 def update_background(%{assigns: %{user: user}} = conn, params) do
231 with {:ok, object} <- ActivityPub.upload(params, type: :background),
232 new_info <- %{"background" => object.data},
233 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
234 %{"url" => [%{"href" => href} | _]} = object.data
236 json(conn, %{url: href})
240 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
241 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
244 AccountView.render("account.json", %{
247 with_pleroma_settings: true,
248 with_chat_token: chat_token
254 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
255 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
258 |> render("short.json", %{app: app})
262 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
263 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
264 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
265 account = AccountView.render("account.json", %{user: user, for: for_user})
268 _e -> render_error(conn, :not_found, "Can't find user")
272 @mastodon_api_level "2.7.2"
274 def masto_instance(conn, _params) do
275 instance = Config.get(:instance)
279 title: Keyword.get(instance, :name),
280 description: Keyword.get(instance, :description),
281 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
282 email: Keyword.get(instance, :email),
284 streaming_api: Pleroma.Web.Endpoint.websocket_url()
286 stats: Stats.get_stats(),
287 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
289 registrations: Pleroma.Config.get([:instance, :registrations_open]),
290 # Extra (not present in Mastodon):
291 max_toot_chars: Keyword.get(instance, :limit),
292 poll_limits: Keyword.get(instance, :poll_limits)
298 def peers(conn, _params) do
299 json(conn, Stats.get_peers())
302 defp mastodonized_emoji do
303 Pleroma.Emoji.get_all()
304 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
305 url = to_string(URI.merge(Web.base_url(), relative_url))
308 "shortcode" => shortcode,
310 "visible_in_picker" => true,
313 # Assuming that a comma is authorized in the category name
314 "category" => (tags -- ["Custom"]) |> Enum.join(",")
319 def custom_emojis(conn, _params) do
320 mastodon_emoji = mastodonized_emoji()
321 json(conn, mastodon_emoji)
324 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
325 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
328 |> Map.put("tag", params["tagged"])
330 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
333 |> add_link_headers(activities)
334 |> put_view(StatusView)
335 |> render("index.json", %{
336 activities: activities,
343 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
344 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
345 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
346 true <- Visibility.visible_for_user?(activity, user) do
348 |> put_view(StatusView)
349 |> try_render("poll.json", %{object: object, for: user})
351 error when is_nil(error) or error == false ->
352 render_error(conn, :not_found, "Record not found")
356 defp get_cached_vote_or_vote(user, object, choices) do
357 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
360 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
361 case CommonAPI.vote(user, object, choices) do
362 {:error, _message} = res -> {:ignore, res}
363 res -> {:commit, res}
370 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
371 with %Object{} = object <- Object.get_by_id(id),
372 true <- object.data["type"] == "Question",
373 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
374 true <- Visibility.visible_for_user?(activity, user),
375 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
377 |> put_view(StatusView)
378 |> try_render("poll.json", %{object: object, for: user})
381 render_error(conn, :not_found, "Record not found")
384 render_error(conn, :not_found, "Record not found")
388 |> put_status(:unprocessable_entity)
389 |> json(%{error: message})
393 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
394 targets = User.get_all_by_ids(List.wrap(id))
397 |> put_view(AccountView)
398 |> render("relationships.json", %{user: user, targets: targets})
401 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
402 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
405 %{assigns: %{user: user}} = conn,
406 %{"id" => id, "description" => description} = _
408 when is_binary(description) do
409 with %Object{} = object <- Repo.get(Object, id),
410 true <- Object.authorize_mutation(object, user),
411 {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
412 attachment_data = Map.put(data, "id", object.id)
415 |> put_view(StatusView)
416 |> render("attachment.json", %{attachment: attachment_data})
420 def update_media(_conn, _data), do: {:error, :bad_request}
422 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
423 with {:ok, object} <-
426 actor: User.ap_id(user),
427 description: Map.get(data, "description")
429 attachment_data = Map.put(object.data, "id", object.id)
432 |> put_view(StatusView)
433 |> render("attachment.json", %{attachment: attachment_data})
437 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
438 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
439 %{} = attachment_data <- Map.put(object.data, "id", object.id),
440 # Reject if not an image
441 %{type: "image"} = rendered <-
442 StatusView.render("attachment.json", %{attachment: attachment_data}) do
444 # Save to the user's info
445 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
449 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
453 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
454 mascot = User.get_mascot(user)
459 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
460 with %User{} = user <- User.get_cached_by_id(id),
461 followers <- MastodonAPI.get_followers(user, params) do
464 for_user && user.id == for_user.id -> followers
465 user.info.hide_followers -> []
470 |> add_link_headers(followers)
471 |> put_view(AccountView)
472 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
476 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
477 with %User{} = user <- User.get_cached_by_id(id),
478 followers <- MastodonAPI.get_friends(user, params) do
481 for_user && user.id == for_user.id -> followers
482 user.info.hide_follows -> []
487 |> add_link_headers(followers)
488 |> put_view(AccountView)
489 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
493 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
494 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
495 {_, true} <- {:followed, follower.id != followed.id},
496 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
498 |> put_view(AccountView)
499 |> render("relationship.json", %{user: follower, target: followed})
506 |> put_status(:forbidden)
507 |> json(%{error: message})
511 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
512 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
513 {_, true} <- {:followed, follower.id != followed.id},
514 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
516 |> put_view(AccountView)
517 |> render("account.json", %{user: followed, for: follower})
524 |> put_status(:forbidden)
525 |> json(%{error: message})
529 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
530 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
531 {_, true} <- {:followed, follower.id != followed.id},
532 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
534 |> put_view(AccountView)
535 |> render("relationship.json", %{user: follower, target: followed})
545 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
547 if Map.has_key?(params, "notifications"),
548 do: params["notifications"] in [true, "True", "true", "1"],
551 with %User{} = muted <- User.get_cached_by_id(id),
552 {:ok, muter} <- User.mute(muter, muted, notifications) do
554 |> put_view(AccountView)
555 |> render("relationship.json", %{user: muter, target: muted})
559 |> put_status(:forbidden)
560 |> json(%{error: message})
564 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
565 with %User{} = muted <- User.get_cached_by_id(id),
566 {:ok, muter} <- User.unmute(muter, muted) do
568 |> put_view(AccountView)
569 |> render("relationship.json", %{user: muter, target: muted})
573 |> put_status(:forbidden)
574 |> json(%{error: message})
578 def mutes(%{assigns: %{user: user}} = conn, _) do
579 with muted_accounts <- User.muted_users(user) do
580 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
585 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
586 with %User{} = blocked <- User.get_cached_by_id(id),
587 {:ok, blocker} <- User.block(blocker, blocked),
588 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
590 |> put_view(AccountView)
591 |> render("relationship.json", %{user: blocker, target: blocked})
595 |> put_status(:forbidden)
596 |> json(%{error: message})
600 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
601 with %User{} = blocked <- User.get_cached_by_id(id),
602 {:ok, blocker} <- User.unblock(blocker, blocked),
603 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
605 |> put_view(AccountView)
606 |> render("relationship.json", %{user: blocker, target: blocked})
610 |> put_status(:forbidden)
611 |> json(%{error: message})
615 def blocks(%{assigns: %{user: user}} = conn, _) do
616 with blocked_accounts <- User.blocked_users(user) do
617 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
622 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
623 with %User{} = subscription_target <- User.get_cached_by_id(id),
624 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
626 |> put_view(AccountView)
627 |> render("relationship.json", %{user: user, target: subscription_target})
629 nil -> {:error, :not_found}
634 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
635 with %User{} = subscription_target <- User.get_cached_by_id(id),
636 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
638 |> put_view(AccountView)
639 |> render("relationship.json", %{user: user, target: subscription_target})
641 nil -> {:error, :not_found}
646 def favourites(%{assigns: %{user: user}} = conn, params) do
649 |> Map.put("type", "Create")
650 |> Map.put("favorited_by", user.ap_id)
651 |> Map.put("blocking_user", user)
654 ActivityPub.fetch_activities([], params)
658 |> add_link_headers(activities)
659 |> put_view(StatusView)
660 |> render("index.json", %{activities: activities, for: user, as: :activity})
663 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
664 with %User{} = user <- User.get_by_id(id),
665 false <- user.info.hide_favorites do
668 |> Map.put("type", "Create")
669 |> Map.put("favorited_by", user.ap_id)
670 |> Map.put("blocking_user", for_user)
674 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
676 [Pleroma.Constants.as_public()]
681 |> ActivityPub.fetch_activities(params)
685 |> add_link_headers(activities)
686 |> put_view(StatusView)
687 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
689 nil -> {:error, :not_found}
690 true -> render_error(conn, :forbidden, "Can't get favorites")
694 def bookmarks(%{assigns: %{user: user}} = conn, params) do
695 user = User.get_cached_by_id(user.id)
698 Bookmark.for_user_query(user.id)
699 |> Pagination.fetch_paginated(params)
703 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
706 |> add_link_headers(bookmarks)
707 |> put_view(StatusView)
708 |> render("index.json", %{activities: activities, for: user, as: :activity})
711 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
712 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
715 |> put_view(ListView)
716 |> render("index.json", %{lists: lists})
719 def index(%{assigns: %{user: user}} = conn, _params) do
720 token = get_session(conn, :oauth_token)
723 mastodon_emoji = mastodonized_emoji()
725 limit = Config.get([:instance, :limit])
728 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
733 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
736 domain: Pleroma.Web.Endpoint.host(),
739 unfollow_modal: false,
742 auto_play_gif: false,
743 display_sensitive_media: false,
744 reduce_motion: false,
745 max_toot_chars: limit,
746 mascot: User.get_mascot(user)["url"]
748 poll_limits: Config.get([:instance, :poll_limits]),
750 delete_others_notice: present?(user.info.is_moderator),
751 admin: present?(user.info.is_admin)
755 default_privacy: user.info.default_scope,
756 default_sensitive: false,
757 allow_content_types: Config.get([:instance, :allowed_post_formats])
759 media_attachments: %{
760 accept_content_types: [
776 user.info.settings ||
806 push_subscription: nil,
808 custom_emojis: mastodon_emoji,
815 |> put_view(MastodonView)
816 |> render("index.html", %{initial_state: initial_state})
819 |> put_session(:return_to, conn.request_path)
820 |> redirect(to: "/web/login")
824 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
825 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
830 |> put_status(:internal_server_error)
831 |> json(%{error: inspect(e)})
835 def login(%{assigns: %{user: %User{}}} = conn, _params) do
836 redirect(conn, to: local_mastodon_root_path(conn))
839 @doc "Local Mastodon FE login init action"
840 def login(conn, %{"code" => auth_token}) do
841 with {:ok, app} <- get_or_make_app(),
842 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
843 {:ok, token} <- Token.exchange_token(app, auth) do
845 |> put_session(:oauth_token, token.token)
846 |> redirect(to: local_mastodon_root_path(conn))
850 @doc "Local Mastodon FE callback action"
851 def login(conn, _) do
852 with {:ok, app} <- get_or_make_app() do
854 o_auth_path(conn, :authorize,
855 response_type: "code",
856 client_id: app.client_id,
858 scope: Enum.join(app.scopes, " ")
861 redirect(conn, to: path)
865 defp local_mastodon_root_path(conn) do
866 case get_session(conn, :return_to) do
868 mastodon_api_path(conn, :index, ["getting-started"])
871 delete_session(conn, :return_to)
876 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
877 defp get_or_make_app do
879 %{client_name: @local_mastodon_name, redirect_uris: "."},
880 ["read", "write", "follow", "push"]
884 def logout(conn, _) do
890 # Stubs for unimplemented mastodon api
892 def empty_array(conn, _) do
893 Logger.debug("Unimplemented, returning an empty array")
897 def empty_object(conn, _) do
898 Logger.debug("Unimplemented, returning an empty object")
902 def suggestions(%{assigns: %{user: user}} = conn, _) do
903 suggestions = Config.get(:suggestions)
905 if Keyword.get(suggestions, :enabled, false) do
906 api = Keyword.get(suggestions, :third_party_engine, "")
907 timeout = Keyword.get(suggestions, :timeout, 5000)
908 limit = Keyword.get(suggestions, :limit, 23)
910 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
916 |> String.replace("{{host}}", host)
917 |> String.replace("{{user}}", user)
919 with {:ok, %{status: 200, body: body}} <-
920 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
921 {:ok, data} <- Jason.decode(body) do
924 |> Enum.slice(0, limit)
927 |> Map.put("id", fetch_suggestion_id(x))
928 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
929 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
935 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
942 defp fetch_suggestion_id(attrs) do
943 case User.get_or_fetch(attrs["acct"]) do
944 {:ok, %User{id: id}} -> id
949 def reports(%{assigns: %{user: user}} = conn, params) do
950 case CommonAPI.report(user, params) do
953 |> put_view(ReportView)
954 |> try_render("report.json", %{activity: activity})
958 |> put_status(:bad_request)
959 |> json(%{error: err})
963 def account_register(
964 %{assigns: %{app: app}} = conn,
965 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
973 "captcha_answer_data",
977 |> Map.put("nickname", nickname)
978 |> Map.put("fullname", params["fullname"] || nickname)
979 |> Map.put("bio", params["bio"] || "")
980 |> Map.put("confirm", params["password"])
982 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
983 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
985 token_type: "Bearer",
986 access_token: token.token,
988 created_at: Token.Utils.format_created_at(token)
993 |> put_status(:bad_request)
998 def account_register(%{assigns: %{app: _app}} = conn, _) do
999 render_error(conn, :bad_request, "Missing parameters")
1002 def account_register(conn, _) do
1003 render_error(conn, :forbidden, "Invalid credentials")
1006 def conversations(%{assigns: %{user: user}} = conn, params) do
1007 participations = Participation.for_user_with_last_activity_id(user, params)
1010 Enum.map(participations, fn participation ->
1011 ConversationView.render("participation.json", %{participation: participation, for: user})
1015 |> add_link_headers(participations)
1016 |> json(conversations)
1019 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1020 with %Participation{} = participation <-
1021 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1022 {:ok, participation} <- Participation.mark_as_read(participation) do
1023 participation_view =
1024 ConversationView.render("participation.json", %{participation: participation, for: user})
1027 |> json(participation_view)
1031 def password_reset(conn, params) do
1032 nickname_or_email = params["email"] || params["nickname"]
1034 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1036 |> put_status(:no_content)
1039 {:error, "unknown user"} ->
1040 send_resp(conn, :not_found, "")
1043 send_resp(conn, :bad_request, "")
1047 def account_confirmation_resend(conn, params) do
1048 nickname_or_email = params["email"] || params["nickname"]
1050 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1051 {:ok, _} <- User.try_send_confirmation_email(user) do
1053 |> json_response(:no_content, "")
1057 def try_render(conn, target, params)
1058 when is_binary(target) do
1059 case render(conn, target, params) do
1060 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1065 def try_render(conn, _, _) do
1066 render_error(conn, :not_implemented, "Can't display this activity")
1069 defp present?(nil), do: false
1070 defp present?(false), do: false
1071 defp present?(_), do: true