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