Fix delete activities not federating
[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 alias Pleroma.Activity
8 alias Pleroma.Config
9 alias Pleroma.Filter
10 alias Pleroma.Notification
11 alias Pleroma.Object
12 alias Pleroma.Repo
13 alias Pleroma.Stats
14 alias Pleroma.User
15 alias Pleroma.Web
16 alias Pleroma.Web.CommonAPI
17 alias Pleroma.Web.MediaProxy
18
19 alias Pleroma.Web.MastodonAPI.AccountView
20 alias Pleroma.Web.MastodonAPI.FilterView
21 alias Pleroma.Web.MastodonAPI.ListView
22 alias Pleroma.Web.MastodonAPI.MastodonView
23 alias Pleroma.Web.MastodonAPI.StatusView
24 alias Pleroma.Web.MastodonAPI.ReportView
25 alias Pleroma.Web.ActivityPub.ActivityPub
26 alias Pleroma.Web.ActivityPub.Utils
27 alias Pleroma.Web.ActivityPub.Visibility
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" => id}) do
135 with %User{} = user <- Repo.get(User, 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}) do
656 with %User{} = user <- Repo.get(User, id),
657 {:ok, followers} <- User.get_followers(user) 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 |> put_view(AccountView)
667 |> render("accounts.json", %{users: followers, as: :user})
668 end
669 end
670
671 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
672 with %User{} = user <- Repo.get(User, id),
673 {:ok, followers} <- User.get_friends(user) do
674 followers =
675 cond do
676 for_user && user.id == for_user.id -> followers
677 user.info.hide_follows -> []
678 true -> followers
679 end
680
681 conn
682 |> put_view(AccountView)
683 |> render("accounts.json", %{users: followers, as: :user})
684 end
685 end
686
687 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
688 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
689 conn
690 |> put_view(AccountView)
691 |> render("accounts.json", %{users: follow_requests, as: :user})
692 end
693 end
694
695 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
696 with %User{} = follower <- Repo.get(User, id),
697 {:ok, follower} <- User.maybe_follow(follower, followed),
698 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
699 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
700 {:ok, _activity} <-
701 ActivityPub.accept(%{
702 to: [follower.ap_id],
703 actor: followed,
704 object: follow_activity.data["id"],
705 type: "Accept"
706 }) do
707 conn
708 |> put_view(AccountView)
709 |> render("relationship.json", %{user: followed, target: follower})
710 else
711 {:error, message} ->
712 conn
713 |> put_resp_content_type("application/json")
714 |> send_resp(403, Jason.encode!(%{"error" => message}))
715 end
716 end
717
718 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
719 with %User{} = follower <- Repo.get(User, id),
720 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
721 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
722 {:ok, _activity} <-
723 ActivityPub.reject(%{
724 to: [follower.ap_id],
725 actor: followed,
726 object: follow_activity.data["id"],
727 type: "Reject"
728 }) do
729 conn
730 |> put_view(AccountView)
731 |> render("relationship.json", %{user: followed, target: follower})
732 else
733 {:error, message} ->
734 conn
735 |> put_resp_content_type("application/json")
736 |> send_resp(403, Jason.encode!(%{"error" => message}))
737 end
738 end
739
740 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
741 with %User{} = followed <- Repo.get(User, id),
742 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
743 conn
744 |> put_view(AccountView)
745 |> render("relationship.json", %{user: follower, target: followed})
746 else
747 {:error, message} ->
748 conn
749 |> put_resp_content_type("application/json")
750 |> send_resp(403, Jason.encode!(%{"error" => message}))
751 end
752 end
753
754 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
755 with %User{} = followed <- Repo.get_by(User, nickname: uri),
756 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
757 conn
758 |> put_view(AccountView)
759 |> render("account.json", %{user: followed, for: follower})
760 else
761 {:error, message} ->
762 conn
763 |> put_resp_content_type("application/json")
764 |> send_resp(403, Jason.encode!(%{"error" => message}))
765 end
766 end
767
768 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
769 with %User{} = followed <- Repo.get(User, id),
770 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
771 {:ok, follower, _} <- User.unfollow(follower, followed) do
772 conn
773 |> put_view(AccountView)
774 |> render("relationship.json", %{user: follower, target: followed})
775 end
776 end
777
778 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
779 with %User{} = muted <- Repo.get(User, id),
780 {:ok, muter} <- User.mute(muter, muted) do
781 conn
782 |> put_view(AccountView)
783 |> render("relationship.json", %{user: muter, target: muted})
784 else
785 {:error, message} ->
786 conn
787 |> put_resp_content_type("application/json")
788 |> send_resp(403, Jason.encode!(%{"error" => message}))
789 end
790 end
791
792 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
793 with %User{} = muted <- Repo.get(User, id),
794 {:ok, muter} <- User.unmute(muter, muted) do
795 conn
796 |> put_view(AccountView)
797 |> render("relationship.json", %{user: muter, target: muted})
798 else
799 {:error, message} ->
800 conn
801 |> put_resp_content_type("application/json")
802 |> send_resp(403, Jason.encode!(%{"error" => message}))
803 end
804 end
805
806 def mutes(%{assigns: %{user: user}} = conn, _) do
807 with muted_accounts <- User.muted_users(user) do
808 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
809 json(conn, res)
810 end
811 end
812
813 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
814 with %User{} = blocked <- Repo.get(User, id),
815 {:ok, blocker} <- User.block(blocker, blocked),
816 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
817 conn
818 |> put_view(AccountView)
819 |> render("relationship.json", %{user: blocker, target: blocked})
820 else
821 {:error, message} ->
822 conn
823 |> put_resp_content_type("application/json")
824 |> send_resp(403, Jason.encode!(%{"error" => message}))
825 end
826 end
827
828 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
829 with %User{} = blocked <- Repo.get(User, id),
830 {:ok, blocker} <- User.unblock(blocker, blocked),
831 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
832 conn
833 |> put_view(AccountView)
834 |> render("relationship.json", %{user: blocker, target: blocked})
835 else
836 {:error, message} ->
837 conn
838 |> put_resp_content_type("application/json")
839 |> send_resp(403, Jason.encode!(%{"error" => message}))
840 end
841 end
842
843 def blocks(%{assigns: %{user: user}} = conn, _) do
844 with blocked_accounts <- User.blocked_users(user) do
845 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
846 json(conn, res)
847 end
848 end
849
850 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
851 json(conn, info.domain_blocks || [])
852 end
853
854 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
855 User.block_domain(blocker, domain)
856 json(conn, %{})
857 end
858
859 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
860 User.unblock_domain(blocker, domain)
861 json(conn, %{})
862 end
863
864 def status_search(user, query) do
865 fetched =
866 if Regex.match?(~r/https?:/, query) do
867 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
868 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
869 true <- Visibility.visible_for_user?(activity, user) do
870 [activity]
871 else
872 _e -> []
873 end
874 end || []
875
876 q =
877 from(
878 a in Activity,
879 where: fragment("?->>'type' = 'Create'", a.data),
880 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
881 where:
882 fragment(
883 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
884 a.data,
885 ^query
886 ),
887 limit: 20,
888 order_by: [desc: :id]
889 )
890
891 Repo.all(q) ++ fetched
892 end
893
894 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
895 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
896
897 statuses = status_search(user, query)
898
899 tags_path = Web.base_url() <> "/tag/"
900
901 tags =
902 query
903 |> String.split()
904 |> Enum.uniq()
905 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
906 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
907 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
908
909 res = %{
910 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
911 "statuses" =>
912 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
913 "hashtags" => tags
914 }
915
916 json(conn, res)
917 end
918
919 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
920 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
921
922 statuses = status_search(user, query)
923
924 tags =
925 query
926 |> String.split()
927 |> Enum.uniq()
928 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
929 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
930
931 res = %{
932 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
933 "statuses" =>
934 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
935 "hashtags" => tags
936 }
937
938 json(conn, res)
939 end
940
941 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
942 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
943
944 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
945
946 json(conn, res)
947 end
948
949 def favourites(%{assigns: %{user: user}} = conn, params) do
950 activities =
951 params
952 |> Map.put("type", "Create")
953 |> Map.put("favorited_by", user.ap_id)
954 |> Map.put("blocking_user", user)
955 |> ActivityPub.fetch_public_activities()
956 |> Enum.reverse()
957
958 conn
959 |> add_link_headers(:favourites, activities)
960 |> put_view(StatusView)
961 |> render("index.json", %{activities: activities, for: user, as: :activity})
962 end
963
964 def bookmarks(%{assigns: %{user: user}} = conn, _) do
965 user = Repo.get(User, user.id)
966
967 activities =
968 user.bookmarks
969 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
970 |> Enum.reverse()
971
972 conn
973 |> put_view(StatusView)
974 |> render("index.json", %{activities: activities, for: user, as: :activity})
975 end
976
977 def get_lists(%{assigns: %{user: user}} = conn, opts) do
978 lists = Pleroma.List.for_user(user, opts)
979 res = ListView.render("lists.json", lists: lists)
980 json(conn, res)
981 end
982
983 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
984 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
985 res = ListView.render("list.json", list: list)
986 json(conn, res)
987 else
988 _e ->
989 conn
990 |> put_status(404)
991 |> json(%{error: "Record not found"})
992 end
993 end
994
995 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
996 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
997 res = ListView.render("lists.json", lists: lists)
998 json(conn, res)
999 end
1000
1001 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1002 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1003 {:ok, _list} <- Pleroma.List.delete(list) do
1004 json(conn, %{})
1005 else
1006 _e ->
1007 json(conn, "error")
1008 end
1009 end
1010
1011 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1012 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1013 res = ListView.render("list.json", list: list)
1014 json(conn, res)
1015 end
1016 end
1017
1018 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1019 accounts
1020 |> Enum.each(fn account_id ->
1021 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1022 %User{} = followed <- Repo.get(User, account_id) do
1023 Pleroma.List.follow(list, followed)
1024 end
1025 end)
1026
1027 json(conn, %{})
1028 end
1029
1030 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1031 accounts
1032 |> Enum.each(fn account_id ->
1033 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1034 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1035 Pleroma.List.unfollow(list, followed)
1036 end
1037 end)
1038
1039 json(conn, %{})
1040 end
1041
1042 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1043 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1044 {:ok, users} = Pleroma.List.get_following(list) do
1045 conn
1046 |> put_view(AccountView)
1047 |> render("accounts.json", %{users: users, as: :user})
1048 end
1049 end
1050
1051 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1052 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1053 {:ok, list} <- Pleroma.List.rename(list, title) do
1054 res = ListView.render("list.json", list: list)
1055 json(conn, res)
1056 else
1057 _e ->
1058 json(conn, "error")
1059 end
1060 end
1061
1062 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1063 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1064 params =
1065 params
1066 |> Map.put("type", "Create")
1067 |> Map.put("blocking_user", user)
1068 |> Map.put("muting_user", user)
1069
1070 # we must filter the following list for the user to avoid leaking statuses the user
1071 # does not actually have permission to see (for more info, peruse security issue #270).
1072 activities =
1073 following
1074 |> Enum.filter(fn x -> x in user.following end)
1075 |> ActivityPub.fetch_activities_bounded(following, params)
1076 |> Enum.reverse()
1077
1078 conn
1079 |> put_view(StatusView)
1080 |> render("index.json", %{activities: activities, for: user, as: :activity})
1081 else
1082 _e ->
1083 conn
1084 |> put_status(403)
1085 |> json(%{error: "Error."})
1086 end
1087 end
1088
1089 def index(%{assigns: %{user: user}} = conn, _params) do
1090 token =
1091 conn
1092 |> get_session(:oauth_token)
1093
1094 if user && token do
1095 mastodon_emoji = mastodonized_emoji()
1096
1097 limit = Config.get([:instance, :limit])
1098
1099 accounts =
1100 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1101
1102 flavour = get_user_flavour(user)
1103
1104 initial_state =
1105 %{
1106 meta: %{
1107 streaming_api_base_url:
1108 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1109 access_token: token,
1110 locale: "en",
1111 domain: Pleroma.Web.Endpoint.host(),
1112 admin: "1",
1113 me: "#{user.id}",
1114 unfollow_modal: false,
1115 boost_modal: false,
1116 delete_modal: true,
1117 auto_play_gif: false,
1118 display_sensitive_media: false,
1119 reduce_motion: false,
1120 max_toot_chars: limit
1121 },
1122 rights: %{
1123 delete_others_notice: present?(user.info.is_moderator),
1124 admin: present?(user.info.is_admin)
1125 },
1126 compose: %{
1127 me: "#{user.id}",
1128 default_privacy: user.info.default_scope,
1129 default_sensitive: false
1130 },
1131 media_attachments: %{
1132 accept_content_types: [
1133 ".jpg",
1134 ".jpeg",
1135 ".png",
1136 ".gif",
1137 ".webm",
1138 ".mp4",
1139 ".m4v",
1140 "image\/jpeg",
1141 "image\/png",
1142 "image\/gif",
1143 "video\/webm",
1144 "video\/mp4"
1145 ]
1146 },
1147 settings:
1148 user.info.settings ||
1149 %{
1150 onboarded: true,
1151 home: %{
1152 shows: %{
1153 reblog: true,
1154 reply: true
1155 }
1156 },
1157 notifications: %{
1158 alerts: %{
1159 follow: true,
1160 favourite: true,
1161 reblog: true,
1162 mention: true
1163 },
1164 shows: %{
1165 follow: true,
1166 favourite: true,
1167 reblog: true,
1168 mention: true
1169 },
1170 sounds: %{
1171 follow: true,
1172 favourite: true,
1173 reblog: true,
1174 mention: true
1175 }
1176 }
1177 },
1178 push_subscription: nil,
1179 accounts: accounts,
1180 custom_emojis: mastodon_emoji,
1181 char_limit: limit
1182 }
1183 |> Jason.encode!()
1184
1185 conn
1186 |> put_layout(false)
1187 |> put_view(MastodonView)
1188 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1189 else
1190 conn
1191 |> redirect(to: "/web/login")
1192 end
1193 end
1194
1195 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1196 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1197
1198 with changeset <- Ecto.Changeset.change(user),
1199 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1200 {:ok, _user} <- User.update_and_set_cache(changeset) do
1201 json(conn, %{})
1202 else
1203 e ->
1204 conn
1205 |> put_resp_content_type("application/json")
1206 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1207 end
1208 end
1209
1210 @supported_flavours ["glitch", "vanilla"]
1211
1212 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1213 when flavour in @supported_flavours do
1214 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1215
1216 with changeset <- Ecto.Changeset.change(user),
1217 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1218 {:ok, user} <- User.update_and_set_cache(changeset),
1219 flavour <- user.info.flavour do
1220 json(conn, flavour)
1221 else
1222 e ->
1223 conn
1224 |> put_resp_content_type("application/json")
1225 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1226 end
1227 end
1228
1229 def set_flavour(conn, _params) do
1230 conn
1231 |> put_status(400)
1232 |> json(%{error: "Unsupported flavour"})
1233 end
1234
1235 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1236 json(conn, get_user_flavour(user))
1237 end
1238
1239 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1240 flavour
1241 end
1242
1243 defp get_user_flavour(_) do
1244 "glitch"
1245 end
1246
1247 def login(conn, %{"code" => code}) do
1248 with {:ok, app} <- get_or_make_app(),
1249 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1250 {:ok, token} <- Token.exchange_token(app, auth) do
1251 conn
1252 |> put_session(:oauth_token, token.token)
1253 |> redirect(to: "/web/getting-started")
1254 end
1255 end
1256
1257 def login(conn, _) do
1258 with {:ok, app} <- get_or_make_app() do
1259 path =
1260 o_auth_path(
1261 conn,
1262 :authorize,
1263 response_type: "code",
1264 client_id: app.client_id,
1265 redirect_uri: ".",
1266 scope: Enum.join(app.scopes, " ")
1267 )
1268
1269 conn
1270 |> redirect(to: path)
1271 end
1272 end
1273
1274 defp get_or_make_app() do
1275 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1276 scopes = ["read", "write", "follow", "push"]
1277
1278 with %App{} = app <- Repo.get_by(App, find_attrs) do
1279 {:ok, app} =
1280 if app.scopes == scopes do
1281 {:ok, app}
1282 else
1283 app
1284 |> Ecto.Changeset.change(%{scopes: scopes})
1285 |> Repo.update()
1286 end
1287
1288 {:ok, app}
1289 else
1290 _e ->
1291 cs =
1292 App.register_changeset(
1293 %App{},
1294 Map.put(find_attrs, :scopes, scopes)
1295 )
1296
1297 Repo.insert(cs)
1298 end
1299 end
1300
1301 def logout(conn, _) do
1302 conn
1303 |> clear_session
1304 |> redirect(to: "/")
1305 end
1306
1307 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1308 Logger.debug("Unimplemented, returning unmodified relationship")
1309
1310 with %User{} = target <- Repo.get(User, id) do
1311 conn
1312 |> put_view(AccountView)
1313 |> render("relationship.json", %{user: user, target: target})
1314 end
1315 end
1316
1317 def empty_array(conn, _) do
1318 Logger.debug("Unimplemented, returning an empty array")
1319 json(conn, [])
1320 end
1321
1322 def empty_object(conn, _) do
1323 Logger.debug("Unimplemented, returning an empty object")
1324 json(conn, %{})
1325 end
1326
1327 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1328 actor = User.get_cached_by_ap_id(activity.data["actor"])
1329 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1330 mastodon_type = Activity.mastodon_notification_type(activity)
1331
1332 response = %{
1333 id: to_string(id),
1334 type: mastodon_type,
1335 created_at: CommonAPI.Utils.to_masto_date(created_at),
1336 account: AccountView.render("account.json", %{user: actor, for: user})
1337 }
1338
1339 case mastodon_type do
1340 "mention" ->
1341 response
1342 |> Map.merge(%{
1343 status: StatusView.render("status.json", %{activity: activity, for: user})
1344 })
1345
1346 "favourite" ->
1347 response
1348 |> Map.merge(%{
1349 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1350 })
1351
1352 "reblog" ->
1353 response
1354 |> Map.merge(%{
1355 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1356 })
1357
1358 "follow" ->
1359 response
1360
1361 _ ->
1362 nil
1363 end
1364 end
1365
1366 def get_filters(%{assigns: %{user: user}} = conn, _) do
1367 filters = Filter.get_filters(user)
1368 res = FilterView.render("filters.json", filters: filters)
1369 json(conn, res)
1370 end
1371
1372 def create_filter(
1373 %{assigns: %{user: user}} = conn,
1374 %{"phrase" => phrase, "context" => context} = params
1375 ) do
1376 query = %Filter{
1377 user_id: user.id,
1378 phrase: phrase,
1379 context: context,
1380 hide: Map.get(params, "irreversible", nil),
1381 whole_word: Map.get(params, "boolean", true)
1382 # expires_at
1383 }
1384
1385 {:ok, response} = Filter.create(query)
1386 res = FilterView.render("filter.json", filter: response)
1387 json(conn, res)
1388 end
1389
1390 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1391 filter = Filter.get(filter_id, user)
1392 res = FilterView.render("filter.json", filter: filter)
1393 json(conn, res)
1394 end
1395
1396 def update_filter(
1397 %{assigns: %{user: user}} = conn,
1398 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1399 ) do
1400 query = %Filter{
1401 user_id: user.id,
1402 filter_id: filter_id,
1403 phrase: phrase,
1404 context: context,
1405 hide: Map.get(params, "irreversible", nil),
1406 whole_word: Map.get(params, "boolean", true)
1407 # expires_at
1408 }
1409
1410 {:ok, response} = Filter.update(query)
1411 res = FilterView.render("filter.json", filter: response)
1412 json(conn, res)
1413 end
1414
1415 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1416 query = %Filter{
1417 user_id: user.id,
1418 filter_id: filter_id
1419 }
1420
1421 {:ok, _} = Filter.delete(query)
1422 json(conn, %{})
1423 end
1424
1425 # fallback action
1426 #
1427 def errors(conn, _) do
1428 conn
1429 |> put_status(500)
1430 |> json("Something went wrong")
1431 end
1432
1433 def suggestions(%{assigns: %{user: user}} = conn, _) do
1434 suggestions = Config.get(:suggestions)
1435
1436 if Keyword.get(suggestions, :enabled, false) do
1437 api = Keyword.get(suggestions, :third_party_engine, "")
1438 timeout = Keyword.get(suggestions, :timeout, 5000)
1439 limit = Keyword.get(suggestions, :limit, 23)
1440
1441 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1442
1443 user = user.nickname
1444
1445 url =
1446 api
1447 |> String.replace("{{host}}", host)
1448 |> String.replace("{{user}}", user)
1449
1450 with {:ok, %{status: 200, body: body}} <-
1451 @httpoison.get(
1452 url,
1453 [],
1454 adapter: [
1455 recv_timeout: timeout,
1456 pool: :default
1457 ]
1458 ),
1459 {:ok, data} <- Jason.decode(body) do
1460 data =
1461 data
1462 |> Enum.slice(0, limit)
1463 |> Enum.map(fn x ->
1464 Map.put(
1465 x,
1466 "id",
1467 case User.get_or_fetch(x["acct"]) do
1468 %{id: id} -> id
1469 _ -> 0
1470 end
1471 )
1472 end)
1473 |> Enum.map(fn x ->
1474 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1475 end)
1476 |> Enum.map(fn x ->
1477 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1478 end)
1479
1480 conn
1481 |> json(data)
1482 else
1483 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1484 end
1485 else
1486 json(conn, [])
1487 end
1488 end
1489
1490 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1491 with %Activity{} = activity <- Repo.get(Activity, status_id),
1492 true <- Visibility.visible_for_user?(activity, user) do
1493 data =
1494 StatusView.render(
1495 "card.json",
1496 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1497 )
1498
1499 json(conn, data)
1500 else
1501 _e ->
1502 %{}
1503 end
1504 end
1505
1506 def reports(%{assigns: %{user: user}} = conn, params) do
1507 case CommonAPI.report(user, params) do
1508 {:ok, activity} ->
1509 conn
1510 |> put_view(ReportView)
1511 |> try_render("report.json", %{activity: activity})
1512
1513 {:error, err} ->
1514 conn
1515 |> put_status(:bad_request)
1516 |> json(%{error: err})
1517 end
1518 end
1519
1520 def try_render(conn, target, params)
1521 when is_binary(target) do
1522 res = render(conn, target, params)
1523
1524 if res == nil do
1525 conn
1526 |> put_status(501)
1527 |> json(%{error: "Can't display this activity"})
1528 else
1529 res
1530 end
1531 end
1532
1533 def try_render(conn, _, _) do
1534 conn
1535 |> put_status(501)
1536 |> json(%{error: "Can't display this activity"})
1537 end
1538
1539 defp present?(nil), do: false
1540 defp present?(false), do: false
1541 defp present?(_), do: true
1542 end