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