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