0ee9f034ad258dae1f8cc9eae5282f8ff064258c
[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,
9 only: [json_response: 3, add_link_headers: 2, truthy_param?: 1]
10
11 alias Ecto.Changeset
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
14 alias Pleroma.Config
15 alias Pleroma.Conversation.Participation
16 alias Pleroma.Emoji
17 alias Pleroma.Filter
18 alias Pleroma.HTTP
19 alias Pleroma.Object
20 alias Pleroma.Pagination
21 alias Pleroma.Plugs.RateLimiter
22 alias Pleroma.Repo
23 alias Pleroma.ScheduledActivity
24 alias Pleroma.Stats
25 alias Pleroma.User
26 alias Pleroma.Web
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Visibility
29 alias Pleroma.Web.CommonAPI
30 alias Pleroma.Web.MastodonAPI.AccountView
31 alias Pleroma.Web.MastodonAPI.AppView
32 alias Pleroma.Web.MastodonAPI.ConversationView
33 alias Pleroma.Web.MastodonAPI.FilterView
34 alias Pleroma.Web.MastodonAPI.ListView
35 alias Pleroma.Web.MastodonAPI.MastodonAPI
36 alias Pleroma.Web.MastodonAPI.MastodonView
37 alias Pleroma.Web.MastodonAPI.ReportView
38 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
39 alias Pleroma.Web.MastodonAPI.StatusView
40 alias Pleroma.Web.MediaProxy
41 alias Pleroma.Web.OAuth.App
42 alias Pleroma.Web.OAuth.Authorization
43 alias Pleroma.Web.OAuth.Scopes
44 alias Pleroma.Web.OAuth.Token
45 alias Pleroma.Web.TwitterAPI.TwitterAPI
46
47 import Ecto.Query
48
49 require Logger
50 require Pleroma.Constants
51
52 @rate_limited_relations_actions ~w(follow unfollow)a
53
54 plug(
55 RateLimiter,
56 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
57 )
58
59 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
60 plug(RateLimiter, :app_account_creation when action == :account_register)
61 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
62 plug(RateLimiter, :password_reset when action == :password_reset)
63 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
64
65 @local_mastodon_name "Mastodon-Local"
66
67 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
68
69 def create_app(conn, params) do
70 scopes = Scopes.fetch_scopes(params, ["read"])
71
72 app_attrs =
73 params
74 |> Map.drop(["scope", "scopes"])
75 |> Map.put("scopes", scopes)
76
77 with cs <- App.register_changeset(%App{}, app_attrs),
78 false <- cs.changes[:client_name] == @local_mastodon_name,
79 {:ok, app} <- Repo.insert(cs) do
80 conn
81 |> put_view(AppView)
82 |> render("show.json", %{app: app})
83 end
84 end
85
86 defp add_if_present(
87 map,
88 params,
89 params_field,
90 map_field,
91 value_function \\ fn x -> {:ok, x} end
92 ) do
93 if Map.has_key?(params, params_field) do
94 case value_function.(params[params_field]) do
95 {:ok, new_value} -> Map.put(map, map_field, new_value)
96 :error -> map
97 end
98 else
99 map
100 end
101 end
102
103 def update_credentials(%{assigns: %{user: user}} = conn, params) do
104 original_user = user
105
106 user_params =
107 %{}
108 |> add_if_present(params, "display_name", :name)
109 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
110 |> add_if_present(params, "avatar", :avatar, fn value ->
111 with %Plug.Upload{} <- value,
112 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
113 {:ok, object.data}
114 else
115 _ -> :error
116 end
117 end)
118
119 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
120
121 user_info_emojis =
122 user.info
123 |> Map.get(:emoji, [])
124 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
125 |> Enum.dedup()
126
127 info_params =
128 [
129 :no_rich_text,
130 :locked,
131 :hide_followers_count,
132 :hide_follows_count,
133 :hide_followers,
134 :hide_follows,
135 :hide_favorites,
136 :show_role,
137 :skip_thread_containment,
138 :discoverable
139 ]
140 |> Enum.reduce(%{}, fn key, acc ->
141 add_if_present(acc, params, to_string(key), key, fn value ->
142 {:ok, truthy_param?(value)}
143 end)
144 end)
145 |> add_if_present(params, "default_scope", :default_scope)
146 |> add_if_present(params, "fields", :fields, fn fields ->
147 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
148
149 {:ok, fields}
150 end)
151 |> add_if_present(params, "fields", :raw_fields)
152 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
153 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
154 end)
155 |> add_if_present(params, "header", :banner, fn value ->
156 with %Plug.Upload{} <- value,
157 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
158 {:ok, object.data}
159 else
160 _ -> :error
161 end
162 end)
163 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
164 with %Plug.Upload{} <- value,
165 {:ok, object} <- ActivityPub.upload(value, type: :background) do
166 {:ok, object.data}
167 else
168 _ -> :error
169 end
170 end)
171 |> Map.put(:emoji, user_info_emojis)
172
173 changeset =
174 user
175 |> User.update_changeset(user_params)
176 |> User.change_info(&User.Info.profile_update(&1, info_params))
177
178 with {:ok, user} <- User.update_and_set_cache(changeset) do
179 if original_user != user, do: CommonAPI.update(user)
180
181 json(
182 conn,
183 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
184 )
185 else
186 _e -> render_error(conn, :forbidden, "Invalid request")
187 end
188 end
189
190 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
191 change = Changeset.change(user, %{avatar: nil})
192 {:ok, user} = User.update_and_set_cache(change)
193 CommonAPI.update(user)
194
195 json(conn, %{url: nil})
196 end
197
198 def update_avatar(%{assigns: %{user: user}} = conn, params) do
199 {:ok, object} = ActivityPub.upload(params, type: :avatar)
200 change = Changeset.change(user, %{avatar: object.data})
201 {:ok, user} = User.update_and_set_cache(change)
202 CommonAPI.update(user)
203 %{"url" => [%{"href" => href} | _]} = object.data
204
205 json(conn, %{url: href})
206 end
207
208 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
209 new_info = %{"banner" => %{}}
210
211 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
212 CommonAPI.update(user)
213 json(conn, %{url: nil})
214 end
215 end
216
217 def update_banner(%{assigns: %{user: user}} = conn, params) do
218 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
219 new_info <- %{"banner" => object.data},
220 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
221 CommonAPI.update(user)
222 %{"url" => [%{"href" => href} | _]} = object.data
223
224 json(conn, %{url: href})
225 end
226 end
227
228 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
229 new_info = %{"background" => %{}}
230
231 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
232 json(conn, %{url: nil})
233 end
234 end
235
236 def update_background(%{assigns: %{user: user}} = conn, params) do
237 with {:ok, object} <- ActivityPub.upload(params, type: :background),
238 new_info <- %{"background" => object.data},
239 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
240 %{"url" => [%{"href" => href} | _]} = object.data
241
242 json(conn, %{url: href})
243 end
244 end
245
246 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
247 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
248
249 account =
250 AccountView.render("account.json", %{
251 user: user,
252 for: user,
253 with_pleroma_settings: true,
254 with_chat_token: chat_token
255 })
256
257 json(conn, account)
258 end
259
260 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
261 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
262 conn
263 |> put_view(AppView)
264 |> render("short.json", %{app: app})
265 end
266 end
267
268 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
269 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
270 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
271 account = AccountView.render("account.json", %{user: user, for: for_user})
272 json(conn, account)
273 else
274 _e -> render_error(conn, :not_found, "Can't find user")
275 end
276 end
277
278 @mastodon_api_level "2.7.2"
279
280 def masto_instance(conn, _params) do
281 instance = Config.get(:instance)
282
283 response = %{
284 uri: Web.base_url(),
285 title: Keyword.get(instance, :name),
286 description: Keyword.get(instance, :description),
287 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
288 email: Keyword.get(instance, :email),
289 urls: %{
290 streaming_api: Pleroma.Web.Endpoint.websocket_url()
291 },
292 stats: Stats.get_stats(),
293 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
294 languages: ["en"],
295 registrations: Pleroma.Config.get([:instance, :registrations_open]),
296 # Extra (not present in Mastodon):
297 max_toot_chars: Keyword.get(instance, :limit),
298 poll_limits: Keyword.get(instance, :poll_limits)
299 }
300
301 json(conn, response)
302 end
303
304 def peers(conn, _params) do
305 json(conn, Stats.get_peers())
306 end
307
308 defp mastodonized_emoji do
309 Pleroma.Emoji.get_all()
310 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
311 url = to_string(URI.merge(Web.base_url(), relative_url))
312
313 %{
314 "shortcode" => shortcode,
315 "static_url" => url,
316 "visible_in_picker" => true,
317 "url" => url,
318 "tags" => tags,
319 # Assuming that a comma is authorized in the category name
320 "category" => (tags -- ["Custom"]) |> Enum.join(",")
321 }
322 end)
323 end
324
325 def custom_emojis(conn, _params) do
326 mastodon_emoji = mastodonized_emoji()
327 json(conn, mastodon_emoji)
328 end
329
330 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
331 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
332 params =
333 params
334 |> Map.put("tag", params["tagged"])
335
336 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
337
338 conn
339 |> add_link_headers(activities)
340 |> put_view(StatusView)
341 |> render("index.json", %{
342 activities: activities,
343 for: reading_user,
344 as: :activity
345 })
346 end
347 end
348
349 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
350 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
351 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
352 true <- Visibility.visible_for_user?(activity, user) do
353 conn
354 |> put_view(StatusView)
355 |> try_render("poll.json", %{object: object, for: user})
356 else
357 error when is_nil(error) or error == false ->
358 render_error(conn, :not_found, "Record not found")
359 end
360 end
361
362 defp get_cached_vote_or_vote(user, object, choices) do
363 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
364
365 {_, res} =
366 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
367 case CommonAPI.vote(user, object, choices) do
368 {:error, _message} = res -> {:ignore, res}
369 res -> {:commit, res}
370 end
371 end)
372
373 res
374 end
375
376 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
377 with %Object{} = object <- Object.get_by_id(id),
378 true <- object.data["type"] == "Question",
379 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
380 true <- Visibility.visible_for_user?(activity, user),
381 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
382 conn
383 |> put_view(StatusView)
384 |> try_render("poll.json", %{object: object, for: user})
385 else
386 nil ->
387 render_error(conn, :not_found, "Record not found")
388
389 false ->
390 render_error(conn, :not_found, "Record not found")
391
392 {:error, message} ->
393 conn
394 |> put_status(:unprocessable_entity)
395 |> json(%{error: message})
396 end
397 end
398
399 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
400 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
401 conn
402 |> add_link_headers(scheduled_activities)
403 |> put_view(ScheduledActivityView)
404 |> render("index.json", %{scheduled_activities: scheduled_activities})
405 end
406 end
407
408 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
409 with %ScheduledActivity{} = scheduled_activity <-
410 ScheduledActivity.get(user, scheduled_activity_id) do
411 conn
412 |> put_view(ScheduledActivityView)
413 |> render("show.json", %{scheduled_activity: scheduled_activity})
414 else
415 _ -> {:error, :not_found}
416 end
417 end
418
419 def update_scheduled_status(
420 %{assigns: %{user: user}} = conn,
421 %{"id" => scheduled_activity_id} = params
422 ) do
423 with %ScheduledActivity{} = scheduled_activity <-
424 ScheduledActivity.get(user, scheduled_activity_id),
425 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
426 conn
427 |> put_view(ScheduledActivityView)
428 |> render("show.json", %{scheduled_activity: scheduled_activity})
429 else
430 nil -> {:error, :not_found}
431 error -> error
432 end
433 end
434
435 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
436 with %ScheduledActivity{} = scheduled_activity <-
437 ScheduledActivity.get(user, scheduled_activity_id),
438 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
439 conn
440 |> put_view(ScheduledActivityView)
441 |> render("show.json", %{scheduled_activity: scheduled_activity})
442 else
443 nil -> {:error, :not_found}
444 error -> error
445 end
446 end
447
448 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
449 id = List.wrap(id)
450 q = from(u in User, where: u.id in ^id)
451 targets = Repo.all(q)
452
453 conn
454 |> put_view(AccountView)
455 |> render("relationships.json", %{user: user, targets: targets})
456 end
457
458 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
459 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
460
461 def update_media(%{assigns: %{user: user}} = conn, data) do
462 with %Object{} = object <- Repo.get(Object, data["id"]),
463 true <- Object.authorize_mutation(object, user),
464 true <- is_binary(data["description"]),
465 description <- data["description"] do
466 new_data = %{object.data | "name" => description}
467
468 {:ok, _} =
469 object
470 |> Object.change(%{data: new_data})
471 |> Repo.update()
472
473 attachment_data = Map.put(new_data, "id", object.id)
474
475 conn
476 |> put_view(StatusView)
477 |> render("attachment.json", %{attachment: attachment_data})
478 end
479 end
480
481 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
482 with {:ok, object} <-
483 ActivityPub.upload(
484 file,
485 actor: User.ap_id(user),
486 description: Map.get(data, "description")
487 ) do
488 attachment_data = Map.put(object.data, "id", object.id)
489
490 conn
491 |> put_view(StatusView)
492 |> render("attachment.json", %{attachment: attachment_data})
493 end
494 end
495
496 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
497 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
498 %{} = attachment_data <- Map.put(object.data, "id", object.id),
499 # Reject if not an image
500 %{type: "image"} = rendered <-
501 StatusView.render("attachment.json", %{attachment: attachment_data}) do
502 # Sure!
503 # Save to the user's info
504 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
505
506 json(conn, rendered)
507 else
508 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
509 end
510 end
511
512 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
513 mascot = User.get_mascot(user)
514
515 conn
516 |> json(mascot)
517 end
518
519 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
520 with %User{} = user <- User.get_cached_by_id(id),
521 followers <- MastodonAPI.get_followers(user, params) do
522 followers =
523 cond do
524 for_user && user.id == for_user.id -> followers
525 user.info.hide_followers -> []
526 true -> followers
527 end
528
529 conn
530 |> add_link_headers(followers)
531 |> put_view(AccountView)
532 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
533 end
534 end
535
536 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
537 with %User{} = user <- User.get_cached_by_id(id),
538 followers <- MastodonAPI.get_friends(user, params) do
539 followers =
540 cond do
541 for_user && user.id == for_user.id -> followers
542 user.info.hide_follows -> []
543 true -> followers
544 end
545
546 conn
547 |> add_link_headers(followers)
548 |> put_view(AccountView)
549 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
550 end
551 end
552
553 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
554 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
555 {_, true} <- {:followed, follower.id != followed.id},
556 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
557 conn
558 |> put_view(AccountView)
559 |> render("relationship.json", %{user: follower, target: followed})
560 else
561 {:followed, _} ->
562 {:error, :not_found}
563
564 {:error, message} ->
565 conn
566 |> put_status(:forbidden)
567 |> json(%{error: message})
568 end
569 end
570
571 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
572 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
573 {_, true} <- {:followed, follower.id != followed.id},
574 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
575 conn
576 |> put_view(AccountView)
577 |> render("account.json", %{user: followed, for: follower})
578 else
579 {:followed, _} ->
580 {:error, :not_found}
581
582 {:error, message} ->
583 conn
584 |> put_status(:forbidden)
585 |> json(%{error: message})
586 end
587 end
588
589 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
590 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
591 {_, true} <- {:followed, follower.id != followed.id},
592 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
593 conn
594 |> put_view(AccountView)
595 |> render("relationship.json", %{user: follower, target: followed})
596 else
597 {:followed, _} ->
598 {:error, :not_found}
599
600 error ->
601 error
602 end
603 end
604
605 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
606 notifications =
607 if Map.has_key?(params, "notifications"),
608 do: params["notifications"] in [true, "True", "true", "1"],
609 else: true
610
611 with %User{} = muted <- User.get_cached_by_id(id),
612 {:ok, muter} <- User.mute(muter, muted, notifications) do
613 conn
614 |> put_view(AccountView)
615 |> render("relationship.json", %{user: muter, target: muted})
616 else
617 {:error, message} ->
618 conn
619 |> put_status(:forbidden)
620 |> json(%{error: message})
621 end
622 end
623
624 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
625 with %User{} = muted <- User.get_cached_by_id(id),
626 {:ok, muter} <- User.unmute(muter, muted) do
627 conn
628 |> put_view(AccountView)
629 |> render("relationship.json", %{user: muter, target: muted})
630 else
631 {:error, message} ->
632 conn
633 |> put_status(:forbidden)
634 |> json(%{error: message})
635 end
636 end
637
638 def mutes(%{assigns: %{user: user}} = conn, _) do
639 with muted_accounts <- User.muted_users(user) do
640 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
641 json(conn, res)
642 end
643 end
644
645 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
646 with %User{} = blocked <- User.get_cached_by_id(id),
647 {:ok, blocker} <- User.block(blocker, blocked),
648 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
649 conn
650 |> put_view(AccountView)
651 |> render("relationship.json", %{user: blocker, target: blocked})
652 else
653 {:error, message} ->
654 conn
655 |> put_status(:forbidden)
656 |> json(%{error: message})
657 end
658 end
659
660 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
661 with %User{} = blocked <- User.get_cached_by_id(id),
662 {:ok, blocker} <- User.unblock(blocker, blocked),
663 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
664 conn
665 |> put_view(AccountView)
666 |> render("relationship.json", %{user: blocker, target: blocked})
667 else
668 {:error, message} ->
669 conn
670 |> put_status(:forbidden)
671 |> json(%{error: message})
672 end
673 end
674
675 def blocks(%{assigns: %{user: user}} = conn, _) do
676 with blocked_accounts <- User.blocked_users(user) do
677 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
678 json(conn, res)
679 end
680 end
681
682 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
683 json(conn, info.domain_blocks || [])
684 end
685
686 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
687 User.block_domain(blocker, domain)
688 json(conn, %{})
689 end
690
691 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
692 User.unblock_domain(blocker, domain)
693 json(conn, %{})
694 end
695
696 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
697 with %User{} = subscription_target <- User.get_cached_by_id(id),
698 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
699 conn
700 |> put_view(AccountView)
701 |> render("relationship.json", %{user: user, target: subscription_target})
702 else
703 {:error, message} ->
704 conn
705 |> put_status(:forbidden)
706 |> json(%{error: message})
707 end
708 end
709
710 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
711 with %User{} = subscription_target <- User.get_cached_by_id(id),
712 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
713 conn
714 |> put_view(AccountView)
715 |> render("relationship.json", %{user: user, target: subscription_target})
716 else
717 {:error, message} ->
718 conn
719 |> put_status(:forbidden)
720 |> json(%{error: message})
721 end
722 end
723
724 def favourites(%{assigns: %{user: user}} = conn, params) do
725 params =
726 params
727 |> Map.put("type", "Create")
728 |> Map.put("favorited_by", user.ap_id)
729 |> Map.put("blocking_user", user)
730
731 activities =
732 ActivityPub.fetch_activities([], params)
733 |> Enum.reverse()
734
735 conn
736 |> add_link_headers(activities)
737 |> put_view(StatusView)
738 |> render("index.json", %{activities: activities, for: user, as: :activity})
739 end
740
741 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
742 with %User{} = user <- User.get_by_id(id),
743 false <- user.info.hide_favorites do
744 params =
745 params
746 |> Map.put("type", "Create")
747 |> Map.put("favorited_by", user.ap_id)
748 |> Map.put("blocking_user", for_user)
749
750 recipients =
751 if for_user do
752 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
753 else
754 [Pleroma.Constants.as_public()]
755 end
756
757 activities =
758 recipients
759 |> ActivityPub.fetch_activities(params)
760 |> Enum.reverse()
761
762 conn
763 |> add_link_headers(activities)
764 |> put_view(StatusView)
765 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
766 else
767 nil -> {:error, :not_found}
768 true -> render_error(conn, :forbidden, "Can't get favorites")
769 end
770 end
771
772 def bookmarks(%{assigns: %{user: user}} = conn, params) do
773 user = User.get_cached_by_id(user.id)
774
775 bookmarks =
776 Bookmark.for_user_query(user.id)
777 |> Pagination.fetch_paginated(params)
778
779 activities =
780 bookmarks
781 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
782
783 conn
784 |> add_link_headers(bookmarks)
785 |> put_view(StatusView)
786 |> render("index.json", %{activities: activities, for: user, as: :activity})
787 end
788
789 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
790 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
791 res = ListView.render("lists.json", lists: lists)
792 json(conn, res)
793 end
794
795 def index(%{assigns: %{user: user}} = conn, _params) do
796 token = get_session(conn, :oauth_token)
797
798 if user && token do
799 mastodon_emoji = mastodonized_emoji()
800
801 limit = Config.get([:instance, :limit])
802
803 accounts =
804 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
805
806 initial_state =
807 %{
808 meta: %{
809 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
810 access_token: token,
811 locale: "en",
812 domain: Pleroma.Web.Endpoint.host(),
813 admin: "1",
814 me: "#{user.id}",
815 unfollow_modal: false,
816 boost_modal: false,
817 delete_modal: true,
818 auto_play_gif: false,
819 display_sensitive_media: false,
820 reduce_motion: false,
821 max_toot_chars: limit,
822 mascot: User.get_mascot(user)["url"]
823 },
824 poll_limits: Config.get([:instance, :poll_limits]),
825 rights: %{
826 delete_others_notice: present?(user.info.is_moderator),
827 admin: present?(user.info.is_admin)
828 },
829 compose: %{
830 me: "#{user.id}",
831 default_privacy: user.info.default_scope,
832 default_sensitive: false,
833 allow_content_types: Config.get([:instance, :allowed_post_formats])
834 },
835 media_attachments: %{
836 accept_content_types: [
837 ".jpg",
838 ".jpeg",
839 ".png",
840 ".gif",
841 ".webm",
842 ".mp4",
843 ".m4v",
844 "image\/jpeg",
845 "image\/png",
846 "image\/gif",
847 "video\/webm",
848 "video\/mp4"
849 ]
850 },
851 settings:
852 user.info.settings ||
853 %{
854 onboarded: true,
855 home: %{
856 shows: %{
857 reblog: true,
858 reply: true
859 }
860 },
861 notifications: %{
862 alerts: %{
863 follow: true,
864 favourite: true,
865 reblog: true,
866 mention: true
867 },
868 shows: %{
869 follow: true,
870 favourite: true,
871 reblog: true,
872 mention: true
873 },
874 sounds: %{
875 follow: true,
876 favourite: true,
877 reblog: true,
878 mention: true
879 }
880 }
881 },
882 push_subscription: nil,
883 accounts: accounts,
884 custom_emojis: mastodon_emoji,
885 char_limit: limit
886 }
887 |> Jason.encode!()
888
889 conn
890 |> put_layout(false)
891 |> put_view(MastodonView)
892 |> render("index.html", %{initial_state: initial_state})
893 else
894 conn
895 |> put_session(:return_to, conn.request_path)
896 |> redirect(to: "/web/login")
897 end
898 end
899
900 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
901 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
902 json(conn, %{})
903 else
904 e ->
905 conn
906 |> put_status(:internal_server_error)
907 |> json(%{error: inspect(e)})
908 end
909 end
910
911 def login(%{assigns: %{user: %User{}}} = conn, _params) do
912 redirect(conn, to: local_mastodon_root_path(conn))
913 end
914
915 @doc "Local Mastodon FE login init action"
916 def login(conn, %{"code" => auth_token}) do
917 with {:ok, app} <- get_or_make_app(),
918 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
919 {:ok, token} <- Token.exchange_token(app, auth) do
920 conn
921 |> put_session(:oauth_token, token.token)
922 |> redirect(to: local_mastodon_root_path(conn))
923 end
924 end
925
926 @doc "Local Mastodon FE callback action"
927 def login(conn, _) do
928 with {:ok, app} <- get_or_make_app() do
929 path =
930 o_auth_path(
931 conn,
932 :authorize,
933 response_type: "code",
934 client_id: app.client_id,
935 redirect_uri: ".",
936 scope: Enum.join(app.scopes, " ")
937 )
938
939 redirect(conn, to: path)
940 end
941 end
942
943 defp local_mastodon_root_path(conn) do
944 case get_session(conn, :return_to) do
945 nil ->
946 mastodon_api_path(conn, :index, ["getting-started"])
947
948 return_to ->
949 delete_session(conn, :return_to)
950 return_to
951 end
952 end
953
954 defp get_or_make_app do
955 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
956 scopes = ["read", "write", "follow", "push"]
957
958 with %App{} = app <- Repo.get_by(App, find_attrs) do
959 {:ok, app} =
960 if app.scopes == scopes do
961 {:ok, app}
962 else
963 app
964 |> Changeset.change(%{scopes: scopes})
965 |> Repo.update()
966 end
967
968 {:ok, app}
969 else
970 _e ->
971 cs =
972 App.register_changeset(
973 %App{},
974 Map.put(find_attrs, :scopes, scopes)
975 )
976
977 Repo.insert(cs)
978 end
979 end
980
981 def logout(conn, _) do
982 conn
983 |> clear_session
984 |> redirect(to: "/")
985 end
986
987 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
988 Logger.debug("Unimplemented, returning unmodified relationship")
989
990 with %User{} = target <- User.get_cached_by_id(id) do
991 conn
992 |> put_view(AccountView)
993 |> render("relationship.json", %{user: user, target: target})
994 end
995 end
996
997 def empty_array(conn, _) do
998 Logger.debug("Unimplemented, returning an empty array")
999 json(conn, [])
1000 end
1001
1002 def empty_object(conn, _) do
1003 Logger.debug("Unimplemented, returning an empty object")
1004 json(conn, %{})
1005 end
1006
1007 def get_filters(%{assigns: %{user: user}} = conn, _) do
1008 filters = Filter.get_filters(user)
1009 res = FilterView.render("filters.json", filters: filters)
1010 json(conn, res)
1011 end
1012
1013 def create_filter(
1014 %{assigns: %{user: user}} = conn,
1015 %{"phrase" => phrase, "context" => context} = params
1016 ) do
1017 query = %Filter{
1018 user_id: user.id,
1019 phrase: phrase,
1020 context: context,
1021 hide: Map.get(params, "irreversible", false),
1022 whole_word: Map.get(params, "boolean", true)
1023 # expires_at
1024 }
1025
1026 {:ok, response} = Filter.create(query)
1027 res = FilterView.render("filter.json", filter: response)
1028 json(conn, res)
1029 end
1030
1031 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1032 filter = Filter.get(filter_id, user)
1033 res = FilterView.render("filter.json", filter: filter)
1034 json(conn, res)
1035 end
1036
1037 def update_filter(
1038 %{assigns: %{user: user}} = conn,
1039 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1040 ) do
1041 query = %Filter{
1042 user_id: user.id,
1043 filter_id: filter_id,
1044 phrase: phrase,
1045 context: context,
1046 hide: Map.get(params, "irreversible", nil),
1047 whole_word: Map.get(params, "boolean", true)
1048 # expires_at
1049 }
1050
1051 {:ok, response} = Filter.update(query)
1052 res = FilterView.render("filter.json", filter: response)
1053 json(conn, res)
1054 end
1055
1056 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1057 query = %Filter{
1058 user_id: user.id,
1059 filter_id: filter_id
1060 }
1061
1062 {:ok, _} = Filter.delete(query)
1063 json(conn, %{})
1064 end
1065
1066 def suggestions(%{assigns: %{user: user}} = conn, _) do
1067 suggestions = Config.get(:suggestions)
1068
1069 if Keyword.get(suggestions, :enabled, false) do
1070 api = Keyword.get(suggestions, :third_party_engine, "")
1071 timeout = Keyword.get(suggestions, :timeout, 5000)
1072 limit = Keyword.get(suggestions, :limit, 23)
1073
1074 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1075
1076 user = user.nickname
1077
1078 url =
1079 api
1080 |> String.replace("{{host}}", host)
1081 |> String.replace("{{user}}", user)
1082
1083 with {:ok, %{status: 200, body: body}} <-
1084 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1085 {:ok, data} <- Jason.decode(body) do
1086 data =
1087 data
1088 |> Enum.slice(0, limit)
1089 |> Enum.map(fn x ->
1090 x
1091 |> Map.put("id", fetch_suggestion_id(x))
1092 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1093 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1094 end)
1095
1096 json(conn, data)
1097 else
1098 e ->
1099 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1100 end
1101 else
1102 json(conn, [])
1103 end
1104 end
1105
1106 defp fetch_suggestion_id(attrs) do
1107 case User.get_or_fetch(attrs["acct"]) do
1108 {:ok, %User{id: id}} -> id
1109 _ -> 0
1110 end
1111 end
1112
1113 def reports(%{assigns: %{user: user}} = conn, params) do
1114 case CommonAPI.report(user, params) do
1115 {:ok, activity} ->
1116 conn
1117 |> put_view(ReportView)
1118 |> try_render("report.json", %{activity: activity})
1119
1120 {:error, err} ->
1121 conn
1122 |> put_status(:bad_request)
1123 |> json(%{error: err})
1124 end
1125 end
1126
1127 def account_register(
1128 %{assigns: %{app: app}} = conn,
1129 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1130 ) do
1131 params =
1132 params
1133 |> Map.take([
1134 "email",
1135 "captcha_solution",
1136 "captcha_token",
1137 "captcha_answer_data",
1138 "token",
1139 "password"
1140 ])
1141 |> Map.put("nickname", nickname)
1142 |> Map.put("fullname", params["fullname"] || nickname)
1143 |> Map.put("bio", params["bio"] || "")
1144 |> Map.put("confirm", params["password"])
1145
1146 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1147 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1148 json(conn, %{
1149 token_type: "Bearer",
1150 access_token: token.token,
1151 scope: app.scopes,
1152 created_at: Token.Utils.format_created_at(token)
1153 })
1154 else
1155 {:error, errors} ->
1156 conn
1157 |> put_status(:bad_request)
1158 |> json(errors)
1159 end
1160 end
1161
1162 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1163 render_error(conn, :bad_request, "Missing parameters")
1164 end
1165
1166 def account_register(conn, _) do
1167 render_error(conn, :forbidden, "Invalid credentials")
1168 end
1169
1170 def conversations(%{assigns: %{user: user}} = conn, params) do
1171 participations = Participation.for_user_with_last_activity_id(user, params)
1172
1173 conversations =
1174 Enum.map(participations, fn participation ->
1175 ConversationView.render("participation.json", %{participation: participation, for: user})
1176 end)
1177
1178 conn
1179 |> add_link_headers(participations)
1180 |> json(conversations)
1181 end
1182
1183 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1184 with %Participation{} = participation <-
1185 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1186 {:ok, participation} <- Participation.mark_as_read(participation) do
1187 participation_view =
1188 ConversationView.render("participation.json", %{participation: participation, for: user})
1189
1190 conn
1191 |> json(participation_view)
1192 end
1193 end
1194
1195 def password_reset(conn, params) do
1196 nickname_or_email = params["email"] || params["nickname"]
1197
1198 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1199 conn
1200 |> put_status(:no_content)
1201 |> json("")
1202 else
1203 {:error, "unknown user"} ->
1204 send_resp(conn, :not_found, "")
1205
1206 {:error, _} ->
1207 send_resp(conn, :bad_request, "")
1208 end
1209 end
1210
1211 def account_confirmation_resend(conn, params) do
1212 nickname_or_email = params["email"] || params["nickname"]
1213
1214 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1215 {:ok, _} <- User.try_send_confirmation_email(user) do
1216 conn
1217 |> json_response(:no_content, "")
1218 end
1219 end
1220
1221 def try_render(conn, target, params)
1222 when is_binary(target) do
1223 case render(conn, target, params) do
1224 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1225 res -> res
1226 end
1227 end
1228
1229 def try_render(conn, _, _) do
1230 render_error(conn, :not_implemented, "Can't display this activity")
1231 end
1232
1233 defp present?(nil), do: false
1234 defp present?(false), do: false
1235 defp present?(_), do: true
1236 end