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