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