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, only: [add_link_headers: 2]
10 alias Pleroma.Bookmark
12 alias Pleroma.Pagination
13 alias Pleroma.Plugs.RateLimiter
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.CommonAPI
19 alias Pleroma.Web.MastodonAPI.AccountView
20 alias Pleroma.Web.MastodonAPI.MastodonView
21 alias Pleroma.Web.MastodonAPI.StatusView
22 alias Pleroma.Web.OAuth.App
23 alias Pleroma.Web.OAuth.Authorization
24 alias Pleroma.Web.OAuth.Token
25 alias Pleroma.Web.TwitterAPI.TwitterAPI
29 plug(RateLimiter, :password_reset when action == :password_reset)
31 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
33 @local_mastodon_name "Mastodon-Local"
34 @mastodon_api_level "2.7.2"
36 def masto_instance(conn, _params) do
37 instance = Config.get(:instance)
41 title: Keyword.get(instance, :name),
42 description: Keyword.get(instance, :description),
43 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
44 email: Keyword.get(instance, :email),
46 streaming_api: Pleroma.Web.Endpoint.websocket_url()
48 stats: Stats.get_stats(),
49 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
51 registrations: Pleroma.Config.get([:instance, :registrations_open]),
52 # Extra (not present in Mastodon):
53 max_toot_chars: Keyword.get(instance, :limit),
54 poll_limits: Keyword.get(instance, :poll_limits),
55 upload_limit: Keyword.get(instance, :upload_limit),
56 avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
57 background_upload_limit: Keyword.get(instance, :background_upload_limit),
58 banner_upload_limit: Keyword.get(instance, :banner_upload_limit)
64 def peers(conn, _params) do
65 json(conn, Stats.get_peers())
68 defp mastodonized_emoji do
69 Pleroma.Emoji.get_all()
70 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
71 url = to_string(URI.merge(Web.base_url(), relative_url))
74 "shortcode" => shortcode,
76 "visible_in_picker" => true,
79 # Assuming that a comma is authorized in the category name
80 "category" => (tags -- ["Custom"]) |> Enum.join(",")
85 def custom_emojis(conn, _params) do
86 mastodon_emoji = mastodonized_emoji()
87 json(conn, mastodon_emoji)
90 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
91 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
92 {_, true} <- {:followed, follower.id != followed.id},
93 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
95 |> put_view(AccountView)
96 |> render("show.json", %{user: followed, for: follower})
103 |> put_status(:forbidden)
104 |> json(%{error: message})
108 def mutes(%{assigns: %{user: user}} = conn, _) do
109 with muted_accounts <- User.muted_users(user) do
110 res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
115 def blocks(%{assigns: %{user: user}} = conn, _) do
116 with blocked_accounts <- User.blocked_users(user) do
117 res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
122 def favourites(%{assigns: %{user: user}} = conn, params) do
125 |> Map.put("type", "Create")
126 |> Map.put("favorited_by", user.ap_id)
127 |> Map.put("blocking_user", user)
130 ActivityPub.fetch_activities([], params)
134 |> add_link_headers(activities)
135 |> put_view(StatusView)
136 |> render("index.json", %{activities: activities, for: user, as: :activity})
139 def bookmarks(%{assigns: %{user: user}} = conn, params) do
140 user = User.get_cached_by_id(user.id)
143 Bookmark.for_user_query(user.id)
144 |> Pagination.fetch_paginated(params)
148 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
151 |> add_link_headers(bookmarks)
152 |> put_view(StatusView)
153 |> render("index.json", %{activities: activities, for: user, as: :activity})
156 def index(%{assigns: %{user: user}} = conn, _params) do
157 token = get_session(conn, :oauth_token)
160 mastodon_emoji = mastodonized_emoji()
162 limit = Config.get([:instance, :limit])
164 accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
169 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
172 domain: Pleroma.Web.Endpoint.host(),
175 unfollow_modal: false,
178 auto_play_gif: false,
179 display_sensitive_media: false,
180 reduce_motion: false,
181 max_toot_chars: limit,
182 mascot: User.get_mascot(user)["url"]
184 poll_limits: Config.get([:instance, :poll_limits]),
186 delete_others_notice: present?(user.info.is_moderator),
187 admin: present?(user.info.is_admin)
191 default_privacy: user.info.default_scope,
192 default_sensitive: false,
193 allow_content_types: Config.get([:instance, :allowed_post_formats])
195 media_attachments: %{
196 accept_content_types: [
212 user.info.settings ||
242 push_subscription: nil,
244 custom_emojis: mastodon_emoji,
251 |> put_view(MastodonView)
252 |> render("index.html", %{initial_state: initial_state})
255 |> put_session(:return_to, conn.request_path)
256 |> redirect(to: "/web/login")
260 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
261 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
266 |> put_status(:internal_server_error)
267 |> json(%{error: inspect(e)})
271 def login(%{assigns: %{user: %User{}}} = conn, _params) do
272 redirect(conn, to: local_mastodon_root_path(conn))
275 @doc "Local Mastodon FE login init action"
276 def login(conn, %{"code" => auth_token}) do
277 with {:ok, app} <- get_or_make_app(),
278 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
279 {:ok, token} <- Token.exchange_token(app, auth) do
281 |> put_session(:oauth_token, token.token)
282 |> redirect(to: local_mastodon_root_path(conn))
286 @doc "Local Mastodon FE callback action"
287 def login(conn, _) do
288 with {:ok, app} <- get_or_make_app() do
290 o_auth_path(conn, :authorize,
291 response_type: "code",
292 client_id: app.client_id,
294 scope: Enum.join(app.scopes, " ")
297 redirect(conn, to: path)
301 defp local_mastodon_root_path(conn) do
302 case get_session(conn, :return_to) do
304 mastodon_api_path(conn, :index, ["getting-started"])
307 delete_session(conn, :return_to)
312 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
313 defp get_or_make_app do
315 %{client_name: @local_mastodon_name, redirect_uris: "."},
316 ["read", "write", "follow", "push"]
320 def logout(conn, _) do
326 # Stubs for unimplemented mastodon api
328 def empty_array(conn, _) do
329 Logger.debug("Unimplemented, returning an empty array")
333 def empty_object(conn, _) do
334 Logger.debug("Unimplemented, returning an empty object")
338 def password_reset(conn, params) do
339 nickname_or_email = params["email"] || params["nickname"]
341 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
343 |> put_status(:no_content)
346 {:error, "unknown user"} ->
347 send_resp(conn, :not_found, "")
350 send_resp(conn, :bad_request, "")
354 defp present?(nil), do: false
355 defp present?(false), do: false
356 defp present?(_), do: true