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