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
23 alias Pleroma.ScheduledActivity
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Visibility
29 alias Pleroma.Web.CommonAPI
30 alias Pleroma.Web.MastodonAPI.AccountView
31 alias Pleroma.Web.MastodonAPI.AppView
32 alias Pleroma.Web.MastodonAPI.ConversationView
33 alias Pleroma.Web.MastodonAPI.FilterView
34 alias Pleroma.Web.MastodonAPI.ListView
35 alias Pleroma.Web.MastodonAPI.MastodonAPI
36 alias Pleroma.Web.MastodonAPI.MastodonView
37 alias Pleroma.Web.MastodonAPI.ReportView
38 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
39 alias Pleroma.Web.MastodonAPI.StatusView
40 alias Pleroma.Web.MediaProxy
41 alias Pleroma.Web.OAuth.App
42 alias Pleroma.Web.OAuth.Authorization
43 alias Pleroma.Web.OAuth.Scopes
44 alias Pleroma.Web.OAuth.Token
45 alias Pleroma.Web.TwitterAPI.TwitterAPI
50 require Pleroma.Constants
52 @rate_limited_relations_actions ~w(follow unfollow)a
56 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
59 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
60 plug(RateLimiter, :app_account_creation when action == :account_register)
61 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
62 plug(RateLimiter, :password_reset when action == :password_reset)
63 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
65 @local_mastodon_name "Mastodon-Local"
67 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
69 def create_app(conn, params) do
70 scopes = Scopes.fetch_scopes(params, ["read"])
74 |> Map.drop(["scope", "scopes"])
75 |> Map.put("scopes", scopes)
77 with cs <- App.register_changeset(%App{}, app_attrs),
78 false <- cs.changes[:client_name] == @local_mastodon_name,
79 {:ok, app} <- Repo.insert(cs) do
82 |> render("show.json", %{app: app})
91 value_function \\ fn x -> {:ok, x} end
93 if Map.has_key?(params, params_field) do
94 case value_function.(params[params_field]) do
95 {:ok, new_value} -> Map.put(map, map_field, new_value)
103 def update_credentials(%{assigns: %{user: user}} = conn, params) do
108 |> add_if_present(params, "display_name", :name)
109 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
110 |> add_if_present(params, "avatar", :avatar, fn value ->
111 with %Plug.Upload{} <- value,
112 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
119 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
123 |> Map.get(:emoji, [])
124 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
131 :hide_followers_count,
137 :skip_thread_containment,
140 |> Enum.reduce(%{}, fn key, acc ->
141 add_if_present(acc, params, to_string(key), key, fn value ->
142 {:ok, truthy_param?(value)}
145 |> add_if_present(params, "default_scope", :default_scope)
146 |> add_if_present(params, "fields", :fields, fn fields ->
147 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
151 |> add_if_present(params, "fields", :raw_fields)
152 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
153 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
155 |> add_if_present(params, "header", :banner, fn value ->
156 with %Plug.Upload{} <- value,
157 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
163 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
164 with %Plug.Upload{} <- value,
165 {:ok, object} <- ActivityPub.upload(value, type: :background) do
171 |> Map.put(:emoji, user_info_emojis)
175 |> User.update_changeset(user_params)
176 |> User.change_info(&User.Info.profile_update(&1, info_params))
178 with {:ok, user} <- User.update_and_set_cache(changeset) do
179 if original_user != user, do: CommonAPI.update(user)
183 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
186 _e -> render_error(conn, :forbidden, "Invalid request")
190 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
191 change = Changeset.change(user, %{avatar: nil})
192 {:ok, user} = User.update_and_set_cache(change)
193 CommonAPI.update(user)
195 json(conn, %{url: nil})
198 def update_avatar(%{assigns: %{user: user}} = conn, params) do
199 {:ok, object} = ActivityPub.upload(params, type: :avatar)
200 change = Changeset.change(user, %{avatar: object.data})
201 {:ok, user} = User.update_and_set_cache(change)
202 CommonAPI.update(user)
203 %{"url" => [%{"href" => href} | _]} = object.data
205 json(conn, %{url: href})
208 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
209 new_info = %{"banner" => %{}}
211 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
212 CommonAPI.update(user)
213 json(conn, %{url: nil})
217 def update_banner(%{assigns: %{user: user}} = conn, params) do
218 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
219 new_info <- %{"banner" => object.data},
220 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
221 CommonAPI.update(user)
222 %{"url" => [%{"href" => href} | _]} = object.data
224 json(conn, %{url: href})
228 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
229 new_info = %{"background" => %{}}
231 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
232 json(conn, %{url: nil})
236 def update_background(%{assigns: %{user: user}} = conn, params) do
237 with {:ok, object} <- ActivityPub.upload(params, type: :background),
238 new_info <- %{"background" => object.data},
239 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
240 %{"url" => [%{"href" => href} | _]} = object.data
242 json(conn, %{url: href})
246 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
247 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
250 AccountView.render("account.json", %{
253 with_pleroma_settings: true,
254 with_chat_token: chat_token
260 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
261 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
264 |> render("short.json", %{app: app})
268 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
269 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
270 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
271 account = AccountView.render("account.json", %{user: user, for: for_user})
274 _e -> render_error(conn, :not_found, "Can't find user")
278 @mastodon_api_level "2.7.2"
280 def masto_instance(conn, _params) do
281 instance = Config.get(:instance)
285 title: Keyword.get(instance, :name),
286 description: Keyword.get(instance, :description),
287 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
288 email: Keyword.get(instance, :email),
290 streaming_api: Pleroma.Web.Endpoint.websocket_url()
292 stats: Stats.get_stats(),
293 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
295 registrations: Pleroma.Config.get([:instance, :registrations_open]),
296 # Extra (not present in Mastodon):
297 max_toot_chars: Keyword.get(instance, :limit),
298 poll_limits: Keyword.get(instance, :poll_limits)
304 def peers(conn, _params) do
305 json(conn, Stats.get_peers())
308 defp mastodonized_emoji do
309 Pleroma.Emoji.get_all()
310 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
311 url = to_string(URI.merge(Web.base_url(), relative_url))
314 "shortcode" => shortcode,
316 "visible_in_picker" => true,
319 # Assuming that a comma is authorized in the category name
320 "category" => (tags -- ["Custom"]) |> Enum.join(",")
325 def custom_emojis(conn, _params) do
326 mastodon_emoji = mastodonized_emoji()
327 json(conn, mastodon_emoji)
330 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
331 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
334 |> Map.put("tag", params["tagged"])
336 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
339 |> add_link_headers(activities)
340 |> put_view(StatusView)
341 |> render("index.json", %{
342 activities: activities,
349 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
350 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
351 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
352 true <- Visibility.visible_for_user?(activity, user) do
354 |> put_view(StatusView)
355 |> try_render("poll.json", %{object: object, for: user})
357 error when is_nil(error) or error == false ->
358 render_error(conn, :not_found, "Record not found")
362 defp get_cached_vote_or_vote(user, object, choices) do
363 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
366 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
367 case CommonAPI.vote(user, object, choices) do
368 {:error, _message} = res -> {:ignore, res}
369 res -> {:commit, res}
376 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
377 with %Object{} = object <- Object.get_by_id(id),
378 true <- object.data["type"] == "Question",
379 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
380 true <- Visibility.visible_for_user?(activity, user),
381 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
383 |> put_view(StatusView)
384 |> try_render("poll.json", %{object: object, for: user})
387 render_error(conn, :not_found, "Record not found")
390 render_error(conn, :not_found, "Record not found")
394 |> put_status(:unprocessable_entity)
395 |> json(%{error: message})
399 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
400 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
402 |> add_link_headers(scheduled_activities)
403 |> put_view(ScheduledActivityView)
404 |> render("index.json", %{scheduled_activities: scheduled_activities})
408 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
409 with %ScheduledActivity{} = scheduled_activity <-
410 ScheduledActivity.get(user, scheduled_activity_id) do
412 |> put_view(ScheduledActivityView)
413 |> render("show.json", %{scheduled_activity: scheduled_activity})
415 _ -> {:error, :not_found}
419 def update_scheduled_status(
420 %{assigns: %{user: user}} = conn,
421 %{"id" => scheduled_activity_id} = params
423 with %ScheduledActivity{} = scheduled_activity <-
424 ScheduledActivity.get(user, scheduled_activity_id),
425 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
427 |> put_view(ScheduledActivityView)
428 |> render("show.json", %{scheduled_activity: scheduled_activity})
430 nil -> {:error, :not_found}
435 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
436 with %ScheduledActivity{} = scheduled_activity <-
437 ScheduledActivity.get(user, scheduled_activity_id),
438 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
440 |> put_view(ScheduledActivityView)
441 |> render("show.json", %{scheduled_activity: scheduled_activity})
443 nil -> {:error, :not_found}
448 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
450 q = from(u in User, where: u.id in ^id)
451 targets = Repo.all(q)
454 |> put_view(AccountView)
455 |> render("relationships.json", %{user: user, targets: targets})
458 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
459 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
461 def update_media(%{assigns: %{user: user}} = conn, data) do
462 with %Object{} = object <- Repo.get(Object, data["id"]),
463 true <- Object.authorize_mutation(object, user),
464 true <- is_binary(data["description"]),
465 description <- data["description"] do
466 new_data = %{object.data | "name" => description}
470 |> Object.change(%{data: new_data})
473 attachment_data = Map.put(new_data, "id", object.id)
476 |> put_view(StatusView)
477 |> render("attachment.json", %{attachment: attachment_data})
481 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
482 with {:ok, object} <-
485 actor: User.ap_id(user),
486 description: Map.get(data, "description")
488 attachment_data = Map.put(object.data, "id", object.id)
491 |> put_view(StatusView)
492 |> render("attachment.json", %{attachment: attachment_data})
496 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
497 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
498 %{} = attachment_data <- Map.put(object.data, "id", object.id),
499 # Reject if not an image
500 %{type: "image"} = rendered <-
501 StatusView.render("attachment.json", %{attachment: attachment_data}) do
503 # Save to the user's info
504 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
508 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
512 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
513 mascot = User.get_mascot(user)
519 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
520 with %User{} = user <- User.get_cached_by_id(id),
521 followers <- MastodonAPI.get_followers(user, params) do
524 for_user && user.id == for_user.id -> followers
525 user.info.hide_followers -> []
530 |> add_link_headers(followers)
531 |> put_view(AccountView)
532 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
536 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
537 with %User{} = user <- User.get_cached_by_id(id),
538 followers <- MastodonAPI.get_friends(user, params) do
541 for_user && user.id == for_user.id -> followers
542 user.info.hide_follows -> []
547 |> add_link_headers(followers)
548 |> put_view(AccountView)
549 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
553 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
554 follow_requests = User.get_follow_requests(followed)
557 |> put_view(AccountView)
558 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
561 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
562 with %User{} = follower <- User.get_cached_by_id(id),
563 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
565 |> put_view(AccountView)
566 |> render("relationship.json", %{user: followed, target: follower})
570 |> put_status(:forbidden)
571 |> json(%{error: message})
575 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
576 with %User{} = follower <- User.get_cached_by_id(id),
577 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
579 |> put_view(AccountView)
580 |> render("relationship.json", %{user: followed, target: follower})
584 |> put_status(:forbidden)
585 |> json(%{error: message})
589 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
590 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
591 {_, true} <- {:followed, follower.id != followed.id},
592 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
594 |> put_view(AccountView)
595 |> render("relationship.json", %{user: follower, target: followed})
602 |> put_status(:forbidden)
603 |> json(%{error: message})
607 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
608 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
609 {_, true} <- {:followed, follower.id != followed.id},
610 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
612 |> put_view(AccountView)
613 |> render("account.json", %{user: followed, for: follower})
620 |> put_status(:forbidden)
621 |> json(%{error: message})
625 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
626 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
627 {_, true} <- {:followed, follower.id != followed.id},
628 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
630 |> put_view(AccountView)
631 |> render("relationship.json", %{user: follower, target: followed})
641 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
643 if Map.has_key?(params, "notifications"),
644 do: params["notifications"] in [true, "True", "true", "1"],
647 with %User{} = muted <- User.get_cached_by_id(id),
648 {:ok, muter} <- User.mute(muter, muted, notifications) do
650 |> put_view(AccountView)
651 |> render("relationship.json", %{user: muter, target: muted})
655 |> put_status(:forbidden)
656 |> json(%{error: message})
660 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
661 with %User{} = muted <- User.get_cached_by_id(id),
662 {:ok, muter} <- User.unmute(muter, muted) do
664 |> put_view(AccountView)
665 |> render("relationship.json", %{user: muter, target: muted})
669 |> put_status(:forbidden)
670 |> json(%{error: message})
674 def mutes(%{assigns: %{user: user}} = conn, _) do
675 with muted_accounts <- User.muted_users(user) do
676 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
681 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
682 with %User{} = blocked <- User.get_cached_by_id(id),
683 {:ok, blocker} <- User.block(blocker, blocked),
684 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
686 |> put_view(AccountView)
687 |> render("relationship.json", %{user: blocker, target: blocked})
691 |> put_status(:forbidden)
692 |> json(%{error: message})
696 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
697 with %User{} = blocked <- User.get_cached_by_id(id),
698 {:ok, blocker} <- User.unblock(blocker, blocked),
699 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
701 |> put_view(AccountView)
702 |> render("relationship.json", %{user: blocker, target: blocked})
706 |> put_status(:forbidden)
707 |> json(%{error: message})
711 def blocks(%{assigns: %{user: user}} = conn, _) do
712 with blocked_accounts <- User.blocked_users(user) do
713 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
718 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
719 json(conn, info.domain_blocks || [])
722 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
723 User.block_domain(blocker, domain)
727 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
728 User.unblock_domain(blocker, domain)
732 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
733 with %User{} = subscription_target <- User.get_cached_by_id(id),
734 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
736 |> put_view(AccountView)
737 |> render("relationship.json", %{user: user, target: subscription_target})
741 |> put_status(:forbidden)
742 |> json(%{error: message})
746 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
747 with %User{} = subscription_target <- User.get_cached_by_id(id),
748 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
750 |> put_view(AccountView)
751 |> render("relationship.json", %{user: user, target: subscription_target})
755 |> put_status(:forbidden)
756 |> json(%{error: message})
760 def favourites(%{assigns: %{user: user}} = conn, params) do
763 |> Map.put("type", "Create")
764 |> Map.put("favorited_by", user.ap_id)
765 |> Map.put("blocking_user", user)
768 ActivityPub.fetch_activities([], params)
772 |> add_link_headers(activities)
773 |> put_view(StatusView)
774 |> render("index.json", %{activities: activities, for: user, as: :activity})
777 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
778 with %User{} = user <- User.get_by_id(id),
779 false <- user.info.hide_favorites do
782 |> Map.put("type", "Create")
783 |> Map.put("favorited_by", user.ap_id)
784 |> Map.put("blocking_user", for_user)
788 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
790 [Pleroma.Constants.as_public()]
795 |> ActivityPub.fetch_activities(params)
799 |> add_link_headers(activities)
800 |> put_view(StatusView)
801 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
803 nil -> {:error, :not_found}
804 true -> render_error(conn, :forbidden, "Can't get favorites")
808 def bookmarks(%{assigns: %{user: user}} = conn, params) do
809 user = User.get_cached_by_id(user.id)
812 Bookmark.for_user_query(user.id)
813 |> Pagination.fetch_paginated(params)
817 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
820 |> add_link_headers(bookmarks)
821 |> put_view(StatusView)
822 |> render("index.json", %{activities: activities, for: user, as: :activity})
825 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
826 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
827 res = ListView.render("lists.json", lists: lists)
831 def index(%{assigns: %{user: user}} = conn, _params) do
832 token = get_session(conn, :oauth_token)
835 mastodon_emoji = mastodonized_emoji()
837 limit = Config.get([:instance, :limit])
840 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
845 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
848 domain: Pleroma.Web.Endpoint.host(),
851 unfollow_modal: false,
854 auto_play_gif: false,
855 display_sensitive_media: false,
856 reduce_motion: false,
857 max_toot_chars: limit,
858 mascot: User.get_mascot(user)["url"]
860 poll_limits: Config.get([:instance, :poll_limits]),
862 delete_others_notice: present?(user.info.is_moderator),
863 admin: present?(user.info.is_admin)
867 default_privacy: user.info.default_scope,
868 default_sensitive: false,
869 allow_content_types: Config.get([:instance, :allowed_post_formats])
871 media_attachments: %{
872 accept_content_types: [
888 user.info.settings ||
918 push_subscription: nil,
920 custom_emojis: mastodon_emoji,
927 |> put_view(MastodonView)
928 |> render("index.html", %{initial_state: initial_state})
931 |> put_session(:return_to, conn.request_path)
932 |> redirect(to: "/web/login")
936 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
937 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
942 |> put_status(:internal_server_error)
943 |> json(%{error: inspect(e)})
947 def login(%{assigns: %{user: %User{}}} = conn, _params) do
948 redirect(conn, to: local_mastodon_root_path(conn))
951 @doc "Local Mastodon FE login init action"
952 def login(conn, %{"code" => auth_token}) do
953 with {:ok, app} <- get_or_make_app(),
954 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
955 {:ok, token} <- Token.exchange_token(app, auth) do
957 |> put_session(:oauth_token, token.token)
958 |> redirect(to: local_mastodon_root_path(conn))
962 @doc "Local Mastodon FE callback action"
963 def login(conn, _) do
964 with {:ok, app} <- get_or_make_app() do
969 response_type: "code",
970 client_id: app.client_id,
972 scope: Enum.join(app.scopes, " ")
975 redirect(conn, to: path)
979 defp local_mastodon_root_path(conn) do
980 case get_session(conn, :return_to) do
982 mastodon_api_path(conn, :index, ["getting-started"])
985 delete_session(conn, :return_to)
990 defp get_or_make_app do
991 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
992 scopes = ["read", "write", "follow", "push"]
994 with %App{} = app <- Repo.get_by(App, find_attrs) do
996 if app.scopes == scopes do
1000 |> Changeset.change(%{scopes: scopes})
1008 App.register_changeset(
1010 Map.put(find_attrs, :scopes, scopes)
1017 def logout(conn, _) do
1020 |> redirect(to: "/")
1023 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1024 Logger.debug("Unimplemented, returning unmodified relationship")
1026 with %User{} = target <- User.get_cached_by_id(id) do
1028 |> put_view(AccountView)
1029 |> render("relationship.json", %{user: user, target: target})
1033 def empty_array(conn, _) do
1034 Logger.debug("Unimplemented, returning an empty array")
1038 def empty_object(conn, _) do
1039 Logger.debug("Unimplemented, returning an empty object")
1043 def get_filters(%{assigns: %{user: user}} = conn, _) do
1044 filters = Filter.get_filters(user)
1045 res = FilterView.render("filters.json", filters: filters)
1050 %{assigns: %{user: user}} = conn,
1051 %{"phrase" => phrase, "context" => context} = params
1057 hide: Map.get(params, "irreversible", false),
1058 whole_word: Map.get(params, "boolean", true)
1062 {:ok, response} = Filter.create(query)
1063 res = FilterView.render("filter.json", filter: response)
1067 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1068 filter = Filter.get(filter_id, user)
1069 res = FilterView.render("filter.json", filter: filter)
1074 %{assigns: %{user: user}} = conn,
1075 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1079 filter_id: filter_id,
1082 hide: Map.get(params, "irreversible", nil),
1083 whole_word: Map.get(params, "boolean", true)
1087 {:ok, response} = Filter.update(query)
1088 res = FilterView.render("filter.json", filter: response)
1092 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1095 filter_id: filter_id
1098 {:ok, _} = Filter.delete(query)
1102 def suggestions(%{assigns: %{user: user}} = conn, _) do
1103 suggestions = Config.get(:suggestions)
1105 if Keyword.get(suggestions, :enabled, false) do
1106 api = Keyword.get(suggestions, :third_party_engine, "")
1107 timeout = Keyword.get(suggestions, :timeout, 5000)
1108 limit = Keyword.get(suggestions, :limit, 23)
1110 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1112 user = user.nickname
1116 |> String.replace("{{host}}", host)
1117 |> String.replace("{{user}}", user)
1119 with {:ok, %{status: 200, body: body}} <-
1120 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1121 {:ok, data} <- Jason.decode(body) do
1124 |> Enum.slice(0, limit)
1127 |> Map.put("id", fetch_suggestion_id(x))
1128 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1129 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1135 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1142 defp fetch_suggestion_id(attrs) do
1143 case User.get_or_fetch(attrs["acct"]) do
1144 {:ok, %User{id: id}} -> id
1149 def reports(%{assigns: %{user: user}} = conn, params) do
1150 case CommonAPI.report(user, params) do
1153 |> put_view(ReportView)
1154 |> try_render("report.json", %{activity: activity})
1158 |> put_status(:bad_request)
1159 |> json(%{error: err})
1163 def account_register(
1164 %{assigns: %{app: app}} = conn,
1165 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1173 "captcha_answer_data",
1177 |> Map.put("nickname", nickname)
1178 |> Map.put("fullname", params["fullname"] || nickname)
1179 |> Map.put("bio", params["bio"] || "")
1180 |> Map.put("confirm", params["password"])
1182 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1183 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1185 token_type: "Bearer",
1186 access_token: token.token,
1188 created_at: Token.Utils.format_created_at(token)
1193 |> put_status(:bad_request)
1198 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1199 render_error(conn, :bad_request, "Missing parameters")
1202 def account_register(conn, _) do
1203 render_error(conn, :forbidden, "Invalid credentials")
1206 def conversations(%{assigns: %{user: user}} = conn, params) do
1207 participations = Participation.for_user_with_last_activity_id(user, params)
1210 Enum.map(participations, fn participation ->
1211 ConversationView.render("participation.json", %{participation: participation, for: user})
1215 |> add_link_headers(participations)
1216 |> json(conversations)
1219 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1220 with %Participation{} = participation <-
1221 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1222 {:ok, participation} <- Participation.mark_as_read(participation) do
1223 participation_view =
1224 ConversationView.render("participation.json", %{participation: participation, for: user})
1227 |> json(participation_view)
1231 def password_reset(conn, params) do
1232 nickname_or_email = params["email"] || params["nickname"]
1234 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1236 |> put_status(:no_content)
1239 {:error, "unknown user"} ->
1240 send_resp(conn, :not_found, "")
1243 send_resp(conn, :bad_request, "")
1247 def account_confirmation_resend(conn, params) do
1248 nickname_or_email = params["email"] || params["nickname"]
1250 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1251 {:ok, _} <- User.try_send_confirmation_email(user) do
1253 |> json_response(:no_content, "")
1257 def try_render(conn, target, params)
1258 when is_binary(target) do
1259 case render(conn, target, params) do
1260 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1265 def try_render(conn, _, _) do
1266 render_error(conn, :not_implemented, "Can't display this activity")
1269 defp present?(nil), do: false
1270 defp present?(false), do: false
1271 defp present?(_), do: true