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)
60 def peers(conn, _params) do
61 json(conn, Stats.get_peers())
64 defp mastodonized_emoji do
65 Pleroma.Emoji.get_all()
66 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
67 url = to_string(URI.merge(Web.base_url(), relative_url))
70 "shortcode" => shortcode,
72 "visible_in_picker" => true,
75 # Assuming that a comma is authorized in the category name
76 "category" => (tags -- ["Custom"]) |> Enum.join(",")
81 def custom_emojis(conn, _params) do
82 mastodon_emoji = mastodonized_emoji()
83 json(conn, mastodon_emoji)
86 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
87 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
88 {_, true} <- {:followed, follower.id != followed.id},
89 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
91 |> put_view(AccountView)
92 |> render("show.json", %{user: followed, for: follower})
99 |> put_status(:forbidden)
100 |> json(%{error: message})
104 def mutes(%{assigns: %{user: user}} = conn, _) do
105 with muted_accounts <- User.muted_users(user) do
106 res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
111 def blocks(%{assigns: %{user: user}} = conn, _) do
112 with blocked_accounts <- User.blocked_users(user) do
113 res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
118 def favourites(%{assigns: %{user: user}} = conn, params) do
121 |> Map.put("type", "Create")
122 |> Map.put("favorited_by", user.ap_id)
123 |> Map.put("blocking_user", user)
126 ActivityPub.fetch_activities([], params)
130 |> add_link_headers(activities)
131 |> put_view(StatusView)
132 |> render("index.json", %{activities: activities, for: user, as: :activity})
135 def bookmarks(%{assigns: %{user: user}} = conn, params) do
136 user = User.get_cached_by_id(user.id)
139 Bookmark.for_user_query(user.id)
140 |> Pagination.fetch_paginated(params)
144 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
147 |> add_link_headers(bookmarks)
148 |> put_view(StatusView)
149 |> render("index.json", %{activities: activities, for: user, as: :activity})
152 def index(%{assigns: %{user: user}} = conn, _params) do
153 token = get_session(conn, :oauth_token)
156 mastodon_emoji = mastodonized_emoji()
158 limit = Config.get([:instance, :limit])
160 accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
165 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
168 domain: Pleroma.Web.Endpoint.host(),
171 unfollow_modal: false,
174 auto_play_gif: false,
175 display_sensitive_media: false,
176 reduce_motion: false,
177 max_toot_chars: limit,
178 mascot: User.get_mascot(user)["url"]
180 poll_limits: Config.get([:instance, :poll_limits]),
182 delete_others_notice: present?(user.info.is_moderator),
183 admin: present?(user.info.is_admin)
187 default_privacy: user.info.default_scope,
188 default_sensitive: false,
189 allow_content_types: Config.get([:instance, :allowed_post_formats])
191 media_attachments: %{
192 accept_content_types: [
208 user.info.settings ||
238 push_subscription: nil,
240 custom_emojis: mastodon_emoji,
247 |> put_view(MastodonView)
248 |> render("index.html", %{initial_state: initial_state})
251 |> put_session(:return_to, conn.request_path)
252 |> redirect(to: "/web/login")
256 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
257 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
262 |> put_status(:internal_server_error)
263 |> json(%{error: inspect(e)})
267 def login(%{assigns: %{user: %User{}}} = conn, _params) do
268 redirect(conn, to: local_mastodon_root_path(conn))
271 @doc "Local Mastodon FE login init action"
272 def login(conn, %{"code" => auth_token}) do
273 with {:ok, app} <- get_or_make_app(),
274 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
275 {:ok, token} <- Token.exchange_token(app, auth) do
277 |> put_session(:oauth_token, token.token)
278 |> redirect(to: local_mastodon_root_path(conn))
282 @doc "Local Mastodon FE callback action"
283 def login(conn, _) do
284 with {:ok, app} <- get_or_make_app() do
286 o_auth_path(conn, :authorize,
287 response_type: "code",
288 client_id: app.client_id,
290 scope: Enum.join(app.scopes, " ")
293 redirect(conn, to: path)
297 defp local_mastodon_root_path(conn) do
298 case get_session(conn, :return_to) do
300 mastodon_api_path(conn, :index, ["getting-started"])
303 delete_session(conn, :return_to)
308 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
309 defp get_or_make_app do
311 %{client_name: @local_mastodon_name, redirect_uris: "."},
312 ["read", "write", "follow", "push"]
316 def logout(conn, _) do
322 # Stubs for unimplemented mastodon api
324 def empty_array(conn, _) do
325 Logger.debug("Unimplemented, returning an empty array")
329 def empty_object(conn, _) do
330 Logger.debug("Unimplemented, returning an empty object")
334 def password_reset(conn, params) do
335 nickname_or_email = params["email"] || params["nickname"]
337 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
339 |> put_status(:no_content)
342 {:error, "unknown user"} ->
343 send_resp(conn, :not_found, "")
346 send_resp(conn, :bad_request, "")
350 defp present?(nil), do: false
351 defp present?(false), do: false
352 defp present?(_), do: true