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
13 alias Pleroma.Pagination
14 alias Pleroma.Plugs.RateLimiter
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.AppView
23 alias Pleroma.Web.MastodonAPI.MastodonView
24 alias Pleroma.Web.MastodonAPI.StatusView
25 alias Pleroma.Web.MediaProxy
26 alias Pleroma.Web.OAuth.App
27 alias Pleroma.Web.OAuth.Authorization
28 alias Pleroma.Web.OAuth.Scopes
29 alias Pleroma.Web.OAuth.Token
30 alias Pleroma.Web.TwitterAPI.TwitterAPI
34 plug(RateLimiter, :password_reset when action == :password_reset)
36 @local_mastodon_name "Mastodon-Local"
38 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
40 def create_app(conn, params) do
41 scopes = Scopes.fetch_scopes(params, ["read"])
45 |> Map.drop(["scope", "scopes"])
46 |> Map.put("scopes", scopes)
48 with cs <- App.register_changeset(%App{}, app_attrs),
49 false <- cs.changes[:client_name] == @local_mastodon_name,
50 {:ok, app} <- Repo.insert(cs) do
53 |> render("show.json", %{app: app})
57 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
58 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
61 |> render("short.json", %{app: app})
65 @mastodon_api_level "2.7.2"
67 def masto_instance(conn, _params) do
68 instance = Config.get(:instance)
72 title: Keyword.get(instance, :name),
73 description: Keyword.get(instance, :description),
74 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
75 email: Keyword.get(instance, :email),
77 streaming_api: Pleroma.Web.Endpoint.websocket_url()
79 stats: Stats.get_stats(),
80 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
82 registrations: Pleroma.Config.get([:instance, :registrations_open]),
83 # Extra (not present in Mastodon):
84 max_toot_chars: Keyword.get(instance, :limit),
85 poll_limits: Keyword.get(instance, :poll_limits)
91 def peers(conn, _params) do
92 json(conn, Stats.get_peers())
95 defp mastodonized_emoji do
96 Pleroma.Emoji.get_all()
97 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
98 url = to_string(URI.merge(Web.base_url(), relative_url))
101 "shortcode" => shortcode,
103 "visible_in_picker" => true,
106 # Assuming that a comma is authorized in the category name
107 "category" => (tags -- ["Custom"]) |> Enum.join(",")
112 def custom_emojis(conn, _params) do
113 mastodon_emoji = mastodonized_emoji()
114 json(conn, mastodon_emoji)
117 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
118 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
119 {_, true} <- {:followed, follower.id != followed.id},
120 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
122 |> put_view(AccountView)
123 |> render("show.json", %{user: followed, for: follower})
130 |> put_status(:forbidden)
131 |> json(%{error: message})
135 def mutes(%{assigns: %{user: user}} = conn, _) do
136 with muted_accounts <- User.muted_users(user) do
137 res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
142 def blocks(%{assigns: %{user: user}} = conn, _) do
143 with blocked_accounts <- User.blocked_users(user) do
144 res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
149 def favourites(%{assigns: %{user: user}} = conn, params) do
152 |> Map.put("type", "Create")
153 |> Map.put("favorited_by", user.ap_id)
154 |> Map.put("blocking_user", user)
157 ActivityPub.fetch_activities([], params)
161 |> add_link_headers(activities)
162 |> put_view(StatusView)
163 |> render("index.json", %{activities: activities, for: user, as: :activity})
166 def bookmarks(%{assigns: %{user: user}} = conn, params) do
167 user = User.get_cached_by_id(user.id)
170 Bookmark.for_user_query(user.id)
171 |> Pagination.fetch_paginated(params)
175 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
178 |> add_link_headers(bookmarks)
179 |> put_view(StatusView)
180 |> render("index.json", %{activities: activities, for: user, as: :activity})
183 def index(%{assigns: %{user: user}} = conn, _params) do
184 token = get_session(conn, :oauth_token)
187 mastodon_emoji = mastodonized_emoji()
189 limit = Config.get([:instance, :limit])
191 accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
196 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
199 domain: Pleroma.Web.Endpoint.host(),
202 unfollow_modal: false,
205 auto_play_gif: false,
206 display_sensitive_media: false,
207 reduce_motion: false,
208 max_toot_chars: limit,
209 mascot: User.get_mascot(user)["url"]
211 poll_limits: Config.get([:instance, :poll_limits]),
213 delete_others_notice: present?(user.info.is_moderator),
214 admin: present?(user.info.is_admin)
218 default_privacy: user.info.default_scope,
219 default_sensitive: false,
220 allow_content_types: Config.get([:instance, :allowed_post_formats])
222 media_attachments: %{
223 accept_content_types: [
239 user.info.settings ||
269 push_subscription: nil,
271 custom_emojis: mastodon_emoji,
278 |> put_view(MastodonView)
279 |> render("index.html", %{initial_state: initial_state})
282 |> put_session(:return_to, conn.request_path)
283 |> redirect(to: "/web/login")
287 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
288 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
293 |> put_status(:internal_server_error)
294 |> json(%{error: inspect(e)})
298 def login(%{assigns: %{user: %User{}}} = conn, _params) do
299 redirect(conn, to: local_mastodon_root_path(conn))
302 @doc "Local Mastodon FE login init action"
303 def login(conn, %{"code" => auth_token}) do
304 with {:ok, app} <- get_or_make_app(),
305 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
306 {:ok, token} <- Token.exchange_token(app, auth) do
308 |> put_session(:oauth_token, token.token)
309 |> redirect(to: local_mastodon_root_path(conn))
313 @doc "Local Mastodon FE callback action"
314 def login(conn, _) do
315 with {:ok, app} <- get_or_make_app() do
317 o_auth_path(conn, :authorize,
318 response_type: "code",
319 client_id: app.client_id,
321 scope: Enum.join(app.scopes, " ")
324 redirect(conn, to: path)
328 defp local_mastodon_root_path(conn) do
329 case get_session(conn, :return_to) do
331 mastodon_api_path(conn, :index, ["getting-started"])
334 delete_session(conn, :return_to)
339 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
340 defp get_or_make_app do
342 %{client_name: @local_mastodon_name, redirect_uris: "."},
343 ["read", "write", "follow", "push"]
347 def logout(conn, _) do
353 # Stubs for unimplemented mastodon api
355 def empty_array(conn, _) do
356 Logger.debug("Unimplemented, returning an empty array")
360 def empty_object(conn, _) do
361 Logger.debug("Unimplemented, returning an empty object")
365 def suggestions(%{assigns: %{user: user}} = conn, _) do
366 suggestions = Config.get(:suggestions)
368 if Keyword.get(suggestions, :enabled, false) do
369 api = Keyword.get(suggestions, :third_party_engine, "")
370 timeout = Keyword.get(suggestions, :timeout, 5000)
371 limit = Keyword.get(suggestions, :limit, 23)
373 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
379 |> String.replace("{{host}}", host)
380 |> String.replace("{{user}}", user)
382 with {:ok, %{status: 200, body: body}} <-
383 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
384 {:ok, data} <- Jason.decode(body) do
387 |> Enum.slice(0, limit)
390 |> Map.put("id", fetch_suggestion_id(x))
391 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
392 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
398 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
405 defp fetch_suggestion_id(attrs) do
406 case User.get_or_fetch(attrs["acct"]) do
407 {:ok, %User{id: id}} -> id
412 def password_reset(conn, params) do
413 nickname_or_email = params["email"] || params["nickname"]
415 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
417 |> put_status(:no_content)
420 {:error, "unknown user"} ->
421 send_resp(conn, :not_found, "")
424 send_resp(conn, :bad_request, "")
428 defp present?(nil), do: false
429 defp present?(false), do: false
430 defp present?(_), do: true