ecb7df4590022756f15930c95e54c9540d50ba6c
[akkoma] / lib / pleroma / web / mastodon_api / 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 alias Ecto.Changeset
8 alias Pleroma.Activity
9 alias Pleroma.Bookmark
10 alias Pleroma.Config
11 alias Pleroma.Conversation.Participation
12 alias Pleroma.Filter
13 alias Pleroma.Formatter
14 alias Pleroma.Notification
15 alias Pleroma.Object
16 alias Pleroma.Object.Fetcher
17 alias Pleroma.Pagination
18 alias Pleroma.Repo
19 alias Pleroma.ScheduledActivity
20 alias Pleroma.Stats
21 alias Pleroma.User
22 alias Pleroma.Web
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.AccountView
27 alias Pleroma.Web.MastodonAPI.AppView
28 alias Pleroma.Web.MastodonAPI.ConversationView
29 alias Pleroma.Web.MastodonAPI.FilterView
30 alias Pleroma.Web.MastodonAPI.ListView
31 alias Pleroma.Web.MastodonAPI.MastodonAPI
32 alias Pleroma.Web.MastodonAPI.MastodonView
33 alias Pleroma.Web.MastodonAPI.NotificationView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36 alias Pleroma.Web.MastodonAPI.StatusView
37 alias Pleroma.Web.MediaProxy
38 alias Pleroma.Web.OAuth.App
39 alias Pleroma.Web.OAuth.Authorization
40 alias Pleroma.Web.OAuth.Scopes
41 alias Pleroma.Web.OAuth.Token
42 alias Pleroma.Web.TwitterAPI.TwitterAPI
43
44 alias Pleroma.Web.ControllerHelper
45 import Ecto.Query
46
47 require Logger
48
49 plug(
50 Pleroma.Plugs.RateLimitPlug,
51 %{
52 max_requests: Config.get([:app_account_creation, :max_requests]),
53 interval: Config.get([:app_account_creation, :interval])
54 }
55 when action in [:account_register]
56 )
57
58 @httpoison Application.get_env(:pleroma, :httpoison)
59 @local_mastodon_name "Mastodon-Local"
60
61 action_fallback(:errors)
62
63 def create_app(conn, params) do
64 scopes = Scopes.fetch_scopes(params, ["read"])
65
66 app_attrs =
67 params
68 |> Map.drop(["scope", "scopes"])
69 |> Map.put("scopes", scopes)
70
71 with cs <- App.register_changeset(%App{}, app_attrs),
72 false <- cs.changes[:client_name] == @local_mastodon_name,
73 {:ok, app} <- Repo.insert(cs) do
74 conn
75 |> put_view(AppView)
76 |> render("show.json", %{app: app})
77 end
78 end
79
80 defp add_if_present(
81 map,
82 params,
83 params_field,
84 map_field,
85 value_function \\ fn x -> {:ok, x} end
86 ) do
87 if Map.has_key?(params, params_field) do
88 case value_function.(params[params_field]) do
89 {:ok, new_value} -> Map.put(map, map_field, new_value)
90 :error -> map
91 end
92 else
93 map
94 end
95 end
96
97 def update_credentials(%{assigns: %{user: user}} = conn, params) do
98 original_user = user
99
100 user_params =
101 %{}
102 |> add_if_present(params, "display_name", :name)
103 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
104 |> add_if_present(params, "avatar", :avatar, fn value ->
105 with %Plug.Upload{} <- value,
106 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
107 {:ok, object.data}
108 else
109 _ -> :error
110 end
111 end)
112
113 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
114
115 user_info_emojis =
116 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
117 |> Enum.dedup()
118
119 info_params =
120 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
121 |> Enum.reduce(%{}, fn key, acc ->
122 add_if_present(acc, params, to_string(key), key, fn value ->
123 {:ok, ControllerHelper.truthy_param?(value)}
124 end)
125 end)
126 |> add_if_present(params, "default_scope", :default_scope)
127 |> add_if_present(params, "header", :banner, fn value ->
128 with %Plug.Upload{} <- value,
129 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
130 {:ok, object.data}
131 else
132 _ -> :error
133 end
134 end)
135 |> Map.put(:emoji, user_info_emojis)
136
137 info_cng = User.Info.profile_update(user.info, info_params)
138
139 with changeset <- User.update_changeset(user, user_params),
140 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
141 {:ok, user} <- User.update_and_set_cache(changeset) do
142 if original_user != user do
143 CommonAPI.update(user)
144 end
145
146 json(conn, AccountView.render("account.json", %{user: user, for: user}))
147 else
148 _e ->
149 conn
150 |> put_status(403)
151 |> json(%{error: "Invalid request"})
152 end
153 end
154
155 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
156 account = AccountView.render("account.json", %{user: user, for: user})
157 json(conn, account)
158 end
159
160 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
161 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
162 conn
163 |> put_view(AppView)
164 |> render("short.json", %{app: app})
165 end
166 end
167
168 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
169 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
170 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
171 account = AccountView.render("account.json", %{user: user, for: for_user})
172 json(conn, account)
173 else
174 _e ->
175 conn
176 |> put_status(404)
177 |> json(%{error: "Can't find user"})
178 end
179 end
180
181 @mastodon_api_level "2.7.2"
182
183 def masto_instance(conn, _params) do
184 instance = Config.get(:instance)
185
186 response = %{
187 uri: Web.base_url(),
188 title: Keyword.get(instance, :name),
189 description: Keyword.get(instance, :description),
190 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
191 email: Keyword.get(instance, :email),
192 urls: %{
193 streaming_api: Pleroma.Web.Endpoint.websocket_url()
194 },
195 stats: Stats.get_stats(),
196 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
197 languages: ["en"],
198 registrations: Pleroma.Config.get([:instance, :registrations_open]),
199 # Extra (not present in Mastodon):
200 max_toot_chars: Keyword.get(instance, :limit),
201 poll_limits: Keyword.get(instance, :poll_limits)
202 }
203
204 json(conn, response)
205 end
206
207 def peers(conn, _params) do
208 json(conn, Stats.get_peers())
209 end
210
211 defp mastodonized_emoji do
212 Pleroma.Emoji.get_all()
213 |> Enum.map(fn {shortcode, relative_url, tags} ->
214 url = to_string(URI.merge(Web.base_url(), relative_url))
215
216 %{
217 "shortcode" => shortcode,
218 "static_url" => url,
219 "visible_in_picker" => true,
220 "url" => url,
221 "tags" => tags
222 }
223 end)
224 end
225
226 def custom_emojis(conn, _params) do
227 mastodon_emoji = mastodonized_emoji()
228 json(conn, mastodon_emoji)
229 end
230
231 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
232 params =
233 conn.params
234 |> Map.drop(["since_id", "max_id", "min_id"])
235 |> Map.merge(params)
236
237 last = List.last(activities)
238
239 if last do
240 max_id = last.id
241
242 limit =
243 params
244 |> Map.get("limit", "20")
245 |> String.to_integer()
246
247 min_id =
248 if length(activities) <= limit do
249 activities
250 |> List.first()
251 |> Map.get(:id)
252 else
253 activities
254 |> Enum.at(limit * -1)
255 |> Map.get(:id)
256 end
257
258 {next_url, prev_url} =
259 if param do
260 {
261 mastodon_api_url(
262 Pleroma.Web.Endpoint,
263 method,
264 param,
265 Map.merge(params, %{max_id: max_id})
266 ),
267 mastodon_api_url(
268 Pleroma.Web.Endpoint,
269 method,
270 param,
271 Map.merge(params, %{min_id: min_id})
272 )
273 }
274 else
275 {
276 mastodon_api_url(
277 Pleroma.Web.Endpoint,
278 method,
279 Map.merge(params, %{max_id: max_id})
280 ),
281 mastodon_api_url(
282 Pleroma.Web.Endpoint,
283 method,
284 Map.merge(params, %{min_id: min_id})
285 )
286 }
287 end
288
289 conn
290 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
291 else
292 conn
293 end
294 end
295
296 def home_timeline(%{assigns: %{user: user}} = conn, params) do
297 params =
298 params
299 |> Map.put("type", ["Create", "Announce"])
300 |> Map.put("blocking_user", user)
301 |> Map.put("muting_user", user)
302 |> Map.put("user", user)
303
304 activities =
305 [user.ap_id | user.following]
306 |> ActivityPub.fetch_activities(params)
307 |> Enum.reverse()
308
309 conn
310 |> add_link_headers(:home_timeline, activities)
311 |> put_view(StatusView)
312 |> render("index.json", %{activities: activities, for: user, as: :activity})
313 end
314
315 def public_timeline(%{assigns: %{user: user}} = conn, params) do
316 local_only = params["local"] in [true, "True", "true", "1"]
317
318 activities =
319 params
320 |> Map.put("type", ["Create", "Announce"])
321 |> Map.put("local_only", local_only)
322 |> Map.put("blocking_user", user)
323 |> Map.put("muting_user", user)
324 |> ActivityPub.fetch_public_activities()
325 |> Enum.reverse()
326
327 conn
328 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
329 |> put_view(StatusView)
330 |> render("index.json", %{activities: activities, for: user, as: :activity})
331 end
332
333 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
334 with %User{} = user <- User.get_cached_by_id(params["id"]) do
335 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
336
337 conn
338 |> add_link_headers(:user_statuses, activities, params["id"])
339 |> put_view(StatusView)
340 |> render("index.json", %{
341 activities: activities,
342 for: reading_user,
343 as: :activity
344 })
345 end
346 end
347
348 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
349 params =
350 params
351 |> Map.put("type", "Create")
352 |> Map.put("blocking_user", user)
353 |> Map.put("user", user)
354 |> Map.put(:visibility, "direct")
355
356 activities =
357 [user.ap_id]
358 |> ActivityPub.fetch_activities_query(params)
359 |> Pagination.fetch_paginated(params)
360
361 conn
362 |> add_link_headers(:dm_timeline, activities)
363 |> put_view(StatusView)
364 |> render("index.json", %{activities: activities, for: user, as: :activity})
365 end
366
367 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
368 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
369 true <- Visibility.visible_for_user?(activity, user) do
370 conn
371 |> put_view(StatusView)
372 |> try_render("status.json", %{activity: activity, for: user})
373 end
374 end
375
376 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
377 with %Activity{} = activity <- Activity.get_by_id(id),
378 activities <-
379 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
380 "blocking_user" => user,
381 "user" => user
382 }),
383 activities <-
384 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
385 activities <-
386 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
387 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
388 result = %{
389 ancestors:
390 StatusView.render(
391 "index.json",
392 for: user,
393 activities: grouped_activities[true] || [],
394 as: :activity
395 )
396 |> Enum.reverse(),
397 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
398 descendants:
399 StatusView.render(
400 "index.json",
401 for: user,
402 activities: grouped_activities[false] || [],
403 as: :activity
404 )
405 |> Enum.reverse()
406 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
407 }
408
409 json(conn, result)
410 end
411 end
412
413 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
414 with %Object{} = object <- Object.get_by_id(id),
415 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
416 true <- Visibility.visible_for_user?(activity, user) do
417 conn
418 |> put_view(StatusView)
419 |> try_render("poll.json", %{object: object, for: user})
420 else
421 nil ->
422 conn
423 |> put_status(404)
424 |> json(%{error: "Record not found"})
425
426 false ->
427 conn
428 |> put_status(404)
429 |> json(%{error: "Record not found"})
430 end
431 end
432
433 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
434 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
435 conn
436 |> add_link_headers(:scheduled_statuses, scheduled_activities)
437 |> put_view(ScheduledActivityView)
438 |> render("index.json", %{scheduled_activities: scheduled_activities})
439 end
440 end
441
442 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
443 with %ScheduledActivity{} = scheduled_activity <-
444 ScheduledActivity.get(user, scheduled_activity_id) do
445 conn
446 |> put_view(ScheduledActivityView)
447 |> render("show.json", %{scheduled_activity: scheduled_activity})
448 else
449 _ -> {:error, :not_found}
450 end
451 end
452
453 def update_scheduled_status(
454 %{assigns: %{user: user}} = conn,
455 %{"id" => scheduled_activity_id} = params
456 ) do
457 with %ScheduledActivity{} = scheduled_activity <-
458 ScheduledActivity.get(user, scheduled_activity_id),
459 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
460 conn
461 |> put_view(ScheduledActivityView)
462 |> render("show.json", %{scheduled_activity: scheduled_activity})
463 else
464 nil -> {:error, :not_found}
465 error -> error
466 end
467 end
468
469 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
470 with %ScheduledActivity{} = scheduled_activity <-
471 ScheduledActivity.get(user, scheduled_activity_id),
472 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
473 conn
474 |> put_view(ScheduledActivityView)
475 |> render("show.json", %{scheduled_activity: scheduled_activity})
476 else
477 nil -> {:error, :not_found}
478 error -> error
479 end
480 end
481
482 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
483 when length(media_ids) > 0 do
484 params =
485 params
486 |> Map.put("status", ".")
487
488 post_status(conn, params)
489 end
490
491 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
492 params =
493 params
494 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
495
496 scheduled_at = params["scheduled_at"]
497
498 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
499 with {:ok, scheduled_activity} <-
500 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
501 conn
502 |> put_view(ScheduledActivityView)
503 |> render("show.json", %{scheduled_activity: scheduled_activity})
504 end
505 else
506 params = Map.drop(params, ["scheduled_at"])
507
508 case get_cached_status_or_post(conn, params) do
509 {:ignore, message} ->
510 conn
511 |> put_status(422)
512 |> json(%{error: message})
513
514 {:error, message} ->
515 conn
516 |> put_status(422)
517 |> json(%{error: message})
518
519 {_, activity} ->
520 conn
521 |> put_view(StatusView)
522 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
523 end
524 end
525 end
526
527 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
528 idempotency_key =
529 case get_req_header(conn, "idempotency-key") do
530 [key] -> key
531 _ -> Ecto.UUID.generate()
532 end
533
534 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
535 case CommonAPI.post(user, params) do
536 {:ok, activity} -> activity
537 {:error, message} -> {:ignore, message}
538 end
539 end)
540 end
541
542 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
543 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
544 json(conn, %{})
545 else
546 _e ->
547 conn
548 |> put_status(403)
549 |> json(%{error: "Can't delete this post"})
550 end
551 end
552
553 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
554 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
555 %Activity{} = announce <- Activity.normalize(announce.data) do
556 conn
557 |> put_view(StatusView)
558 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
559 end
560 end
561
562 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
563 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
564 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
565 conn
566 |> put_view(StatusView)
567 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
568 end
569 end
570
571 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
572 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
573 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
574 conn
575 |> put_view(StatusView)
576 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
577 end
578 end
579
580 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
581 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
582 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
583 conn
584 |> put_view(StatusView)
585 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
586 end
587 end
588
589 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
590 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
591 conn
592 |> put_view(StatusView)
593 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
594 else
595 {:error, reason} ->
596 conn
597 |> put_resp_content_type("application/json")
598 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
599 end
600 end
601
602 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
603 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
604 conn
605 |> put_view(StatusView)
606 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
607 end
608 end
609
610 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
611 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
612 %User{} = user <- User.get_cached_by_nickname(user.nickname),
613 true <- Visibility.visible_for_user?(activity, user),
614 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
615 conn
616 |> put_view(StatusView)
617 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
618 end
619 end
620
621 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
622 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
623 %User{} = user <- User.get_cached_by_nickname(user.nickname),
624 true <- Visibility.visible_for_user?(activity, user),
625 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
626 conn
627 |> put_view(StatusView)
628 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
629 end
630 end
631
632 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
633 activity = Activity.get_by_id(id)
634
635 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
636 conn
637 |> put_view(StatusView)
638 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
639 else
640 {:error, reason} ->
641 conn
642 |> put_resp_content_type("application/json")
643 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
644 end
645 end
646
647 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
648 activity = Activity.get_by_id(id)
649
650 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
651 conn
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
654 end
655 end
656
657 def notifications(%{assigns: %{user: user}} = conn, params) do
658 notifications = MastodonAPI.get_notifications(user, params)
659
660 conn
661 |> add_link_headers(:notifications, notifications)
662 |> put_view(NotificationView)
663 |> render("index.json", %{notifications: notifications, for: user})
664 end
665
666 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
667 with {:ok, notification} <- Notification.get(user, id) do
668 conn
669 |> put_view(NotificationView)
670 |> render("show.json", %{notification: notification, for: user})
671 else
672 {:error, reason} ->
673 conn
674 |> put_resp_content_type("application/json")
675 |> send_resp(403, Jason.encode!(%{"error" => reason}))
676 end
677 end
678
679 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
680 Notification.clear(user)
681 json(conn, %{})
682 end
683
684 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
685 with {:ok, _notif} <- Notification.dismiss(user, id) do
686 json(conn, %{})
687 else
688 {:error, reason} ->
689 conn
690 |> put_resp_content_type("application/json")
691 |> send_resp(403, Jason.encode!(%{"error" => reason}))
692 end
693 end
694
695 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
696 Notification.destroy_multiple(user, ids)
697 json(conn, %{})
698 end
699
700 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
701 id = List.wrap(id)
702 q = from(u in User, where: u.id in ^id)
703 targets = Repo.all(q)
704
705 conn
706 |> put_view(AccountView)
707 |> render("relationships.json", %{user: user, targets: targets})
708 end
709
710 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
711 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
712
713 def update_media(%{assigns: %{user: user}} = conn, data) do
714 with %Object{} = object <- Repo.get(Object, data["id"]),
715 true <- Object.authorize_mutation(object, user),
716 true <- is_binary(data["description"]),
717 description <- data["description"] do
718 new_data = %{object.data | "name" => description}
719
720 {:ok, _} =
721 object
722 |> Object.change(%{data: new_data})
723 |> Repo.update()
724
725 attachment_data = Map.put(new_data, "id", object.id)
726
727 conn
728 |> put_view(StatusView)
729 |> render("attachment.json", %{attachment: attachment_data})
730 end
731 end
732
733 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
734 with {:ok, object} <-
735 ActivityPub.upload(
736 file,
737 actor: User.ap_id(user),
738 description: Map.get(data, "description")
739 ) do
740 attachment_data = Map.put(object.data, "id", object.id)
741
742 conn
743 |> put_view(StatusView)
744 |> render("attachment.json", %{attachment: attachment_data})
745 end
746 end
747
748 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
749 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
750 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
751 q = from(u in User, where: u.ap_id in ^likes)
752 users = Repo.all(q)
753
754 conn
755 |> put_view(AccountView)
756 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
757 else
758 _ -> json(conn, [])
759 end
760 end
761
762 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
763 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
764 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
765 q = from(u in User, where: u.ap_id in ^announces)
766 users = Repo.all(q)
767
768 conn
769 |> put_view(AccountView)
770 |> render("accounts.json", %{for: user, users: users, as: :user})
771 else
772 _ -> json(conn, [])
773 end
774 end
775
776 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
777 local_only = params["local"] in [true, "True", "true", "1"]
778
779 tags =
780 [params["tag"], params["any"]]
781 |> List.flatten()
782 |> Enum.uniq()
783 |> Enum.filter(& &1)
784 |> Enum.map(&String.downcase(&1))
785
786 tag_all =
787 params["all"] ||
788 []
789 |> Enum.map(&String.downcase(&1))
790
791 tag_reject =
792 params["none"] ||
793 []
794 |> Enum.map(&String.downcase(&1))
795
796 activities =
797 params
798 |> Map.put("type", "Create")
799 |> Map.put("local_only", local_only)
800 |> Map.put("blocking_user", user)
801 |> Map.put("muting_user", user)
802 |> Map.put("tag", tags)
803 |> Map.put("tag_all", tag_all)
804 |> Map.put("tag_reject", tag_reject)
805 |> ActivityPub.fetch_public_activities()
806 |> Enum.reverse()
807
808 conn
809 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
810 |> put_view(StatusView)
811 |> render("index.json", %{activities: activities, for: user, as: :activity})
812 end
813
814 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
815 with %User{} = user <- User.get_cached_by_id(id),
816 followers <- MastodonAPI.get_followers(user, params) do
817 followers =
818 cond do
819 for_user && user.id == for_user.id -> followers
820 user.info.hide_followers -> []
821 true -> followers
822 end
823
824 conn
825 |> add_link_headers(:followers, followers, user)
826 |> put_view(AccountView)
827 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
828 end
829 end
830
831 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
832 with %User{} = user <- User.get_cached_by_id(id),
833 followers <- MastodonAPI.get_friends(user, params) do
834 followers =
835 cond do
836 for_user && user.id == for_user.id -> followers
837 user.info.hide_follows -> []
838 true -> followers
839 end
840
841 conn
842 |> add_link_headers(:following, followers, user)
843 |> put_view(AccountView)
844 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
845 end
846 end
847
848 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
849 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
850 conn
851 |> put_view(AccountView)
852 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
853 end
854 end
855
856 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
857 with %User{} = follower <- User.get_cached_by_id(id),
858 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
859 conn
860 |> put_view(AccountView)
861 |> render("relationship.json", %{user: followed, target: follower})
862 else
863 {:error, message} ->
864 conn
865 |> put_resp_content_type("application/json")
866 |> send_resp(403, Jason.encode!(%{"error" => message}))
867 end
868 end
869
870 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
871 with %User{} = follower <- User.get_cached_by_id(id),
872 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
873 conn
874 |> put_view(AccountView)
875 |> render("relationship.json", %{user: followed, target: follower})
876 else
877 {:error, message} ->
878 conn
879 |> put_resp_content_type("application/json")
880 |> send_resp(403, Jason.encode!(%{"error" => message}))
881 end
882 end
883
884 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
885 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
886 {_, true} <- {:followed, follower.id != followed.id},
887 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
888 conn
889 |> put_view(AccountView)
890 |> render("relationship.json", %{user: follower, target: followed})
891 else
892 {:followed, _} ->
893 {:error, :not_found}
894
895 {:error, message} ->
896 conn
897 |> put_resp_content_type("application/json")
898 |> send_resp(403, Jason.encode!(%{"error" => message}))
899 end
900 end
901
902 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
903 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
904 {_, true} <- {:followed, follower.id != followed.id},
905 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
906 conn
907 |> put_view(AccountView)
908 |> render("account.json", %{user: followed, for: follower})
909 else
910 {:followed, _} ->
911 {:error, :not_found}
912
913 {:error, message} ->
914 conn
915 |> put_resp_content_type("application/json")
916 |> send_resp(403, Jason.encode!(%{"error" => message}))
917 end
918 end
919
920 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
921 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
922 {_, true} <- {:followed, follower.id != followed.id},
923 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
924 conn
925 |> put_view(AccountView)
926 |> render("relationship.json", %{user: follower, target: followed})
927 else
928 {:followed, _} ->
929 {:error, :not_found}
930
931 error ->
932 error
933 end
934 end
935
936 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
937 with %User{} = muted <- User.get_cached_by_id(id),
938 {:ok, muter} <- User.mute(muter, muted) do
939 conn
940 |> put_view(AccountView)
941 |> render("relationship.json", %{user: muter, target: muted})
942 else
943 {:error, message} ->
944 conn
945 |> put_resp_content_type("application/json")
946 |> send_resp(403, Jason.encode!(%{"error" => message}))
947 end
948 end
949
950 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
951 with %User{} = muted <- User.get_cached_by_id(id),
952 {:ok, muter} <- User.unmute(muter, muted) do
953 conn
954 |> put_view(AccountView)
955 |> render("relationship.json", %{user: muter, target: muted})
956 else
957 {:error, message} ->
958 conn
959 |> put_resp_content_type("application/json")
960 |> send_resp(403, Jason.encode!(%{"error" => message}))
961 end
962 end
963
964 def mutes(%{assigns: %{user: user}} = conn, _) do
965 with muted_accounts <- User.muted_users(user) do
966 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
967 json(conn, res)
968 end
969 end
970
971 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
972 with %User{} = blocked <- User.get_cached_by_id(id),
973 {:ok, blocker} <- User.block(blocker, blocked),
974 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
975 conn
976 |> put_view(AccountView)
977 |> render("relationship.json", %{user: blocker, target: blocked})
978 else
979 {:error, message} ->
980 conn
981 |> put_resp_content_type("application/json")
982 |> send_resp(403, Jason.encode!(%{"error" => message}))
983 end
984 end
985
986 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
987 with %User{} = blocked <- User.get_cached_by_id(id),
988 {:ok, blocker} <- User.unblock(blocker, blocked),
989 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
990 conn
991 |> put_view(AccountView)
992 |> render("relationship.json", %{user: blocker, target: blocked})
993 else
994 {:error, message} ->
995 conn
996 |> put_resp_content_type("application/json")
997 |> send_resp(403, Jason.encode!(%{"error" => message}))
998 end
999 end
1000
1001 def blocks(%{assigns: %{user: user}} = conn, _) do
1002 with blocked_accounts <- User.blocked_users(user) do
1003 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1004 json(conn, res)
1005 end
1006 end
1007
1008 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1009 json(conn, info.domain_blocks || [])
1010 end
1011
1012 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1013 User.block_domain(blocker, domain)
1014 json(conn, %{})
1015 end
1016
1017 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1018 User.unblock_domain(blocker, domain)
1019 json(conn, %{})
1020 end
1021
1022 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1023 with %User{} = subscription_target <- User.get_cached_by_id(id),
1024 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1025 conn
1026 |> put_view(AccountView)
1027 |> render("relationship.json", %{user: user, target: subscription_target})
1028 else
1029 {:error, message} ->
1030 conn
1031 |> put_resp_content_type("application/json")
1032 |> send_resp(403, Jason.encode!(%{"error" => message}))
1033 end
1034 end
1035
1036 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1037 with %User{} = subscription_target <- User.get_cached_by_id(id),
1038 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1039 conn
1040 |> put_view(AccountView)
1041 |> render("relationship.json", %{user: user, target: subscription_target})
1042 else
1043 {:error, message} ->
1044 conn
1045 |> put_resp_content_type("application/json")
1046 |> send_resp(403, Jason.encode!(%{"error" => message}))
1047 end
1048 end
1049
1050 def status_search_query_with_gin(q, query) do
1051 from([a, o] in q,
1052 where:
1053 fragment(
1054 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1055 o.data,
1056 ^query
1057 ),
1058 order_by: [desc: :id]
1059 )
1060 end
1061
1062 def status_search_query_with_rum(q, query) do
1063 from([a, o] in q,
1064 where:
1065 fragment(
1066 "? @@ plainto_tsquery('english', ?)",
1067 o.fts_content,
1068 ^query
1069 ),
1070 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1071 )
1072 end
1073
1074 def status_search(user, query) do
1075 fetched =
1076 if Regex.match?(~r/https?:/, query) do
1077 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1078 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1079 true <- Visibility.visible_for_user?(activity, user) do
1080 [activity]
1081 else
1082 _e -> []
1083 end
1084 end || []
1085
1086 q =
1087 from([a, o] in Activity.with_preloaded_object(Activity),
1088 where: fragment("?->>'type' = 'Create'", a.data),
1089 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1090 limit: 20
1091 )
1092
1093 q =
1094 if Pleroma.Config.get([:database, :rum_enabled]) do
1095 status_search_query_with_rum(q, query)
1096 else
1097 status_search_query_with_gin(q, query)
1098 end
1099
1100 Repo.all(q) ++ fetched
1101 end
1102
1103 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1104 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1105
1106 statuses = status_search(user, query)
1107
1108 tags_path = Web.base_url() <> "/tag/"
1109
1110 tags =
1111 query
1112 |> String.split()
1113 |> Enum.uniq()
1114 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1115 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1116 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1117
1118 res = %{
1119 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1120 "statuses" =>
1121 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1122 "hashtags" => tags
1123 }
1124
1125 json(conn, res)
1126 end
1127
1128 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1129 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1130
1131 statuses = status_search(user, query)
1132
1133 tags =
1134 query
1135 |> String.split()
1136 |> Enum.uniq()
1137 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1138 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1139
1140 res = %{
1141 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1142 "statuses" =>
1143 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1144 "hashtags" => tags
1145 }
1146
1147 json(conn, res)
1148 end
1149
1150 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1151 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1152
1153 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1154
1155 json(conn, res)
1156 end
1157
1158 def favourites(%{assigns: %{user: user}} = conn, params) do
1159 params =
1160 params
1161 |> Map.put("type", "Create")
1162 |> Map.put("favorited_by", user.ap_id)
1163 |> Map.put("blocking_user", user)
1164
1165 activities =
1166 ActivityPub.fetch_activities([], params)
1167 |> Enum.reverse()
1168
1169 conn
1170 |> add_link_headers(:favourites, activities)
1171 |> put_view(StatusView)
1172 |> render("index.json", %{activities: activities, for: user, as: :activity})
1173 end
1174
1175 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1176 with %User{} = user <- User.get_by_id(id),
1177 false <- user.info.hide_favorites do
1178 params =
1179 params
1180 |> Map.put("type", "Create")
1181 |> Map.put("favorited_by", user.ap_id)
1182 |> Map.put("blocking_user", for_user)
1183
1184 recipients =
1185 if for_user do
1186 ["https://www.w3.org/ns/activitystreams#Public"] ++
1187 [for_user.ap_id | for_user.following]
1188 else
1189 ["https://www.w3.org/ns/activitystreams#Public"]
1190 end
1191
1192 activities =
1193 recipients
1194 |> ActivityPub.fetch_activities(params)
1195 |> Enum.reverse()
1196
1197 conn
1198 |> add_link_headers(:favourites, activities)
1199 |> put_view(StatusView)
1200 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1201 else
1202 nil ->
1203 {:error, :not_found}
1204
1205 true ->
1206 conn
1207 |> put_status(403)
1208 |> json(%{error: "Can't get favorites"})
1209 end
1210 end
1211
1212 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1213 user = User.get_cached_by_id(user.id)
1214
1215 bookmarks =
1216 Bookmark.for_user_query(user.id)
1217 |> Pagination.fetch_paginated(params)
1218
1219 activities =
1220 bookmarks
1221 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1222
1223 conn
1224 |> add_link_headers(:bookmarks, bookmarks)
1225 |> put_view(StatusView)
1226 |> render("index.json", %{activities: activities, for: user, as: :activity})
1227 end
1228
1229 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1230 lists = Pleroma.List.for_user(user, opts)
1231 res = ListView.render("lists.json", lists: lists)
1232 json(conn, res)
1233 end
1234
1235 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1236 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1237 res = ListView.render("list.json", list: list)
1238 json(conn, res)
1239 else
1240 _e ->
1241 conn
1242 |> put_status(404)
1243 |> json(%{error: "Record not found"})
1244 end
1245 end
1246
1247 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1248 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1249 res = ListView.render("lists.json", lists: lists)
1250 json(conn, res)
1251 end
1252
1253 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1254 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1255 {:ok, _list} <- Pleroma.List.delete(list) do
1256 json(conn, %{})
1257 else
1258 _e ->
1259 json(conn, "error")
1260 end
1261 end
1262
1263 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1264 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1265 res = ListView.render("list.json", list: list)
1266 json(conn, res)
1267 end
1268 end
1269
1270 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1271 accounts
1272 |> Enum.each(fn account_id ->
1273 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1274 %User{} = followed <- User.get_cached_by_id(account_id) do
1275 Pleroma.List.follow(list, followed)
1276 end
1277 end)
1278
1279 json(conn, %{})
1280 end
1281
1282 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1283 accounts
1284 |> Enum.each(fn account_id ->
1285 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1286 %User{} = followed <- User.get_cached_by_id(account_id) do
1287 Pleroma.List.unfollow(list, followed)
1288 end
1289 end)
1290
1291 json(conn, %{})
1292 end
1293
1294 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1295 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1296 {:ok, users} = Pleroma.List.get_following(list) do
1297 conn
1298 |> put_view(AccountView)
1299 |> render("accounts.json", %{for: user, users: users, as: :user})
1300 end
1301 end
1302
1303 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1304 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1305 {:ok, list} <- Pleroma.List.rename(list, title) do
1306 res = ListView.render("list.json", list: list)
1307 json(conn, res)
1308 else
1309 _e ->
1310 json(conn, "error")
1311 end
1312 end
1313
1314 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1315 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1316 params =
1317 params
1318 |> Map.put("type", "Create")
1319 |> Map.put("blocking_user", user)
1320 |> Map.put("muting_user", user)
1321
1322 # we must filter the following list for the user to avoid leaking statuses the user
1323 # does not actually have permission to see (for more info, peruse security issue #270).
1324 activities =
1325 following
1326 |> Enum.filter(fn x -> x in user.following end)
1327 |> ActivityPub.fetch_activities_bounded(following, params)
1328 |> Enum.reverse()
1329
1330 conn
1331 |> put_view(StatusView)
1332 |> render("index.json", %{activities: activities, for: user, as: :activity})
1333 else
1334 _e ->
1335 conn
1336 |> put_status(403)
1337 |> json(%{error: "Error."})
1338 end
1339 end
1340
1341 def index(%{assigns: %{user: user}} = conn, _params) do
1342 token = get_session(conn, :oauth_token)
1343
1344 if user && token do
1345 mastodon_emoji = mastodonized_emoji()
1346
1347 limit = Config.get([:instance, :limit])
1348
1349 accounts =
1350 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1351
1352 flavour = get_user_flavour(user)
1353
1354 initial_state =
1355 %{
1356 meta: %{
1357 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1358 access_token: token,
1359 locale: "en",
1360 domain: Pleroma.Web.Endpoint.host(),
1361 admin: "1",
1362 me: "#{user.id}",
1363 unfollow_modal: false,
1364 boost_modal: false,
1365 delete_modal: true,
1366 auto_play_gif: false,
1367 display_sensitive_media: false,
1368 reduce_motion: false,
1369 max_toot_chars: limit,
1370 mascot: "/images/pleroma-fox-tan-smol.png"
1371 },
1372 poll_limits: Config.get([:instance, :poll_limits]),
1373 rights: %{
1374 delete_others_notice: present?(user.info.is_moderator),
1375 admin: present?(user.info.is_admin)
1376 },
1377 compose: %{
1378 me: "#{user.id}",
1379 default_privacy: user.info.default_scope,
1380 default_sensitive: false,
1381 allow_content_types: Config.get([:instance, :allowed_post_formats])
1382 },
1383 media_attachments: %{
1384 accept_content_types: [
1385 ".jpg",
1386 ".jpeg",
1387 ".png",
1388 ".gif",
1389 ".webm",
1390 ".mp4",
1391 ".m4v",
1392 "image\/jpeg",
1393 "image\/png",
1394 "image\/gif",
1395 "video\/webm",
1396 "video\/mp4"
1397 ]
1398 },
1399 settings:
1400 user.info.settings ||
1401 %{
1402 onboarded: true,
1403 home: %{
1404 shows: %{
1405 reblog: true,
1406 reply: true
1407 }
1408 },
1409 notifications: %{
1410 alerts: %{
1411 follow: true,
1412 favourite: true,
1413 reblog: true,
1414 mention: true
1415 },
1416 shows: %{
1417 follow: true,
1418 favourite: true,
1419 reblog: true,
1420 mention: true
1421 },
1422 sounds: %{
1423 follow: true,
1424 favourite: true,
1425 reblog: true,
1426 mention: true
1427 }
1428 }
1429 },
1430 push_subscription: nil,
1431 accounts: accounts,
1432 custom_emojis: mastodon_emoji,
1433 char_limit: limit
1434 }
1435 |> Jason.encode!()
1436
1437 conn
1438 |> put_layout(false)
1439 |> put_view(MastodonView)
1440 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1441 else
1442 conn
1443 |> put_session(:return_to, conn.request_path)
1444 |> redirect(to: "/web/login")
1445 end
1446 end
1447
1448 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1449 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1450
1451 with changeset <- Ecto.Changeset.change(user),
1452 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1453 {:ok, _user} <- User.update_and_set_cache(changeset) do
1454 json(conn, %{})
1455 else
1456 e ->
1457 conn
1458 |> put_resp_content_type("application/json")
1459 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1460 end
1461 end
1462
1463 @supported_flavours ["glitch", "vanilla"]
1464
1465 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1466 when flavour in @supported_flavours do
1467 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1468
1469 with changeset <- Ecto.Changeset.change(user),
1470 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1471 {:ok, user} <- User.update_and_set_cache(changeset),
1472 flavour <- user.info.flavour do
1473 json(conn, flavour)
1474 else
1475 e ->
1476 conn
1477 |> put_resp_content_type("application/json")
1478 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1479 end
1480 end
1481
1482 def set_flavour(conn, _params) do
1483 conn
1484 |> put_status(400)
1485 |> json(%{error: "Unsupported flavour"})
1486 end
1487
1488 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1489 json(conn, get_user_flavour(user))
1490 end
1491
1492 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1493 flavour
1494 end
1495
1496 defp get_user_flavour(_) do
1497 "glitch"
1498 end
1499
1500 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1501 redirect(conn, to: local_mastodon_root_path(conn))
1502 end
1503
1504 @doc "Local Mastodon FE login init action"
1505 def login(conn, %{"code" => auth_token}) do
1506 with {:ok, app} <- get_or_make_app(),
1507 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1508 {:ok, token} <- Token.exchange_token(app, auth) do
1509 conn
1510 |> put_session(:oauth_token, token.token)
1511 |> redirect(to: local_mastodon_root_path(conn))
1512 end
1513 end
1514
1515 @doc "Local Mastodon FE callback action"
1516 def login(conn, _) do
1517 with {:ok, app} <- get_or_make_app() do
1518 path =
1519 o_auth_path(
1520 conn,
1521 :authorize,
1522 response_type: "code",
1523 client_id: app.client_id,
1524 redirect_uri: ".",
1525 scope: Enum.join(app.scopes, " ")
1526 )
1527
1528 redirect(conn, to: path)
1529 end
1530 end
1531
1532 defp local_mastodon_root_path(conn) do
1533 case get_session(conn, :return_to) do
1534 nil ->
1535 mastodon_api_path(conn, :index, ["getting-started"])
1536
1537 return_to ->
1538 delete_session(conn, :return_to)
1539 return_to
1540 end
1541 end
1542
1543 defp get_or_make_app do
1544 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1545 scopes = ["read", "write", "follow", "push"]
1546
1547 with %App{} = app <- Repo.get_by(App, find_attrs) do
1548 {:ok, app} =
1549 if app.scopes == scopes do
1550 {:ok, app}
1551 else
1552 app
1553 |> Ecto.Changeset.change(%{scopes: scopes})
1554 |> Repo.update()
1555 end
1556
1557 {:ok, app}
1558 else
1559 _e ->
1560 cs =
1561 App.register_changeset(
1562 %App{},
1563 Map.put(find_attrs, :scopes, scopes)
1564 )
1565
1566 Repo.insert(cs)
1567 end
1568 end
1569
1570 def logout(conn, _) do
1571 conn
1572 |> clear_session
1573 |> redirect(to: "/")
1574 end
1575
1576 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1577 Logger.debug("Unimplemented, returning unmodified relationship")
1578
1579 with %User{} = target <- User.get_cached_by_id(id) do
1580 conn
1581 |> put_view(AccountView)
1582 |> render("relationship.json", %{user: user, target: target})
1583 end
1584 end
1585
1586 def empty_array(conn, _) do
1587 Logger.debug("Unimplemented, returning an empty array")
1588 json(conn, [])
1589 end
1590
1591 def empty_object(conn, _) do
1592 Logger.debug("Unimplemented, returning an empty object")
1593 json(conn, %{})
1594 end
1595
1596 def get_filters(%{assigns: %{user: user}} = conn, _) do
1597 filters = Filter.get_filters(user)
1598 res = FilterView.render("filters.json", filters: filters)
1599 json(conn, res)
1600 end
1601
1602 def create_filter(
1603 %{assigns: %{user: user}} = conn,
1604 %{"phrase" => phrase, "context" => context} = params
1605 ) do
1606 query = %Filter{
1607 user_id: user.id,
1608 phrase: phrase,
1609 context: context,
1610 hide: Map.get(params, "irreversible", false),
1611 whole_word: Map.get(params, "boolean", true)
1612 # expires_at
1613 }
1614
1615 {:ok, response} = Filter.create(query)
1616 res = FilterView.render("filter.json", filter: response)
1617 json(conn, res)
1618 end
1619
1620 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1621 filter = Filter.get(filter_id, user)
1622 res = FilterView.render("filter.json", filter: filter)
1623 json(conn, res)
1624 end
1625
1626 def update_filter(
1627 %{assigns: %{user: user}} = conn,
1628 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1629 ) do
1630 query = %Filter{
1631 user_id: user.id,
1632 filter_id: filter_id,
1633 phrase: phrase,
1634 context: context,
1635 hide: Map.get(params, "irreversible", nil),
1636 whole_word: Map.get(params, "boolean", true)
1637 # expires_at
1638 }
1639
1640 {:ok, response} = Filter.update(query)
1641 res = FilterView.render("filter.json", filter: response)
1642 json(conn, res)
1643 end
1644
1645 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1646 query = %Filter{
1647 user_id: user.id,
1648 filter_id: filter_id
1649 }
1650
1651 {:ok, _} = Filter.delete(query)
1652 json(conn, %{})
1653 end
1654
1655 # fallback action
1656 #
1657 def errors(conn, {:error, %Changeset{} = changeset}) do
1658 error_message =
1659 changeset
1660 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1661 |> Enum.map_join(", ", fn {_k, v} -> v end)
1662
1663 conn
1664 |> put_status(422)
1665 |> json(%{error: error_message})
1666 end
1667
1668 def errors(conn, {:error, :not_found}) do
1669 conn
1670 |> put_status(404)
1671 |> json(%{error: "Record not found"})
1672 end
1673
1674 def errors(conn, _) do
1675 conn
1676 |> put_status(500)
1677 |> json("Something went wrong")
1678 end
1679
1680 def suggestions(%{assigns: %{user: user}} = conn, _) do
1681 suggestions = Config.get(:suggestions)
1682
1683 if Keyword.get(suggestions, :enabled, false) do
1684 api = Keyword.get(suggestions, :third_party_engine, "")
1685 timeout = Keyword.get(suggestions, :timeout, 5000)
1686 limit = Keyword.get(suggestions, :limit, 23)
1687
1688 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1689
1690 user = user.nickname
1691
1692 url =
1693 api
1694 |> String.replace("{{host}}", host)
1695 |> String.replace("{{user}}", user)
1696
1697 with {:ok, %{status: 200, body: body}} <-
1698 @httpoison.get(
1699 url,
1700 [],
1701 adapter: [
1702 recv_timeout: timeout,
1703 pool: :default
1704 ]
1705 ),
1706 {:ok, data} <- Jason.decode(body) do
1707 data =
1708 data
1709 |> Enum.slice(0, limit)
1710 |> Enum.map(fn x ->
1711 Map.put(
1712 x,
1713 "id",
1714 case User.get_or_fetch(x["acct"]) do
1715 {:ok, %User{id: id}} -> id
1716 _ -> 0
1717 end
1718 )
1719 end)
1720 |> Enum.map(fn x ->
1721 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1722 end)
1723 |> Enum.map(fn x ->
1724 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1725 end)
1726
1727 conn
1728 |> json(data)
1729 else
1730 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1731 end
1732 else
1733 json(conn, [])
1734 end
1735 end
1736
1737 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1738 with %Activity{} = activity <- Activity.get_by_id(status_id),
1739 true <- Visibility.visible_for_user?(activity, user) do
1740 data =
1741 StatusView.render(
1742 "card.json",
1743 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1744 )
1745
1746 json(conn, data)
1747 else
1748 _e ->
1749 %{}
1750 end
1751 end
1752
1753 def reports(%{assigns: %{user: user}} = conn, params) do
1754 case CommonAPI.report(user, params) do
1755 {:ok, activity} ->
1756 conn
1757 |> put_view(ReportView)
1758 |> try_render("report.json", %{activity: activity})
1759
1760 {:error, err} ->
1761 conn
1762 |> put_status(:bad_request)
1763 |> json(%{error: err})
1764 end
1765 end
1766
1767 def account_register(
1768 %{assigns: %{app: app}} = conn,
1769 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1770 ) do
1771 params =
1772 params
1773 |> Map.take([
1774 "email",
1775 "captcha_solution",
1776 "captcha_token",
1777 "captcha_answer_data",
1778 "token",
1779 "password"
1780 ])
1781 |> Map.put("nickname", nickname)
1782 |> Map.put("fullname", params["fullname"] || nickname)
1783 |> Map.put("bio", params["bio"] || "")
1784 |> Map.put("confirm", params["password"])
1785
1786 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1787 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1788 json(conn, %{
1789 token_type: "Bearer",
1790 access_token: token.token,
1791 scope: app.scopes,
1792 created_at: Token.Utils.format_created_at(token)
1793 })
1794 else
1795 {:error, errors} ->
1796 conn
1797 |> put_status(400)
1798 |> json(Jason.encode!(errors))
1799 end
1800 end
1801
1802 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1803 conn
1804 |> put_status(400)
1805 |> json(%{error: "Missing parameters"})
1806 end
1807
1808 def account_register(conn, _) do
1809 conn
1810 |> put_status(403)
1811 |> json(%{error: "Invalid credentials"})
1812 end
1813
1814 def conversations(%{assigns: %{user: user}} = conn, params) do
1815 participations = Participation.for_user_with_last_activity_id(user, params)
1816
1817 conversations =
1818 Enum.map(participations, fn participation ->
1819 ConversationView.render("participation.json", %{participation: participation, user: user})
1820 end)
1821
1822 conn
1823 |> add_link_headers(:conversations, participations)
1824 |> json(conversations)
1825 end
1826
1827 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1828 with %Participation{} = participation <-
1829 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1830 {:ok, participation} <- Participation.mark_as_read(participation) do
1831 participation_view =
1832 ConversationView.render("participation.json", %{participation: participation, user: user})
1833
1834 conn
1835 |> json(participation_view)
1836 end
1837 end
1838
1839 def try_render(conn, target, params)
1840 when is_binary(target) do
1841 res = render(conn, target, params)
1842
1843 if res == nil do
1844 conn
1845 |> put_status(501)
1846 |> json(%{error: "Can't display this activity"})
1847 else
1848 res
1849 end
1850 end
1851
1852 def try_render(conn, _, _) do
1853 conn
1854 |> put_status(501)
1855 |> json(%{error: "Can't display this activity"})
1856 end
1857
1858 defp present?(nil), do: false
1859 defp present?(false), do: false
1860 defp present?(_), do: true
1861 end