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