Merge branch 'split-masto-api/suggestions' into 'develop'
[akkoma] / lib / pleroma / web / mastodon_api / controllers / mastodon_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
7
8 import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
9
10 alias Pleroma.Bookmark
11 alias Pleroma.Config
12 alias Pleroma.Pagination
13 alias Pleroma.Plugs.RateLimiter
14 alias Pleroma.Repo
15 alias Pleroma.Stats
16 alias Pleroma.User
17 alias Pleroma.Web
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
29
30 require Logger
31
32 plug(RateLimiter, :password_reset when action == :password_reset)
33
34 @local_mastodon_name "Mastodon-Local"
35
36 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
37
38 def create_app(conn, params) do
39 scopes = Scopes.fetch_scopes(params, ["read"])
40
41 app_attrs =
42 params
43 |> Map.drop(["scope", "scopes"])
44 |> Map.put("scopes", scopes)
45
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
49 conn
50 |> put_view(AppView)
51 |> render("show.json", %{app: app})
52 end
53 end
54
55 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
56 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
57 conn
58 |> put_view(AppView)
59 |> render("short.json", %{app: app})
60 end
61 end
62
63 @mastodon_api_level "2.7.2"
64
65 def masto_instance(conn, _params) do
66 instance = Config.get(:instance)
67
68 response = %{
69 uri: Web.base_url(),
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),
74 urls: %{
75 streaming_api: Pleroma.Web.Endpoint.websocket_url()
76 },
77 stats: Stats.get_stats(),
78 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
79 languages: ["en"],
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)
84 }
85
86 json(conn, response)
87 end
88
89 def peers(conn, _params) do
90 json(conn, Stats.get_peers())
91 end
92
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))
97
98 %{
99 "shortcode" => shortcode,
100 "static_url" => url,
101 "visible_in_picker" => true,
102 "url" => url,
103 "tags" => tags,
104 # Assuming that a comma is authorized in the category name
105 "category" => (tags -- ["Custom"]) |> Enum.join(",")
106 }
107 end)
108 end
109
110 def custom_emojis(conn, _params) do
111 mastodon_emoji = mastodonized_emoji()
112 json(conn, mastodon_emoji)
113 end
114
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
119 conn
120 |> put_view(AccountView)
121 |> render("show.json", %{user: followed, for: follower})
122 else
123 {:followed, _} ->
124 {:error, :not_found}
125
126 {:error, message} ->
127 conn
128 |> put_status(:forbidden)
129 |> json(%{error: message})
130 end
131 end
132
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)
136 json(conn, res)
137 end
138 end
139
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)
143 json(conn, res)
144 end
145 end
146
147 def favourites(%{assigns: %{user: user}} = conn, params) do
148 params =
149 params
150 |> Map.put("type", "Create")
151 |> Map.put("favorited_by", user.ap_id)
152 |> Map.put("blocking_user", user)
153
154 activities =
155 ActivityPub.fetch_activities([], params)
156 |> Enum.reverse()
157
158 conn
159 |> add_link_headers(activities)
160 |> put_view(StatusView)
161 |> render("index.json", %{activities: activities, for: user, as: :activity})
162 end
163
164 def bookmarks(%{assigns: %{user: user}} = conn, params) do
165 user = User.get_cached_by_id(user.id)
166
167 bookmarks =
168 Bookmark.for_user_query(user.id)
169 |> Pagination.fetch_paginated(params)
170
171 activities =
172 bookmarks
173 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
174
175 conn
176 |> add_link_headers(bookmarks)
177 |> put_view(StatusView)
178 |> render("index.json", %{activities: activities, for: user, as: :activity})
179 end
180
181 def index(%{assigns: %{user: user}} = conn, _params) do
182 token = get_session(conn, :oauth_token)
183
184 if user && token do
185 mastodon_emoji = mastodonized_emoji()
186
187 limit = Config.get([:instance, :limit])
188
189 accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
190
191 initial_state =
192 %{
193 meta: %{
194 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
195 access_token: token,
196 locale: "en",
197 domain: Pleroma.Web.Endpoint.host(),
198 admin: "1",
199 me: "#{user.id}",
200 unfollow_modal: false,
201 boost_modal: false,
202 delete_modal: true,
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"]
208 },
209 poll_limits: Config.get([:instance, :poll_limits]),
210 rights: %{
211 delete_others_notice: present?(user.info.is_moderator),
212 admin: present?(user.info.is_admin)
213 },
214 compose: %{
215 me: "#{user.id}",
216 default_privacy: user.info.default_scope,
217 default_sensitive: false,
218 allow_content_types: Config.get([:instance, :allowed_post_formats])
219 },
220 media_attachments: %{
221 accept_content_types: [
222 ".jpg",
223 ".jpeg",
224 ".png",
225 ".gif",
226 ".webm",
227 ".mp4",
228 ".m4v",
229 "image\/jpeg",
230 "image\/png",
231 "image\/gif",
232 "video\/webm",
233 "video\/mp4"
234 ]
235 },
236 settings:
237 user.info.settings ||
238 %{
239 onboarded: true,
240 home: %{
241 shows: %{
242 reblog: true,
243 reply: true
244 }
245 },
246 notifications: %{
247 alerts: %{
248 follow: true,
249 favourite: true,
250 reblog: true,
251 mention: true
252 },
253 shows: %{
254 follow: true,
255 favourite: true,
256 reblog: true,
257 mention: true
258 },
259 sounds: %{
260 follow: true,
261 favourite: true,
262 reblog: true,
263 mention: true
264 }
265 }
266 },
267 push_subscription: nil,
268 accounts: accounts,
269 custom_emojis: mastodon_emoji,
270 char_limit: limit
271 }
272 |> Jason.encode!()
273
274 conn
275 |> put_layout(false)
276 |> put_view(MastodonView)
277 |> render("index.html", %{initial_state: initial_state})
278 else
279 conn
280 |> put_session(:return_to, conn.request_path)
281 |> redirect(to: "/web/login")
282 end
283 end
284
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
287 json(conn, %{})
288 else
289 e ->
290 conn
291 |> put_status(:internal_server_error)
292 |> json(%{error: inspect(e)})
293 end
294 end
295
296 def login(%{assigns: %{user: %User{}}} = conn, _params) do
297 redirect(conn, to: local_mastodon_root_path(conn))
298 end
299
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
305 conn
306 |> put_session(:oauth_token, token.token)
307 |> redirect(to: local_mastodon_root_path(conn))
308 end
309 end
310
311 @doc "Local Mastodon FE callback action"
312 def login(conn, _) do
313 with {:ok, app} <- get_or_make_app() do
314 path =
315 o_auth_path(conn, :authorize,
316 response_type: "code",
317 client_id: app.client_id,
318 redirect_uri: ".",
319 scope: Enum.join(app.scopes, " ")
320 )
321
322 redirect(conn, to: path)
323 end
324 end
325
326 defp local_mastodon_root_path(conn) do
327 case get_session(conn, :return_to) do
328 nil ->
329 mastodon_api_path(conn, :index, ["getting-started"])
330
331 return_to ->
332 delete_session(conn, :return_to)
333 return_to
334 end
335 end
336
337 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
338 defp get_or_make_app do
339 App.get_or_make(
340 %{client_name: @local_mastodon_name, redirect_uris: "."},
341 ["read", "write", "follow", "push"]
342 )
343 end
344
345 def logout(conn, _) do
346 conn
347 |> clear_session
348 |> redirect(to: "/")
349 end
350
351 # Stubs for unimplemented mastodon api
352 #
353 def empty_array(conn, _) do
354 Logger.debug("Unimplemented, returning an empty array")
355 json(conn, [])
356 end
357
358 def empty_object(conn, _) do
359 Logger.debug("Unimplemented, returning an empty object")
360 json(conn, %{})
361 end
362
363 def password_reset(conn, params) do
364 nickname_or_email = params["email"] || params["nickname"]
365
366 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
367 conn
368 |> put_status(:no_content)
369 |> json("")
370 else
371 {:error, "unknown user"} ->
372 send_resp(conn, :not_found, "")
373
374 {:error, _} ->
375 send_resp(conn, :bad_request, "")
376 end
377 end
378
379 defp present?(nil), do: false
380 defp present?(false), do: false
381 defp present?(_), do: true
382 end