Merge branch 'hotfix/delete-activities' into 'develop'
[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 allow_content_types: Config.get([:instance, :allowed_post_formats])
1115 },
1116 media_attachments: %{
1117 accept_content_types: [
1118 ".jpg",
1119 ".jpeg",
1120 ".png",
1121 ".gif",
1122 ".webm",
1123 ".mp4",
1124 ".m4v",
1125 "image\/jpeg",
1126 "image\/png",
1127 "image\/gif",
1128 "video\/webm",
1129 "video\/mp4"
1130 ]
1131 },
1132 settings:
1133 user.info.settings ||
1134 %{
1135 onboarded: true,
1136 home: %{
1137 shows: %{
1138 reblog: true,
1139 reply: true
1140 }
1141 },
1142 notifications: %{
1143 alerts: %{
1144 follow: true,
1145 favourite: true,
1146 reblog: true,
1147 mention: true
1148 },
1149 shows: %{
1150 follow: true,
1151 favourite: true,
1152 reblog: true,
1153 mention: true
1154 },
1155 sounds: %{
1156 follow: true,
1157 favourite: true,
1158 reblog: true,
1159 mention: true
1160 }
1161 }
1162 },
1163 push_subscription: nil,
1164 accounts: accounts,
1165 custom_emojis: mastodon_emoji,
1166 char_limit: limit
1167 }
1168 |> Jason.encode!()
1169
1170 conn
1171 |> put_layout(false)
1172 |> put_view(MastodonView)
1173 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1174 else
1175 conn
1176 |> redirect(to: "/web/login")
1177 end
1178 end
1179
1180 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1181 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1182
1183 with changeset <- Ecto.Changeset.change(user),
1184 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1185 {:ok, _user} <- User.update_and_set_cache(changeset) do
1186 json(conn, %{})
1187 else
1188 e ->
1189 conn
1190 |> put_resp_content_type("application/json")
1191 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1192 end
1193 end
1194
1195 @supported_flavours ["glitch", "vanilla"]
1196
1197 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1198 when flavour in @supported_flavours do
1199 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1200
1201 with changeset <- Ecto.Changeset.change(user),
1202 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1203 {:ok, user} <- User.update_and_set_cache(changeset),
1204 flavour <- user.info.flavour do
1205 json(conn, flavour)
1206 else
1207 e ->
1208 conn
1209 |> put_resp_content_type("application/json")
1210 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1211 end
1212 end
1213
1214 def set_flavour(conn, _params) do
1215 conn
1216 |> put_status(400)
1217 |> json(%{error: "Unsupported flavour"})
1218 end
1219
1220 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1221 json(conn, get_user_flavour(user))
1222 end
1223
1224 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1225 flavour
1226 end
1227
1228 defp get_user_flavour(_) do
1229 "glitch"
1230 end
1231
1232 def login(conn, %{"code" => code}) do
1233 with {:ok, app} <- get_or_make_app(),
1234 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1235 {:ok, token} <- Token.exchange_token(app, auth) do
1236 conn
1237 |> put_session(:oauth_token, token.token)
1238 |> redirect(to: "/web/getting-started")
1239 end
1240 end
1241
1242 def login(conn, _) do
1243 with {:ok, app} <- get_or_make_app() do
1244 path =
1245 o_auth_path(
1246 conn,
1247 :authorize,
1248 response_type: "code",
1249 client_id: app.client_id,
1250 redirect_uri: ".",
1251 scope: Enum.join(app.scopes, " ")
1252 )
1253
1254 conn
1255 |> redirect(to: path)
1256 end
1257 end
1258
1259 defp get_or_make_app do
1260 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1261 scopes = ["read", "write", "follow", "push"]
1262
1263 with %App{} = app <- Repo.get_by(App, find_attrs) do
1264 {:ok, app} =
1265 if app.scopes == scopes do
1266 {:ok, app}
1267 else
1268 app
1269 |> Ecto.Changeset.change(%{scopes: scopes})
1270 |> Repo.update()
1271 end
1272
1273 {:ok, app}
1274 else
1275 _e ->
1276 cs =
1277 App.register_changeset(
1278 %App{},
1279 Map.put(find_attrs, :scopes, scopes)
1280 )
1281
1282 Repo.insert(cs)
1283 end
1284 end
1285
1286 def logout(conn, _) do
1287 conn
1288 |> clear_session
1289 |> redirect(to: "/")
1290 end
1291
1292 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1293 Logger.debug("Unimplemented, returning unmodified relationship")
1294
1295 with %User{} = target <- Repo.get(User, id) do
1296 conn
1297 |> put_view(AccountView)
1298 |> render("relationship.json", %{user: user, target: target})
1299 end
1300 end
1301
1302 def empty_array(conn, _) do
1303 Logger.debug("Unimplemented, returning an empty array")
1304 json(conn, [])
1305 end
1306
1307 def empty_object(conn, _) do
1308 Logger.debug("Unimplemented, returning an empty object")
1309 json(conn, %{})
1310 end
1311
1312 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1313 actor = User.get_cached_by_ap_id(activity.data["actor"])
1314 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1315 mastodon_type = Activity.mastodon_notification_type(activity)
1316
1317 response = %{
1318 id: to_string(id),
1319 type: mastodon_type,
1320 created_at: CommonAPI.Utils.to_masto_date(created_at),
1321 account: AccountView.render("account.json", %{user: actor, for: user})
1322 }
1323
1324 case mastodon_type do
1325 "mention" ->
1326 response
1327 |> Map.merge(%{
1328 status: StatusView.render("status.json", %{activity: activity, for: user})
1329 })
1330
1331 "favourite" ->
1332 response
1333 |> Map.merge(%{
1334 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1335 })
1336
1337 "reblog" ->
1338 response
1339 |> Map.merge(%{
1340 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1341 })
1342
1343 "follow" ->
1344 response
1345
1346 _ ->
1347 nil
1348 end
1349 end
1350
1351 def get_filters(%{assigns: %{user: user}} = conn, _) do
1352 filters = Filter.get_filters(user)
1353 res = FilterView.render("filters.json", filters: filters)
1354 json(conn, res)
1355 end
1356
1357 def create_filter(
1358 %{assigns: %{user: user}} = conn,
1359 %{"phrase" => phrase, "context" => context} = params
1360 ) do
1361 query = %Filter{
1362 user_id: user.id,
1363 phrase: phrase,
1364 context: context,
1365 hide: Map.get(params, "irreversible", nil),
1366 whole_word: Map.get(params, "boolean", true)
1367 # expires_at
1368 }
1369
1370 {:ok, response} = Filter.create(query)
1371 res = FilterView.render("filter.json", filter: response)
1372 json(conn, res)
1373 end
1374
1375 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1376 filter = Filter.get(filter_id, user)
1377 res = FilterView.render("filter.json", filter: filter)
1378 json(conn, res)
1379 end
1380
1381 def update_filter(
1382 %{assigns: %{user: user}} = conn,
1383 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1384 ) do
1385 query = %Filter{
1386 user_id: user.id,
1387 filter_id: filter_id,
1388 phrase: phrase,
1389 context: context,
1390 hide: Map.get(params, "irreversible", nil),
1391 whole_word: Map.get(params, "boolean", true)
1392 # expires_at
1393 }
1394
1395 {:ok, response} = Filter.update(query)
1396 res = FilterView.render("filter.json", filter: response)
1397 json(conn, res)
1398 end
1399
1400 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1401 query = %Filter{
1402 user_id: user.id,
1403 filter_id: filter_id
1404 }
1405
1406 {:ok, _} = Filter.delete(query)
1407 json(conn, %{})
1408 end
1409
1410 # fallback action
1411 #
1412 def errors(conn, _) do
1413 conn
1414 |> put_status(500)
1415 |> json("Something went wrong")
1416 end
1417
1418 def suggestions(%{assigns: %{user: user}} = conn, _) do
1419 suggestions = Config.get(:suggestions)
1420
1421 if Keyword.get(suggestions, :enabled, false) do
1422 api = Keyword.get(suggestions, :third_party_engine, "")
1423 timeout = Keyword.get(suggestions, :timeout, 5000)
1424 limit = Keyword.get(suggestions, :limit, 23)
1425
1426 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1427
1428 user = user.nickname
1429
1430 url =
1431 api
1432 |> String.replace("{{host}}", host)
1433 |> String.replace("{{user}}", user)
1434
1435 with {:ok, %{status: 200, body: body}} <-
1436 @httpoison.get(
1437 url,
1438 [],
1439 adapter: [
1440 recv_timeout: timeout,
1441 pool: :default
1442 ]
1443 ),
1444 {:ok, data} <- Jason.decode(body) do
1445 data =
1446 data
1447 |> Enum.slice(0, limit)
1448 |> Enum.map(fn x ->
1449 Map.put(
1450 x,
1451 "id",
1452 case User.get_or_fetch(x["acct"]) do
1453 %{id: id} -> id
1454 _ -> 0
1455 end
1456 )
1457 end)
1458 |> Enum.map(fn x ->
1459 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1460 end)
1461 |> Enum.map(fn x ->
1462 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1463 end)
1464
1465 conn
1466 |> json(data)
1467 else
1468 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1469 end
1470 else
1471 json(conn, [])
1472 end
1473 end
1474
1475 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1476 with %Activity{} = activity <- Repo.get(Activity, status_id),
1477 true <- Visibility.visible_for_user?(activity, user) do
1478 data =
1479 StatusView.render(
1480 "card.json",
1481 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1482 )
1483
1484 json(conn, data)
1485 else
1486 _e ->
1487 %{}
1488 end
1489 end
1490
1491 def reports(%{assigns: %{user: user}} = conn, params) do
1492 case CommonAPI.report(user, params) do
1493 {:ok, activity} ->
1494 conn
1495 |> put_view(ReportView)
1496 |> try_render("report.json", %{activity: activity})
1497
1498 {:error, err} ->
1499 conn
1500 |> put_status(:bad_request)
1501 |> json(%{error: err})
1502 end
1503 end
1504
1505 def try_render(conn, target, params)
1506 when is_binary(target) do
1507 res = render(conn, target, params)
1508
1509 if res == nil do
1510 conn
1511 |> put_status(501)
1512 |> json(%{error: "Can't display this activity"})
1513 else
1514 res
1515 end
1516 end
1517
1518 def try_render(conn, _, _) do
1519 conn
1520 |> put_status(501)
1521 |> json(%{error: "Can't display this activity"})
1522 end
1523
1524 defp present?(nil), do: false
1525 defp present?(false), do: false
1526 defp present?(_), do: true
1527 end