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