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