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
18 alias Pleroma.Web.ActivityPub.ActivityPub
19 alias Pleroma.Web.CommonAPI
20 alias Pleroma.Web.MastodonAPI.AccountView
21 alias Pleroma.Web.MastodonAPI.AppView
22 alias Pleroma.Web.MastodonAPI.MastodonView
23 alias Pleroma.Web.MastodonAPI.StatusView
24 alias Pleroma.Web.OAuth.App
25 alias Pleroma.Web.OAuth.Authorization
26 alias Pleroma.Web.OAuth.Scopes
27 alias Pleroma.Web.OAuth.Token
28 alias Pleroma.Web.TwitterAPI.TwitterAPI
32 plug(RateLimiter, :password_reset when action == :password_reset)
34 @local_mastodon_name "Mastodon-Local"
36 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
38 def create_app(conn, params) do
39 scopes = Scopes.fetch_scopes(params, ["read"])
43 |> Map.drop(["scope", "scopes"])
44 |> Map.put("scopes", scopes)
46 with cs <- App.register_changeset(%App{}, app_attrs),
47 false <- cs.changes[:client_name] == @local_mastodon_name,
48 {:ok, app} <- Repo.insert(cs) do
51 |> render("show.json", %{app: app})
55 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
56 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
59 |> render("short.json", %{app: app})
63 @mastodon_api_level "2.7.2"
65 def masto_instance(conn, _params) do
66 instance = Config.get(:instance)
70 title: Keyword.get(instance, :name),
71 description: Keyword.get(instance, :description),
72 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
73 email: Keyword.get(instance, :email),
75 streaming_api: Pleroma.Web.Endpoint.websocket_url()
77 stats: Stats.get_stats(),
78 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
80 registrations: Pleroma.Config.get([:instance, :registrations_open]),
81 # Extra (not present in Mastodon):
82 max_toot_chars: Keyword.get(instance, :limit),
83 poll_limits: Keyword.get(instance, :poll_limits)
89 def peers(conn, _params) do
90 json(conn, Stats.get_peers())
93 defp mastodonized_emoji do
94 Pleroma.Emoji.get_all()
95 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
96 url = to_string(URI.merge(Web.base_url(), relative_url))
99 "shortcode" => shortcode,
101 "visible_in_picker" => true,
104 # Assuming that a comma is authorized in the category name
105 "category" => (tags -- ["Custom"]) |> Enum.join(",")
110 def custom_emojis(conn, _params) do
111 mastodon_emoji = mastodonized_emoji()
112 json(conn, mastodon_emoji)
115 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
116 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
117 {_, true} <- {:followed, follower.id != followed.id},
118 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
120 |> put_view(AccountView)
121 |> render("show.json", %{user: followed, for: follower})
128 |> put_status(:forbidden)
129 |> json(%{error: message})
133 def mutes(%{assigns: %{user: user}} = conn, _) do
134 with muted_accounts <- User.muted_users(user) do
135 res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
140 def blocks(%{assigns: %{user: user}} = conn, _) do
141 with blocked_accounts <- User.blocked_users(user) do
142 res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
147 def favourites(%{assigns: %{user: user}} = conn, params) do
150 |> Map.put("type", "Create")
151 |> Map.put("favorited_by", user.ap_id)
152 |> Map.put("blocking_user", user)
155 ActivityPub.fetch_activities([], params)
159 |> add_link_headers(activities)
160 |> put_view(StatusView)
161 |> render("index.json", %{activities: activities, for: user, as: :activity})
164 def bookmarks(%{assigns: %{user: user}} = conn, params) do
165 user = User.get_cached_by_id(user.id)
168 Bookmark.for_user_query(user.id)
169 |> Pagination.fetch_paginated(params)
173 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
176 |> add_link_headers(bookmarks)
177 |> put_view(StatusView)
178 |> render("index.json", %{activities: activities, for: user, as: :activity})
181 def index(%{assigns: %{user: user}} = conn, _params) do
182 token = get_session(conn, :oauth_token)
185 mastodon_emoji = mastodonized_emoji()
187 limit = Config.get([:instance, :limit])
189 accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
194 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
197 domain: Pleroma.Web.Endpoint.host(),
200 unfollow_modal: false,
203 auto_play_gif: false,
204 display_sensitive_media: false,
205 reduce_motion: false,
206 max_toot_chars: limit,
207 mascot: User.get_mascot(user)["url"]
209 poll_limits: Config.get([:instance, :poll_limits]),
211 delete_others_notice: present?(user.info.is_moderator),
212 admin: present?(user.info.is_admin)
216 default_privacy: user.info.default_scope,
217 default_sensitive: false,
218 allow_content_types: Config.get([:instance, :allowed_post_formats])
220 media_attachments: %{
221 accept_content_types: [
237 user.info.settings ||
267 push_subscription: nil,
269 custom_emojis: mastodon_emoji,
276 |> put_view(MastodonView)
277 |> render("index.html", %{initial_state: initial_state})
280 |> put_session(:return_to, conn.request_path)
281 |> redirect(to: "/web/login")
285 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
286 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
291 |> put_status(:internal_server_error)
292 |> json(%{error: inspect(e)})
296 def login(%{assigns: %{user: %User{}}} = conn, _params) do
297 redirect(conn, to: local_mastodon_root_path(conn))
300 @doc "Local Mastodon FE login init action"
301 def login(conn, %{"code" => auth_token}) do
302 with {:ok, app} <- get_or_make_app(),
303 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
304 {:ok, token} <- Token.exchange_token(app, auth) do
306 |> put_session(:oauth_token, token.token)
307 |> redirect(to: local_mastodon_root_path(conn))
311 @doc "Local Mastodon FE callback action"
312 def login(conn, _) do
313 with {:ok, app} <- get_or_make_app() do
315 o_auth_path(conn, :authorize,
316 response_type: "code",
317 client_id: app.client_id,
319 scope: Enum.join(app.scopes, " ")
322 redirect(conn, to: path)
326 defp local_mastodon_root_path(conn) do
327 case get_session(conn, :return_to) do
329 mastodon_api_path(conn, :index, ["getting-started"])
332 delete_session(conn, :return_to)
337 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
338 defp get_or_make_app do
340 %{client_name: @local_mastodon_name, redirect_uris: "."},
341 ["read", "write", "follow", "push"]
345 def logout(conn, _) do
351 # Stubs for unimplemented mastodon api
353 def empty_array(conn, _) do
354 Logger.debug("Unimplemented, returning an empty array")
358 def empty_object(conn, _) do
359 Logger.debug("Unimplemented, returning an empty object")
363 def password_reset(conn, params) do
364 nickname_or_email = params["email"] || params["nickname"]
366 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
368 |> put_status(:no_content)
371 {:error, "unknown user"} ->
372 send_resp(conn, :not_found, "")
375 send_resp(conn, :bad_request, "")
379 defp present?(nil), do: false
380 defp present?(false), do: false
381 defp present?(_), do: true