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
20 alias Pleroma.Pagination
21 alias Pleroma.Plugs.RateLimiter
26 alias Pleroma.Web.ActivityPub.ActivityPub
27 alias Pleroma.Web.ActivityPub.Visibility
28 alias Pleroma.Web.CommonAPI
29 alias Pleroma.Web.MastodonAPI.AccountView
30 alias Pleroma.Web.MastodonAPI.AppView
31 alias Pleroma.Web.MastodonAPI.ConversationView
32 alias Pleroma.Web.MastodonAPI.FilterView
33 alias Pleroma.Web.MastodonAPI.ListView
34 alias Pleroma.Web.MastodonAPI.MastodonAPI
35 alias Pleroma.Web.MastodonAPI.MastodonView
36 alias Pleroma.Web.MastodonAPI.ReportView
37 alias Pleroma.Web.MastodonAPI.StatusView
38 alias Pleroma.Web.MediaProxy
39 alias Pleroma.Web.OAuth.App
40 alias Pleroma.Web.OAuth.Authorization
41 alias Pleroma.Web.OAuth.Scopes
42 alias Pleroma.Web.OAuth.Token
43 alias Pleroma.Web.TwitterAPI.TwitterAPI
48 require Pleroma.Constants
50 @rate_limited_relations_actions ~w(follow unfollow)a
54 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
57 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
58 plug(RateLimiter, :app_account_creation when action == :account_register)
59 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
60 plug(RateLimiter, :password_reset when action == :password_reset)
61 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
63 @local_mastodon_name "Mastodon-Local"
65 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
67 def create_app(conn, params) do
68 scopes = Scopes.fetch_scopes(params, ["read"])
72 |> Map.drop(["scope", "scopes"])
73 |> Map.put("scopes", scopes)
75 with cs <- App.register_changeset(%App{}, app_attrs),
76 false <- cs.changes[:client_name] == @local_mastodon_name,
77 {:ok, app} <- Repo.insert(cs) do
80 |> render("show.json", %{app: app})
89 value_function \\ fn x -> {:ok, x} end
91 if Map.has_key?(params, params_field) do
92 case value_function.(params[params_field]) do
93 {:ok, new_value} -> Map.put(map, map_field, new_value)
101 def update_credentials(%{assigns: %{user: user}} = conn, params) do
106 |> add_if_present(params, "display_name", :name)
107 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
108 |> add_if_present(params, "avatar", :avatar, fn value ->
109 with %Plug.Upload{} <- value,
110 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
117 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
121 |> Map.get(:emoji, [])
122 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
129 :hide_followers_count,
135 :skip_thread_containment,
138 |> Enum.reduce(%{}, fn key, acc ->
139 add_if_present(acc, params, to_string(key), key, fn value ->
140 {:ok, truthy_param?(value)}
143 |> add_if_present(params, "default_scope", :default_scope)
144 |> add_if_present(params, "fields", :fields, fn fields ->
145 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
149 |> add_if_present(params, "fields", :raw_fields)
150 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
151 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
153 |> add_if_present(params, "header", :banner, fn value ->
154 with %Plug.Upload{} <- value,
155 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
161 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
162 with %Plug.Upload{} <- value,
163 {:ok, object} <- ActivityPub.upload(value, type: :background) do
169 |> Map.put(:emoji, user_info_emojis)
173 |> User.update_changeset(user_params)
174 |> User.change_info(&User.Info.profile_update(&1, info_params))
176 with {:ok, user} <- User.update_and_set_cache(changeset) do
177 if original_user != user, do: CommonAPI.update(user)
181 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
184 _e -> render_error(conn, :forbidden, "Invalid request")
188 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
189 change = Changeset.change(user, %{avatar: nil})
190 {:ok, user} = User.update_and_set_cache(change)
191 CommonAPI.update(user)
193 json(conn, %{url: nil})
196 def update_avatar(%{assigns: %{user: user}} = conn, params) do
197 {:ok, object} = ActivityPub.upload(params, type: :avatar)
198 change = Changeset.change(user, %{avatar: object.data})
199 {:ok, user} = User.update_and_set_cache(change)
200 CommonAPI.update(user)
201 %{"url" => [%{"href" => href} | _]} = object.data
203 json(conn, %{url: href})
206 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
207 new_info = %{"banner" => %{}}
209 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
210 CommonAPI.update(user)
211 json(conn, %{url: nil})
215 def update_banner(%{assigns: %{user: user}} = conn, params) do
216 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
217 new_info <- %{"banner" => object.data},
218 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
219 CommonAPI.update(user)
220 %{"url" => [%{"href" => href} | _]} = object.data
222 json(conn, %{url: href})
226 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
227 new_info = %{"background" => %{}}
229 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
230 json(conn, %{url: nil})
234 def update_background(%{assigns: %{user: user}} = conn, params) do
235 with {:ok, object} <- ActivityPub.upload(params, type: :background),
236 new_info <- %{"background" => object.data},
237 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
238 %{"url" => [%{"href" => href} | _]} = object.data
240 json(conn, %{url: href})
244 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
245 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
248 AccountView.render("account.json", %{
251 with_pleroma_settings: true,
252 with_chat_token: chat_token
258 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
259 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
262 |> render("short.json", %{app: app})
266 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
267 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
268 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
269 account = AccountView.render("account.json", %{user: user, for: for_user})
272 _e -> render_error(conn, :not_found, "Can't find user")
276 @mastodon_api_level "2.7.2"
278 def masto_instance(conn, _params) do
279 instance = Config.get(:instance)
283 title: Keyword.get(instance, :name),
284 description: Keyword.get(instance, :description),
285 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
286 email: Keyword.get(instance, :email),
288 streaming_api: Pleroma.Web.Endpoint.websocket_url()
290 stats: Stats.get_stats(),
291 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
293 registrations: Pleroma.Config.get([:instance, :registrations_open]),
294 # Extra (not present in Mastodon):
295 max_toot_chars: Keyword.get(instance, :limit),
296 poll_limits: Keyword.get(instance, :poll_limits)
302 def peers(conn, _params) do
303 json(conn, Stats.get_peers())
306 defp mastodonized_emoji do
307 Pleroma.Emoji.get_all()
308 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
309 url = to_string(URI.merge(Web.base_url(), relative_url))
312 "shortcode" => shortcode,
314 "visible_in_picker" => true,
317 # Assuming that a comma is authorized in the category name
318 "category" => (tags -- ["Custom"]) |> Enum.join(",")
323 def custom_emojis(conn, _params) do
324 mastodon_emoji = mastodonized_emoji()
325 json(conn, mastodon_emoji)
328 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
329 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
332 |> Map.put("tag", params["tagged"])
334 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
337 |> add_link_headers(activities)
338 |> put_view(StatusView)
339 |> render("index.json", %{
340 activities: activities,
347 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
348 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
349 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
350 true <- Visibility.visible_for_user?(activity, user) do
352 |> put_view(StatusView)
353 |> try_render("poll.json", %{object: object, for: user})
355 error when is_nil(error) or error == false ->
356 render_error(conn, :not_found, "Record not found")
360 defp get_cached_vote_or_vote(user, object, choices) do
361 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
364 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
365 case CommonAPI.vote(user, object, choices) do
366 {:error, _message} = res -> {:ignore, res}
367 res -> {:commit, res}
374 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
375 with %Object{} = object <- Object.get_by_id(id),
376 true <- object.data["type"] == "Question",
377 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
378 true <- Visibility.visible_for_user?(activity, user),
379 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
381 |> put_view(StatusView)
382 |> try_render("poll.json", %{object: object, for: user})
385 render_error(conn, :not_found, "Record not found")
388 render_error(conn, :not_found, "Record not found")
392 |> put_status(:unprocessable_entity)
393 |> json(%{error: message})
397 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
399 q = from(u in User, where: u.id in ^id)
400 targets = Repo.all(q)
403 |> put_view(AccountView)
404 |> render("relationships.json", %{user: user, targets: targets})
407 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
408 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
410 def update_media(%{assigns: %{user: user}} = conn, data) do
411 with %Object{} = object <- Repo.get(Object, data["id"]),
412 true <- Object.authorize_mutation(object, user),
413 true <- is_binary(data["description"]),
414 description <- data["description"] do
415 new_data = %{object.data | "name" => description}
419 |> Object.change(%{data: new_data})
422 attachment_data = Map.put(new_data, "id", object.id)
425 |> put_view(StatusView)
426 |> render("attachment.json", %{attachment: attachment_data})
430 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
431 with {:ok, object} <-
434 actor: User.ap_id(user),
435 description: Map.get(data, "description")
437 attachment_data = Map.put(object.data, "id", object.id)
440 |> put_view(StatusView)
441 |> render("attachment.json", %{attachment: attachment_data})
445 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
446 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
447 %{} = attachment_data <- Map.put(object.data, "id", object.id),
448 # Reject if not an image
449 %{type: "image"} = rendered <-
450 StatusView.render("attachment.json", %{attachment: attachment_data}) do
452 # Save to the user's info
453 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
457 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
461 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
462 mascot = User.get_mascot(user)
468 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
469 with %User{} = user <- User.get_cached_by_id(id),
470 followers <- MastodonAPI.get_followers(user, params) do
473 for_user && user.id == for_user.id -> followers
474 user.info.hide_followers -> []
479 |> add_link_headers(followers)
480 |> put_view(AccountView)
481 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
485 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
486 with %User{} = user <- User.get_cached_by_id(id),
487 followers <- MastodonAPI.get_friends(user, params) do
490 for_user && user.id == for_user.id -> followers
491 user.info.hide_follows -> []
496 |> add_link_headers(followers)
497 |> put_view(AccountView)
498 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
502 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
503 follow_requests = User.get_follow_requests(followed)
506 |> put_view(AccountView)
507 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
510 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
511 with %User{} = follower <- User.get_cached_by_id(id),
512 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
514 |> put_view(AccountView)
515 |> render("relationship.json", %{user: followed, target: follower})
519 |> put_status(:forbidden)
520 |> json(%{error: message})
524 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
525 with %User{} = follower <- User.get_cached_by_id(id),
526 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
528 |> put_view(AccountView)
529 |> render("relationship.json", %{user: followed, target: follower})
533 |> put_status(:forbidden)
534 |> json(%{error: message})
538 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
539 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
540 {_, true} <- {:followed, follower.id != followed.id},
541 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
543 |> put_view(AccountView)
544 |> render("relationship.json", %{user: follower, target: followed})
551 |> put_status(:forbidden)
552 |> json(%{error: message})
556 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
557 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
558 {_, true} <- {:followed, follower.id != followed.id},
559 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
561 |> put_view(AccountView)
562 |> render("account.json", %{user: followed, for: follower})
569 |> put_status(:forbidden)
570 |> json(%{error: message})
574 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
575 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
576 {_, true} <- {:followed, follower.id != followed.id},
577 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
579 |> put_view(AccountView)
580 |> render("relationship.json", %{user: follower, target: followed})
590 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
592 if Map.has_key?(params, "notifications"),
593 do: params["notifications"] in [true, "True", "true", "1"],
596 with %User{} = muted <- User.get_cached_by_id(id),
597 {:ok, muter} <- User.mute(muter, muted, notifications) do
599 |> put_view(AccountView)
600 |> render("relationship.json", %{user: muter, target: muted})
604 |> put_status(:forbidden)
605 |> json(%{error: message})
609 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
610 with %User{} = muted <- User.get_cached_by_id(id),
611 {:ok, muter} <- User.unmute(muter, muted) do
613 |> put_view(AccountView)
614 |> render("relationship.json", %{user: muter, target: muted})
618 |> put_status(:forbidden)
619 |> json(%{error: message})
623 def mutes(%{assigns: %{user: user}} = conn, _) do
624 with muted_accounts <- User.muted_users(user) do
625 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
630 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
631 with %User{} = blocked <- User.get_cached_by_id(id),
632 {:ok, blocker} <- User.block(blocker, blocked),
633 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
635 |> put_view(AccountView)
636 |> render("relationship.json", %{user: blocker, target: blocked})
640 |> put_status(:forbidden)
641 |> json(%{error: message})
645 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
646 with %User{} = blocked <- User.get_cached_by_id(id),
647 {:ok, blocker} <- User.unblock(blocker, blocked),
648 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
650 |> put_view(AccountView)
651 |> render("relationship.json", %{user: blocker, target: blocked})
655 |> put_status(:forbidden)
656 |> json(%{error: message})
660 def blocks(%{assigns: %{user: user}} = conn, _) do
661 with blocked_accounts <- User.blocked_users(user) do
662 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
667 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
668 json(conn, info.domain_blocks || [])
671 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
672 User.block_domain(blocker, domain)
676 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
677 User.unblock_domain(blocker, domain)
681 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
682 with %User{} = subscription_target <- User.get_cached_by_id(id),
683 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
685 |> put_view(AccountView)
686 |> render("relationship.json", %{user: user, target: subscription_target})
690 |> put_status(:forbidden)
691 |> json(%{error: message})
695 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
696 with %User{} = subscription_target <- User.get_cached_by_id(id),
697 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
699 |> put_view(AccountView)
700 |> render("relationship.json", %{user: user, target: subscription_target})
704 |> put_status(:forbidden)
705 |> json(%{error: message})
709 def favourites(%{assigns: %{user: user}} = conn, params) do
712 |> Map.put("type", "Create")
713 |> Map.put("favorited_by", user.ap_id)
714 |> Map.put("blocking_user", user)
717 ActivityPub.fetch_activities([], params)
721 |> add_link_headers(activities)
722 |> put_view(StatusView)
723 |> render("index.json", %{activities: activities, for: user, as: :activity})
726 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
727 with %User{} = user <- User.get_by_id(id),
728 false <- user.info.hide_favorites do
731 |> Map.put("type", "Create")
732 |> Map.put("favorited_by", user.ap_id)
733 |> Map.put("blocking_user", for_user)
737 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
739 [Pleroma.Constants.as_public()]
744 |> ActivityPub.fetch_activities(params)
748 |> add_link_headers(activities)
749 |> put_view(StatusView)
750 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
752 nil -> {:error, :not_found}
753 true -> render_error(conn, :forbidden, "Can't get favorites")
757 def bookmarks(%{assigns: %{user: user}} = conn, params) do
758 user = User.get_cached_by_id(user.id)
761 Bookmark.for_user_query(user.id)
762 |> Pagination.fetch_paginated(params)
766 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
769 |> add_link_headers(bookmarks)
770 |> put_view(StatusView)
771 |> render("index.json", %{activities: activities, for: user, as: :activity})
774 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
775 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
776 res = ListView.render("lists.json", lists: lists)
780 def index(%{assigns: %{user: user}} = conn, _params) do
781 token = get_session(conn, :oauth_token)
784 mastodon_emoji = mastodonized_emoji()
786 limit = Config.get([:instance, :limit])
789 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
794 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
797 domain: Pleroma.Web.Endpoint.host(),
800 unfollow_modal: false,
803 auto_play_gif: false,
804 display_sensitive_media: false,
805 reduce_motion: false,
806 max_toot_chars: limit,
807 mascot: User.get_mascot(user)["url"]
809 poll_limits: Config.get([:instance, :poll_limits]),
811 delete_others_notice: present?(user.info.is_moderator),
812 admin: present?(user.info.is_admin)
816 default_privacy: user.info.default_scope,
817 default_sensitive: false,
818 allow_content_types: Config.get([:instance, :allowed_post_formats])
820 media_attachments: %{
821 accept_content_types: [
837 user.info.settings ||
867 push_subscription: nil,
869 custom_emojis: mastodon_emoji,
876 |> put_view(MastodonView)
877 |> render("index.html", %{initial_state: initial_state})
880 |> put_session(:return_to, conn.request_path)
881 |> redirect(to: "/web/login")
885 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
886 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
891 |> put_status(:internal_server_error)
892 |> json(%{error: inspect(e)})
896 def login(%{assigns: %{user: %User{}}} = conn, _params) do
897 redirect(conn, to: local_mastodon_root_path(conn))
900 @doc "Local Mastodon FE login init action"
901 def login(conn, %{"code" => auth_token}) do
902 with {:ok, app} <- get_or_make_app(),
903 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
904 {:ok, token} <- Token.exchange_token(app, auth) do
906 |> put_session(:oauth_token, token.token)
907 |> redirect(to: local_mastodon_root_path(conn))
911 @doc "Local Mastodon FE callback action"
912 def login(conn, _) do
913 with {:ok, app} <- get_or_make_app() do
918 response_type: "code",
919 client_id: app.client_id,
921 scope: Enum.join(app.scopes, " ")
924 redirect(conn, to: path)
928 defp local_mastodon_root_path(conn) do
929 case get_session(conn, :return_to) do
931 mastodon_api_path(conn, :index, ["getting-started"])
934 delete_session(conn, :return_to)
939 defp get_or_make_app do
940 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
941 scopes = ["read", "write", "follow", "push"]
943 with %App{} = app <- Repo.get_by(App, find_attrs) do
945 if app.scopes == scopes do
949 |> Changeset.change(%{scopes: scopes})
957 App.register_changeset(
959 Map.put(find_attrs, :scopes, scopes)
966 def logout(conn, _) do
972 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
973 Logger.debug("Unimplemented, returning unmodified relationship")
975 with %User{} = target <- User.get_cached_by_id(id) do
977 |> put_view(AccountView)
978 |> render("relationship.json", %{user: user, target: target})
982 def empty_array(conn, _) do
983 Logger.debug("Unimplemented, returning an empty array")
987 def empty_object(conn, _) do
988 Logger.debug("Unimplemented, returning an empty object")
992 def get_filters(%{assigns: %{user: user}} = conn, _) do
993 filters = Filter.get_filters(user)
994 res = FilterView.render("filters.json", filters: filters)
999 %{assigns: %{user: user}} = conn,
1000 %{"phrase" => phrase, "context" => context} = params
1006 hide: Map.get(params, "irreversible", false),
1007 whole_word: Map.get(params, "boolean", true)
1011 {:ok, response} = Filter.create(query)
1012 res = FilterView.render("filter.json", filter: response)
1016 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1017 filter = Filter.get(filter_id, user)
1018 res = FilterView.render("filter.json", filter: filter)
1023 %{assigns: %{user: user}} = conn,
1024 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1028 filter_id: filter_id,
1031 hide: Map.get(params, "irreversible", nil),
1032 whole_word: Map.get(params, "boolean", true)
1036 {:ok, response} = Filter.update(query)
1037 res = FilterView.render("filter.json", filter: response)
1041 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1044 filter_id: filter_id
1047 {:ok, _} = Filter.delete(query)
1051 def suggestions(%{assigns: %{user: user}} = conn, _) do
1052 suggestions = Config.get(:suggestions)
1054 if Keyword.get(suggestions, :enabled, false) do
1055 api = Keyword.get(suggestions, :third_party_engine, "")
1056 timeout = Keyword.get(suggestions, :timeout, 5000)
1057 limit = Keyword.get(suggestions, :limit, 23)
1059 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1061 user = user.nickname
1065 |> String.replace("{{host}}", host)
1066 |> String.replace("{{user}}", user)
1068 with {:ok, %{status: 200, body: body}} <-
1069 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1070 {:ok, data} <- Jason.decode(body) do
1073 |> Enum.slice(0, limit)
1076 |> Map.put("id", fetch_suggestion_id(x))
1077 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1078 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1084 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1091 defp fetch_suggestion_id(attrs) do
1092 case User.get_or_fetch(attrs["acct"]) do
1093 {:ok, %User{id: id}} -> id
1098 def reports(%{assigns: %{user: user}} = conn, params) do
1099 case CommonAPI.report(user, params) do
1102 |> put_view(ReportView)
1103 |> try_render("report.json", %{activity: activity})
1107 |> put_status(:bad_request)
1108 |> json(%{error: err})
1112 def account_register(
1113 %{assigns: %{app: app}} = conn,
1114 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1122 "captcha_answer_data",
1126 |> Map.put("nickname", nickname)
1127 |> Map.put("fullname", params["fullname"] || nickname)
1128 |> Map.put("bio", params["bio"] || "")
1129 |> Map.put("confirm", params["password"])
1131 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1132 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1134 token_type: "Bearer",
1135 access_token: token.token,
1137 created_at: Token.Utils.format_created_at(token)
1142 |> put_status(:bad_request)
1147 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1148 render_error(conn, :bad_request, "Missing parameters")
1151 def account_register(conn, _) do
1152 render_error(conn, :forbidden, "Invalid credentials")
1155 def conversations(%{assigns: %{user: user}} = conn, params) do
1156 participations = Participation.for_user_with_last_activity_id(user, params)
1159 Enum.map(participations, fn participation ->
1160 ConversationView.render("participation.json", %{participation: participation, for: user})
1164 |> add_link_headers(participations)
1165 |> json(conversations)
1168 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1169 with %Participation{} = participation <-
1170 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1171 {:ok, participation} <- Participation.mark_as_read(participation) do
1172 participation_view =
1173 ConversationView.render("participation.json", %{participation: participation, for: user})
1176 |> json(participation_view)
1180 def password_reset(conn, params) do
1181 nickname_or_email = params["email"] || params["nickname"]
1183 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1185 |> put_status(:no_content)
1188 {:error, "unknown user"} ->
1189 send_resp(conn, :not_found, "")
1192 send_resp(conn, :bad_request, "")
1196 def account_confirmation_resend(conn, params) do
1197 nickname_or_email = params["email"] || params["nickname"]
1199 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1200 {:ok, _} <- User.try_send_confirmation_email(user) do
1202 |> json_response(:no_content, "")
1206 def try_render(conn, target, params)
1207 when is_binary(target) do
1208 case render(conn, target, params) do
1209 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1214 def try_render(conn, _, _) do
1215 render_error(conn, :not_implemented, "Can't display this activity")
1218 defp present?(nil), do: false
1219 defp present?(false), do: false
1220 defp present?(_), do: true