Extract apps actions from `MastodonAPIController` to `AppController`
[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.Stats
15 alias Pleroma.User
16 alias Pleroma.Web
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
26
27 require Logger
28
29 plug(RateLimiter, :password_reset when action == :password_reset)
30
31 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
32
33 @local_mastodon_name "Mastodon-Local"
34 @mastodon_api_level "2.7.2"
35
36 def masto_instance(conn, _params) do
37 instance = Config.get(:instance)
38
39 response = %{
40 uri: Web.base_url(),
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),
45 urls: %{
46 streaming_api: Pleroma.Web.Endpoint.websocket_url()
47 },
48 stats: Stats.get_stats(),
49 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
50 languages: ["en"],
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 }
56
57 json(conn, response)
58 end
59
60 def peers(conn, _params) do
61 json(conn, Stats.get_peers())
62 end
63
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))
68
69 %{
70 "shortcode" => shortcode,
71 "static_url" => url,
72 "visible_in_picker" => true,
73 "url" => url,
74 "tags" => tags,
75 # Assuming that a comma is authorized in the category name
76 "category" => (tags -- ["Custom"]) |> Enum.join(",")
77 }
78 end)
79 end
80
81 def custom_emojis(conn, _params) do
82 mastodon_emoji = mastodonized_emoji()
83 json(conn, mastodon_emoji)
84 end
85
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
90 conn
91 |> put_view(AccountView)
92 |> render("show.json", %{user: followed, for: follower})
93 else
94 {:followed, _} ->
95 {:error, :not_found}
96
97 {:error, message} ->
98 conn
99 |> put_status(:forbidden)
100 |> json(%{error: message})
101 end
102 end
103
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)
107 json(conn, res)
108 end
109 end
110
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)
114 json(conn, res)
115 end
116 end
117
118 def favourites(%{assigns: %{user: user}} = conn, params) do
119 params =
120 params
121 |> Map.put("type", "Create")
122 |> Map.put("favorited_by", user.ap_id)
123 |> Map.put("blocking_user", user)
124
125 activities =
126 ActivityPub.fetch_activities([], params)
127 |> Enum.reverse()
128
129 conn
130 |> add_link_headers(activities)
131 |> put_view(StatusView)
132 |> render("index.json", %{activities: activities, for: user, as: :activity})
133 end
134
135 def bookmarks(%{assigns: %{user: user}} = conn, params) do
136 user = User.get_cached_by_id(user.id)
137
138 bookmarks =
139 Bookmark.for_user_query(user.id)
140 |> Pagination.fetch_paginated(params)
141
142 activities =
143 bookmarks
144 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
145
146 conn
147 |> add_link_headers(bookmarks)
148 |> put_view(StatusView)
149 |> render("index.json", %{activities: activities, for: user, as: :activity})
150 end
151
152 def index(%{assigns: %{user: user}} = conn, _params) do
153 token = get_session(conn, :oauth_token)
154
155 if user && token do
156 mastodon_emoji = mastodonized_emoji()
157
158 limit = Config.get([:instance, :limit])
159
160 accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
161
162 initial_state =
163 %{
164 meta: %{
165 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
166 access_token: token,
167 locale: "en",
168 domain: Pleroma.Web.Endpoint.host(),
169 admin: "1",
170 me: "#{user.id}",
171 unfollow_modal: false,
172 boost_modal: false,
173 delete_modal: true,
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"]
179 },
180 poll_limits: Config.get([:instance, :poll_limits]),
181 rights: %{
182 delete_others_notice: present?(user.info.is_moderator),
183 admin: present?(user.info.is_admin)
184 },
185 compose: %{
186 me: "#{user.id}",
187 default_privacy: user.info.default_scope,
188 default_sensitive: false,
189 allow_content_types: Config.get([:instance, :allowed_post_formats])
190 },
191 media_attachments: %{
192 accept_content_types: [
193 ".jpg",
194 ".jpeg",
195 ".png",
196 ".gif",
197 ".webm",
198 ".mp4",
199 ".m4v",
200 "image\/jpeg",
201 "image\/png",
202 "image\/gif",
203 "video\/webm",
204 "video\/mp4"
205 ]
206 },
207 settings:
208 user.info.settings ||
209 %{
210 onboarded: true,
211 home: %{
212 shows: %{
213 reblog: true,
214 reply: true
215 }
216 },
217 notifications: %{
218 alerts: %{
219 follow: true,
220 favourite: true,
221 reblog: true,
222 mention: true
223 },
224 shows: %{
225 follow: true,
226 favourite: true,
227 reblog: true,
228 mention: true
229 },
230 sounds: %{
231 follow: true,
232 favourite: true,
233 reblog: true,
234 mention: true
235 }
236 }
237 },
238 push_subscription: nil,
239 accounts: accounts,
240 custom_emojis: mastodon_emoji,
241 char_limit: limit
242 }
243 |> Jason.encode!()
244
245 conn
246 |> put_layout(false)
247 |> put_view(MastodonView)
248 |> render("index.html", %{initial_state: initial_state})
249 else
250 conn
251 |> put_session(:return_to, conn.request_path)
252 |> redirect(to: "/web/login")
253 end
254 end
255
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
258 json(conn, %{})
259 else
260 e ->
261 conn
262 |> put_status(:internal_server_error)
263 |> json(%{error: inspect(e)})
264 end
265 end
266
267 def login(%{assigns: %{user: %User{}}} = conn, _params) do
268 redirect(conn, to: local_mastodon_root_path(conn))
269 end
270
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
276 conn
277 |> put_session(:oauth_token, token.token)
278 |> redirect(to: local_mastodon_root_path(conn))
279 end
280 end
281
282 @doc "Local Mastodon FE callback action"
283 def login(conn, _) do
284 with {:ok, app} <- get_or_make_app() do
285 path =
286 o_auth_path(conn, :authorize,
287 response_type: "code",
288 client_id: app.client_id,
289 redirect_uri: ".",
290 scope: Enum.join(app.scopes, " ")
291 )
292
293 redirect(conn, to: path)
294 end
295 end
296
297 defp local_mastodon_root_path(conn) do
298 case get_session(conn, :return_to) do
299 nil ->
300 mastodon_api_path(conn, :index, ["getting-started"])
301
302 return_to ->
303 delete_session(conn, :return_to)
304 return_to
305 end
306 end
307
308 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
309 defp get_or_make_app do
310 App.get_or_make(
311 %{client_name: @local_mastodon_name, redirect_uris: "."},
312 ["read", "write", "follow", "push"]
313 )
314 end
315
316 def logout(conn, _) do
317 conn
318 |> clear_session
319 |> redirect(to: "/")
320 end
321
322 # Stubs for unimplemented mastodon api
323 #
324 def empty_array(conn, _) do
325 Logger.debug("Unimplemented, returning an empty array")
326 json(conn, [])
327 end
328
329 def empty_object(conn, _) do
330 Logger.debug("Unimplemented, returning an empty object")
331 json(conn, %{})
332 end
333
334 def password_reset(conn, params) do
335 nickname_or_email = params["email"] || params["nickname"]
336
337 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
338 conn
339 |> put_status(:no_content)
340 |> json("")
341 else
342 {:error, "unknown user"} ->
343 send_resp(conn, :not_found, "")
344
345 {:error, _} ->
346 send_resp(conn, :bad_request, "")
347 end
348 end
349
350 defp present?(nil), do: false
351 defp present?(false), do: false
352 defp present?(_), do: true
353 end