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
22 alias Pleroma.ScheduledActivity
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.ListView
33 alias Pleroma.Web.MastodonAPI.MastodonAPI
34 alias Pleroma.Web.MastodonAPI.MastodonView
35 alias Pleroma.Web.MastodonAPI.ReportView
36 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
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 scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
398 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
400 |> add_link_headers(scheduled_activities)
401 |> put_view(ScheduledActivityView)
402 |> render("index.json", %{scheduled_activities: scheduled_activities})
406 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
407 with %ScheduledActivity{} = scheduled_activity <-
408 ScheduledActivity.get(user, scheduled_activity_id) do
410 |> put_view(ScheduledActivityView)
411 |> render("show.json", %{scheduled_activity: scheduled_activity})
413 _ -> {:error, :not_found}
417 def update_scheduled_status(
418 %{assigns: %{user: user}} = conn,
419 %{"id" => scheduled_activity_id} = params
421 with %ScheduledActivity{} = scheduled_activity <-
422 ScheduledActivity.get(user, scheduled_activity_id),
423 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
425 |> put_view(ScheduledActivityView)
426 |> render("show.json", %{scheduled_activity: scheduled_activity})
428 nil -> {:error, :not_found}
433 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
434 with %ScheduledActivity{} = scheduled_activity <-
435 ScheduledActivity.get(user, scheduled_activity_id),
436 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
438 |> put_view(ScheduledActivityView)
439 |> render("show.json", %{scheduled_activity: scheduled_activity})
441 nil -> {:error, :not_found}
446 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
448 q = from(u in User, where: u.id in ^id)
449 targets = Repo.all(q)
452 |> put_view(AccountView)
453 |> render("relationships.json", %{user: user, targets: targets})
456 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
457 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
459 def update_media(%{assigns: %{user: user}} = conn, data) do
460 with %Object{} = object <- Repo.get(Object, data["id"]),
461 true <- Object.authorize_mutation(object, user),
462 true <- is_binary(data["description"]),
463 description <- data["description"] do
464 new_data = %{object.data | "name" => description}
468 |> Object.change(%{data: new_data})
471 attachment_data = Map.put(new_data, "id", object.id)
474 |> put_view(StatusView)
475 |> render("attachment.json", %{attachment: attachment_data})
479 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
480 with {:ok, object} <-
483 actor: User.ap_id(user),
484 description: Map.get(data, "description")
486 attachment_data = Map.put(object.data, "id", object.id)
489 |> put_view(StatusView)
490 |> render("attachment.json", %{attachment: attachment_data})
494 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
495 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
496 %{} = attachment_data <- Map.put(object.data, "id", object.id),
497 # Reject if not an image
498 %{type: "image"} = rendered <-
499 StatusView.render("attachment.json", %{attachment: attachment_data}) do
501 # Save to the user's info
502 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
506 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
510 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
511 mascot = User.get_mascot(user)
517 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
518 with %User{} = user <- User.get_cached_by_id(id),
519 followers <- MastodonAPI.get_followers(user, params) do
522 for_user && user.id == for_user.id -> followers
523 user.info.hide_followers -> []
528 |> add_link_headers(followers)
529 |> put_view(AccountView)
530 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
534 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
535 with %User{} = user <- User.get_cached_by_id(id),
536 followers <- MastodonAPI.get_friends(user, params) do
539 for_user && user.id == for_user.id -> followers
540 user.info.hide_follows -> []
545 |> add_link_headers(followers)
546 |> put_view(AccountView)
547 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
551 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
552 follow_requests = User.get_follow_requests(followed)
555 |> put_view(AccountView)
556 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
559 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
560 with %User{} = follower <- User.get_cached_by_id(id),
561 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
563 |> put_view(AccountView)
564 |> render("relationship.json", %{user: followed, target: follower})
568 |> put_status(:forbidden)
569 |> json(%{error: message})
573 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
574 with %User{} = follower <- User.get_cached_by_id(id),
575 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
577 |> put_view(AccountView)
578 |> render("relationship.json", %{user: followed, target: follower})
582 |> put_status(:forbidden)
583 |> json(%{error: message})
587 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
588 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
589 {_, true} <- {:followed, follower.id != followed.id},
590 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
592 |> put_view(AccountView)
593 |> render("relationship.json", %{user: follower, target: followed})
600 |> put_status(:forbidden)
601 |> json(%{error: message})
605 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
606 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
607 {_, true} <- {:followed, follower.id != followed.id},
608 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
610 |> put_view(AccountView)
611 |> render("account.json", %{user: followed, for: follower})
618 |> put_status(:forbidden)
619 |> json(%{error: message})
623 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
624 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
625 {_, true} <- {:followed, follower.id != followed.id},
626 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
628 |> put_view(AccountView)
629 |> render("relationship.json", %{user: follower, target: followed})
639 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
641 if Map.has_key?(params, "notifications"),
642 do: params["notifications"] in [true, "True", "true", "1"],
645 with %User{} = muted <- User.get_cached_by_id(id),
646 {:ok, muter} <- User.mute(muter, muted, notifications) do
648 |> put_view(AccountView)
649 |> render("relationship.json", %{user: muter, target: muted})
653 |> put_status(:forbidden)
654 |> json(%{error: message})
658 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
659 with %User{} = muted <- User.get_cached_by_id(id),
660 {:ok, muter} <- User.unmute(muter, muted) do
662 |> put_view(AccountView)
663 |> render("relationship.json", %{user: muter, target: muted})
667 |> put_status(:forbidden)
668 |> json(%{error: message})
672 def mutes(%{assigns: %{user: user}} = conn, _) do
673 with muted_accounts <- User.muted_users(user) do
674 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
679 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
680 with %User{} = blocked <- User.get_cached_by_id(id),
681 {:ok, blocker} <- User.block(blocker, blocked),
682 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
684 |> put_view(AccountView)
685 |> render("relationship.json", %{user: blocker, target: blocked})
689 |> put_status(:forbidden)
690 |> json(%{error: message})
694 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
695 with %User{} = blocked <- User.get_cached_by_id(id),
696 {:ok, blocker} <- User.unblock(blocker, blocked),
697 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
699 |> put_view(AccountView)
700 |> render("relationship.json", %{user: blocker, target: blocked})
704 |> put_status(:forbidden)
705 |> json(%{error: message})
709 def blocks(%{assigns: %{user: user}} = conn, _) do
710 with blocked_accounts <- User.blocked_users(user) do
711 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
716 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
717 json(conn, info.domain_blocks || [])
720 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
721 User.block_domain(blocker, domain)
725 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
726 User.unblock_domain(blocker, domain)
730 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
731 with %User{} = subscription_target <- User.get_cached_by_id(id),
732 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
734 |> put_view(AccountView)
735 |> render("relationship.json", %{user: user, target: subscription_target})
739 |> put_status(:forbidden)
740 |> json(%{error: message})
744 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
745 with %User{} = subscription_target <- User.get_cached_by_id(id),
746 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
748 |> put_view(AccountView)
749 |> render("relationship.json", %{user: user, target: subscription_target})
753 |> put_status(:forbidden)
754 |> json(%{error: message})
758 def favourites(%{assigns: %{user: user}} = conn, params) do
761 |> Map.put("type", "Create")
762 |> Map.put("favorited_by", user.ap_id)
763 |> Map.put("blocking_user", user)
766 ActivityPub.fetch_activities([], params)
770 |> add_link_headers(activities)
771 |> put_view(StatusView)
772 |> render("index.json", %{activities: activities, for: user, as: :activity})
775 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
776 with %User{} = user <- User.get_by_id(id),
777 false <- user.info.hide_favorites do
780 |> Map.put("type", "Create")
781 |> Map.put("favorited_by", user.ap_id)
782 |> Map.put("blocking_user", for_user)
786 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
788 [Pleroma.Constants.as_public()]
793 |> ActivityPub.fetch_activities(params)
797 |> add_link_headers(activities)
798 |> put_view(StatusView)
799 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
801 nil -> {:error, :not_found}
802 true -> render_error(conn, :forbidden, "Can't get favorites")
806 def bookmarks(%{assigns: %{user: user}} = conn, params) do
807 user = User.get_cached_by_id(user.id)
810 Bookmark.for_user_query(user.id)
811 |> Pagination.fetch_paginated(params)
815 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
818 |> add_link_headers(bookmarks)
819 |> put_view(StatusView)
820 |> render("index.json", %{activities: activities, for: user, as: :activity})
823 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
824 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
825 res = ListView.render("lists.json", lists: lists)
829 def index(%{assigns: %{user: user}} = conn, _params) do
830 token = get_session(conn, :oauth_token)
833 mastodon_emoji = mastodonized_emoji()
835 limit = Config.get([:instance, :limit])
838 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
843 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
846 domain: Pleroma.Web.Endpoint.host(),
849 unfollow_modal: false,
852 auto_play_gif: false,
853 display_sensitive_media: false,
854 reduce_motion: false,
855 max_toot_chars: limit,
856 mascot: User.get_mascot(user)["url"]
858 poll_limits: Config.get([:instance, :poll_limits]),
860 delete_others_notice: present?(user.info.is_moderator),
861 admin: present?(user.info.is_admin)
865 default_privacy: user.info.default_scope,
866 default_sensitive: false,
867 allow_content_types: Config.get([:instance, :allowed_post_formats])
869 media_attachments: %{
870 accept_content_types: [
886 user.info.settings ||
916 push_subscription: nil,
918 custom_emojis: mastodon_emoji,
925 |> put_view(MastodonView)
926 |> render("index.html", %{initial_state: initial_state})
929 |> put_session(:return_to, conn.request_path)
930 |> redirect(to: "/web/login")
934 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
935 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
940 |> put_status(:internal_server_error)
941 |> json(%{error: inspect(e)})
945 def login(%{assigns: %{user: %User{}}} = conn, _params) do
946 redirect(conn, to: local_mastodon_root_path(conn))
949 @doc "Local Mastodon FE login init action"
950 def login(conn, %{"code" => auth_token}) do
951 with {:ok, app} <- get_or_make_app(),
952 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
953 {:ok, token} <- Token.exchange_token(app, auth) do
955 |> put_session(:oauth_token, token.token)
956 |> redirect(to: local_mastodon_root_path(conn))
960 @doc "Local Mastodon FE callback action"
961 def login(conn, _) do
962 with {:ok, app} <- get_or_make_app() do
967 response_type: "code",
968 client_id: app.client_id,
970 scope: Enum.join(app.scopes, " ")
973 redirect(conn, to: path)
977 defp local_mastodon_root_path(conn) do
978 case get_session(conn, :return_to) do
980 mastodon_api_path(conn, :index, ["getting-started"])
983 delete_session(conn, :return_to)
988 defp get_or_make_app do
989 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
990 scopes = ["read", "write", "follow", "push"]
992 with %App{} = app <- Repo.get_by(App, find_attrs) do
994 if app.scopes == scopes do
998 |> Changeset.change(%{scopes: scopes})
1006 App.register_changeset(
1008 Map.put(find_attrs, :scopes, scopes)
1015 def logout(conn, _) do
1018 |> redirect(to: "/")
1021 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1022 Logger.debug("Unimplemented, returning unmodified relationship")
1024 with %User{} = target <- User.get_cached_by_id(id) do
1026 |> put_view(AccountView)
1027 |> render("relationship.json", %{user: user, target: target})
1031 def empty_array(conn, _) do
1032 Logger.debug("Unimplemented, returning an empty array")
1036 def empty_object(conn, _) do
1037 Logger.debug("Unimplemented, returning an empty object")
1041 def suggestions(%{assigns: %{user: user}} = conn, _) do
1042 suggestions = Config.get(:suggestions)
1044 if Keyword.get(suggestions, :enabled, false) do
1045 api = Keyword.get(suggestions, :third_party_engine, "")
1046 timeout = Keyword.get(suggestions, :timeout, 5000)
1047 limit = Keyword.get(suggestions, :limit, 23)
1049 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1051 user = user.nickname
1055 |> String.replace("{{host}}", host)
1056 |> String.replace("{{user}}", user)
1058 with {:ok, %{status: 200, body: body}} <-
1059 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1060 {:ok, data} <- Jason.decode(body) do
1063 |> Enum.slice(0, limit)
1066 |> Map.put("id", fetch_suggestion_id(x))
1067 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1068 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1074 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1081 defp fetch_suggestion_id(attrs) do
1082 case User.get_or_fetch(attrs["acct"]) do
1083 {:ok, %User{id: id}} -> id
1088 def reports(%{assigns: %{user: user}} = conn, params) do
1089 case CommonAPI.report(user, params) do
1092 |> put_view(ReportView)
1093 |> try_render("report.json", %{activity: activity})
1097 |> put_status(:bad_request)
1098 |> json(%{error: err})
1102 def account_register(
1103 %{assigns: %{app: app}} = conn,
1104 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1112 "captcha_answer_data",
1116 |> Map.put("nickname", nickname)
1117 |> Map.put("fullname", params["fullname"] || nickname)
1118 |> Map.put("bio", params["bio"] || "")
1119 |> Map.put("confirm", params["password"])
1121 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1122 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1124 token_type: "Bearer",
1125 access_token: token.token,
1127 created_at: Token.Utils.format_created_at(token)
1132 |> put_status(:bad_request)
1137 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1138 render_error(conn, :bad_request, "Missing parameters")
1141 def account_register(conn, _) do
1142 render_error(conn, :forbidden, "Invalid credentials")
1145 def conversations(%{assigns: %{user: user}} = conn, params) do
1146 participations = Participation.for_user_with_last_activity_id(user, params)
1149 Enum.map(participations, fn participation ->
1150 ConversationView.render("participation.json", %{participation: participation, for: user})
1154 |> add_link_headers(participations)
1155 |> json(conversations)
1158 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1159 with %Participation{} = participation <-
1160 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1161 {:ok, participation} <- Participation.mark_as_read(participation) do
1162 participation_view =
1163 ConversationView.render("participation.json", %{participation: participation, for: user})
1166 |> json(participation_view)
1170 def password_reset(conn, params) do
1171 nickname_or_email = params["email"] || params["nickname"]
1173 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1175 |> put_status(:no_content)
1178 {:error, "unknown user"} ->
1179 send_resp(conn, :not_found, "")
1182 send_resp(conn, :bad_request, "")
1186 def account_confirmation_resend(conn, params) do
1187 nickname_or_email = params["email"] || params["nickname"]
1189 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1190 {:ok, _} <- User.try_send_confirmation_email(user) do
1192 |> json_response(:no_content, "")
1196 def try_render(conn, target, params)
1197 when is_binary(target) do
1198 case render(conn, target, params) do
1199 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1204 def try_render(conn, _, _) do
1205 render_error(conn, :not_implemented, "Can't display this activity")
1208 defp present?(nil), do: false
1209 defp present?(false), do: false
1210 defp present?(_), do: true