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