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