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