Change relationship direction of subscriptions
[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 subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
867 with %User{} = subscription_target <- User.get_by_id(id) do
868 {:ok, subscription_target} = User.subscribe(user, subscription_target)
869
870 conn
871 |> put_view(AccountView)
872 |> render("relationship.json", %{user: user, target: subscription_target})
873 end
874 end
875
876 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
877 with %User{} = subscription_target <- User.get_by_id(id) do
878 {:ok, subscription_target} = User.unsubscribe(user, subscription_target)
879
880 conn
881 |> put_view(AccountView)
882 |> render("relationship.json", %{user: user, target: subscription_target})
883 end
884 end
885
886 def status_search(user, query) do
887 fetched =
888 if Regex.match?(~r/https?:/, query) do
889 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
890 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
891 true <- Visibility.visible_for_user?(activity, user) do
892 [activity]
893 else
894 _e -> []
895 end
896 end || []
897
898 q =
899 from(
900 a in Activity,
901 where: fragment("?->>'type' = 'Create'", a.data),
902 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
903 where:
904 fragment(
905 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
906 a.data,
907 ^query
908 ),
909 limit: 20,
910 order_by: [desc: :id]
911 )
912
913 Repo.all(q) ++ fetched
914 end
915
916 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
917 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
918
919 statuses = status_search(user, query)
920
921 tags_path = Web.base_url() <> "/tag/"
922
923 tags =
924 query
925 |> String.split()
926 |> Enum.uniq()
927 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
928 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
929 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} 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 search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
942 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
943
944 statuses = status_search(user, query)
945
946 tags =
947 query
948 |> String.split()
949 |> Enum.uniq()
950 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
951 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
952
953 res = %{
954 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
955 "statuses" =>
956 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
957 "hashtags" => tags
958 }
959
960 json(conn, res)
961 end
962
963 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
964 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
965
966 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
967
968 json(conn, res)
969 end
970
971 def favourites(%{assigns: %{user: user}} = conn, params) do
972 params =
973 params
974 |> Map.put("type", "Create")
975 |> Map.put("favorited_by", user.ap_id)
976 |> Map.put("blocking_user", user)
977
978 activities =
979 ActivityPub.fetch_activities([], params)
980 |> Enum.reverse()
981
982 conn
983 |> add_link_headers(:favourites, activities)
984 |> put_view(StatusView)
985 |> render("index.json", %{activities: activities, for: user, as: :activity})
986 end
987
988 def bookmarks(%{assigns: %{user: user}} = conn, _) do
989 user = User.get_by_id(user.id)
990
991 activities =
992 user.bookmarks
993 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
994 |> Enum.reverse()
995
996 conn
997 |> put_view(StatusView)
998 |> render("index.json", %{activities: activities, for: user, as: :activity})
999 end
1000
1001 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1002 lists = Pleroma.List.for_user(user, opts)
1003 res = ListView.render("lists.json", lists: lists)
1004 json(conn, res)
1005 end
1006
1007 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1008 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1009 res = ListView.render("list.json", list: list)
1010 json(conn, res)
1011 else
1012 _e ->
1013 conn
1014 |> put_status(404)
1015 |> json(%{error: "Record not found"})
1016 end
1017 end
1018
1019 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1020 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1021 res = ListView.render("lists.json", lists: lists)
1022 json(conn, res)
1023 end
1024
1025 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1026 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1027 {:ok, _list} <- Pleroma.List.delete(list) do
1028 json(conn, %{})
1029 else
1030 _e ->
1031 json(conn, "error")
1032 end
1033 end
1034
1035 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1036 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1037 res = ListView.render("list.json", list: list)
1038 json(conn, res)
1039 end
1040 end
1041
1042 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1043 accounts
1044 |> Enum.each(fn account_id ->
1045 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1046 %User{} = followed <- User.get_by_id(account_id) do
1047 Pleroma.List.follow(list, followed)
1048 end
1049 end)
1050
1051 json(conn, %{})
1052 end
1053
1054 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1055 accounts
1056 |> Enum.each(fn account_id ->
1057 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1058 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1059 Pleroma.List.unfollow(list, followed)
1060 end
1061 end)
1062
1063 json(conn, %{})
1064 end
1065
1066 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1067 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1068 {:ok, users} = Pleroma.List.get_following(list) do
1069 conn
1070 |> put_view(AccountView)
1071 |> render("accounts.json", %{users: users, as: :user})
1072 end
1073 end
1074
1075 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1076 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1077 {:ok, list} <- Pleroma.List.rename(list, title) do
1078 res = ListView.render("list.json", list: list)
1079 json(conn, res)
1080 else
1081 _e ->
1082 json(conn, "error")
1083 end
1084 end
1085
1086 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1087 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1088 params =
1089 params
1090 |> Map.put("type", "Create")
1091 |> Map.put("blocking_user", user)
1092 |> Map.put("muting_user", user)
1093
1094 # we must filter the following list for the user to avoid leaking statuses the user
1095 # does not actually have permission to see (for more info, peruse security issue #270).
1096 activities =
1097 following
1098 |> Enum.filter(fn x -> x in user.following end)
1099 |> ActivityPub.fetch_activities_bounded(following, params)
1100 |> Enum.reverse()
1101
1102 conn
1103 |> put_view(StatusView)
1104 |> render("index.json", %{activities: activities, for: user, as: :activity})
1105 else
1106 _e ->
1107 conn
1108 |> put_status(403)
1109 |> json(%{error: "Error."})
1110 end
1111 end
1112
1113 def index(%{assigns: %{user: user}} = conn, _params) do
1114 token =
1115 conn
1116 |> get_session(:oauth_token)
1117
1118 if user && token do
1119 mastodon_emoji = mastodonized_emoji()
1120
1121 limit = Config.get([:instance, :limit])
1122
1123 accounts =
1124 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1125
1126 flavour = get_user_flavour(user)
1127
1128 initial_state =
1129 %{
1130 meta: %{
1131 streaming_api_base_url:
1132 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1133 access_token: token,
1134 locale: "en",
1135 domain: Pleroma.Web.Endpoint.host(),
1136 admin: "1",
1137 me: "#{user.id}",
1138 unfollow_modal: false,
1139 boost_modal: false,
1140 delete_modal: true,
1141 auto_play_gif: false,
1142 display_sensitive_media: false,
1143 reduce_motion: false,
1144 max_toot_chars: limit,
1145 mascot: "/images/pleroma-fox-tan-smol.png"
1146 },
1147 rights: %{
1148 delete_others_notice: present?(user.info.is_moderator),
1149 admin: present?(user.info.is_admin)
1150 },
1151 compose: %{
1152 me: "#{user.id}",
1153 default_privacy: user.info.default_scope,
1154 default_sensitive: false,
1155 allow_content_types: Config.get([:instance, :allowed_post_formats])
1156 },
1157 media_attachments: %{
1158 accept_content_types: [
1159 ".jpg",
1160 ".jpeg",
1161 ".png",
1162 ".gif",
1163 ".webm",
1164 ".mp4",
1165 ".m4v",
1166 "image\/jpeg",
1167 "image\/png",
1168 "image\/gif",
1169 "video\/webm",
1170 "video\/mp4"
1171 ]
1172 },
1173 settings:
1174 user.info.settings ||
1175 %{
1176 onboarded: true,
1177 home: %{
1178 shows: %{
1179 reblog: true,
1180 reply: true
1181 }
1182 },
1183 notifications: %{
1184 alerts: %{
1185 follow: true,
1186 favourite: true,
1187 reblog: true,
1188 mention: true
1189 },
1190 shows: %{
1191 follow: true,
1192 favourite: true,
1193 reblog: true,
1194 mention: true
1195 },
1196 sounds: %{
1197 follow: true,
1198 favourite: true,
1199 reblog: true,
1200 mention: true
1201 }
1202 }
1203 },
1204 push_subscription: nil,
1205 accounts: accounts,
1206 custom_emojis: mastodon_emoji,
1207 char_limit: limit
1208 }
1209 |> Jason.encode!()
1210
1211 conn
1212 |> put_layout(false)
1213 |> put_view(MastodonView)
1214 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1215 else
1216 conn
1217 |> redirect(to: "/web/login")
1218 end
1219 end
1220
1221 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1222 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1223
1224 with changeset <- Ecto.Changeset.change(user),
1225 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1226 {:ok, _user} <- User.update_and_set_cache(changeset) do
1227 json(conn, %{})
1228 else
1229 e ->
1230 conn
1231 |> put_resp_content_type("application/json")
1232 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1233 end
1234 end
1235
1236 @supported_flavours ["glitch", "vanilla"]
1237
1238 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1239 when flavour in @supported_flavours do
1240 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1241
1242 with changeset <- Ecto.Changeset.change(user),
1243 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1244 {:ok, user} <- User.update_and_set_cache(changeset),
1245 flavour <- user.info.flavour do
1246 json(conn, flavour)
1247 else
1248 e ->
1249 conn
1250 |> put_resp_content_type("application/json")
1251 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1252 end
1253 end
1254
1255 def set_flavour(conn, _params) do
1256 conn
1257 |> put_status(400)
1258 |> json(%{error: "Unsupported flavour"})
1259 end
1260
1261 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1262 json(conn, get_user_flavour(user))
1263 end
1264
1265 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1266 flavour
1267 end
1268
1269 defp get_user_flavour(_) do
1270 "glitch"
1271 end
1272
1273 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1274 redirect(conn, to: local_mastodon_root_path(conn))
1275 end
1276
1277 @doc "Local Mastodon FE login init action"
1278 def login(conn, %{"code" => auth_token}) do
1279 with {:ok, app} <- get_or_make_app(),
1280 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1281 {:ok, token} <- Token.exchange_token(app, auth) do
1282 conn
1283 |> put_session(:oauth_token, token.token)
1284 |> redirect(to: local_mastodon_root_path(conn))
1285 end
1286 end
1287
1288 @doc "Local Mastodon FE callback action"
1289 def login(conn, _) do
1290 with {:ok, app} <- get_or_make_app() do
1291 path =
1292 o_auth_path(
1293 conn,
1294 :authorize,
1295 response_type: "code",
1296 client_id: app.client_id,
1297 redirect_uri: ".",
1298 scope: Enum.join(app.scopes, " ")
1299 )
1300
1301 conn
1302 |> redirect(to: path)
1303 end
1304 end
1305
1306 defp local_mastodon_root_path(conn), do: mastodon_api_path(conn, :index, ["getting-started"])
1307
1308 defp get_or_make_app do
1309 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1310 scopes = ["read", "write", "follow", "push"]
1311
1312 with %App{} = app <- Repo.get_by(App, find_attrs) do
1313 {:ok, app} =
1314 if app.scopes == scopes do
1315 {:ok, app}
1316 else
1317 app
1318 |> Ecto.Changeset.change(%{scopes: scopes})
1319 |> Repo.update()
1320 end
1321
1322 {:ok, app}
1323 else
1324 _e ->
1325 cs =
1326 App.register_changeset(
1327 %App{},
1328 Map.put(find_attrs, :scopes, scopes)
1329 )
1330
1331 Repo.insert(cs)
1332 end
1333 end
1334
1335 def logout(conn, _) do
1336 conn
1337 |> clear_session
1338 |> redirect(to: "/")
1339 end
1340
1341 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1342 Logger.debug("Unimplemented, returning unmodified relationship")
1343
1344 with %User{} = target <- User.get_by_id(id) do
1345 conn
1346 |> put_view(AccountView)
1347 |> render("relationship.json", %{user: user, target: target})
1348 end
1349 end
1350
1351 def empty_array(conn, _) do
1352 Logger.debug("Unimplemented, returning an empty array")
1353 json(conn, [])
1354 end
1355
1356 def empty_object(conn, _) do
1357 Logger.debug("Unimplemented, returning an empty object")
1358 json(conn, %{})
1359 end
1360
1361 def get_filters(%{assigns: %{user: user}} = conn, _) do
1362 filters = Filter.get_filters(user)
1363 res = FilterView.render("filters.json", filters: filters)
1364 json(conn, res)
1365 end
1366
1367 def create_filter(
1368 %{assigns: %{user: user}} = conn,
1369 %{"phrase" => phrase, "context" => context} = params
1370 ) do
1371 query = %Filter{
1372 user_id: user.id,
1373 phrase: phrase,
1374 context: context,
1375 hide: Map.get(params, "irreversible", nil),
1376 whole_word: Map.get(params, "boolean", true)
1377 # expires_at
1378 }
1379
1380 {:ok, response} = Filter.create(query)
1381 res = FilterView.render("filter.json", filter: response)
1382 json(conn, res)
1383 end
1384
1385 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1386 filter = Filter.get(filter_id, user)
1387 res = FilterView.render("filter.json", filter: filter)
1388 json(conn, res)
1389 end
1390
1391 def update_filter(
1392 %{assigns: %{user: user}} = conn,
1393 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1394 ) do
1395 query = %Filter{
1396 user_id: user.id,
1397 filter_id: filter_id,
1398 phrase: phrase,
1399 context: context,
1400 hide: Map.get(params, "irreversible", nil),
1401 whole_word: Map.get(params, "boolean", true)
1402 # expires_at
1403 }
1404
1405 {:ok, response} = Filter.update(query)
1406 res = FilterView.render("filter.json", filter: response)
1407 json(conn, res)
1408 end
1409
1410 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1411 query = %Filter{
1412 user_id: user.id,
1413 filter_id: filter_id
1414 }
1415
1416 {:ok, _} = Filter.delete(query)
1417 json(conn, %{})
1418 end
1419
1420 # fallback action
1421 #
1422 def errors(conn, _) do
1423 conn
1424 |> put_status(500)
1425 |> json("Something went wrong")
1426 end
1427
1428 def suggestions(%{assigns: %{user: user}} = conn, _) do
1429 suggestions = Config.get(:suggestions)
1430
1431 if Keyword.get(suggestions, :enabled, false) do
1432 api = Keyword.get(suggestions, :third_party_engine, "")
1433 timeout = Keyword.get(suggestions, :timeout, 5000)
1434 limit = Keyword.get(suggestions, :limit, 23)
1435
1436 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1437
1438 user = user.nickname
1439
1440 url =
1441 api
1442 |> String.replace("{{host}}", host)
1443 |> String.replace("{{user}}", user)
1444
1445 with {:ok, %{status: 200, body: body}} <-
1446 @httpoison.get(
1447 url,
1448 [],
1449 adapter: [
1450 recv_timeout: timeout,
1451 pool: :default
1452 ]
1453 ),
1454 {:ok, data} <- Jason.decode(body) do
1455 data =
1456 data
1457 |> Enum.slice(0, limit)
1458 |> Enum.map(fn x ->
1459 Map.put(
1460 x,
1461 "id",
1462 case User.get_or_fetch(x["acct"]) do
1463 %{id: id} -> id
1464 _ -> 0
1465 end
1466 )
1467 end)
1468 |> Enum.map(fn x ->
1469 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1470 end)
1471 |> Enum.map(fn x ->
1472 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1473 end)
1474
1475 conn
1476 |> json(data)
1477 else
1478 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1479 end
1480 else
1481 json(conn, [])
1482 end
1483 end
1484
1485 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1486 with %Activity{} = activity <- Activity.get_by_id(status_id),
1487 true <- Visibility.visible_for_user?(activity, user) do
1488 data =
1489 StatusView.render(
1490 "card.json",
1491 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1492 )
1493
1494 json(conn, data)
1495 else
1496 _e ->
1497 %{}
1498 end
1499 end
1500
1501 def reports(%{assigns: %{user: user}} = conn, params) do
1502 case CommonAPI.report(user, params) do
1503 {:ok, activity} ->
1504 conn
1505 |> put_view(ReportView)
1506 |> try_render("report.json", %{activity: activity})
1507
1508 {:error, err} ->
1509 conn
1510 |> put_status(:bad_request)
1511 |> json(%{error: err})
1512 end
1513 end
1514
1515 def try_render(conn, target, params)
1516 when is_binary(target) do
1517 res = render(conn, target, params)
1518
1519 if res == nil do
1520 conn
1521 |> put_status(501)
1522 |> json(%{error: "Can't display this activity"})
1523 else
1524 res
1525 end
1526 end
1527
1528 def try_render(conn, _, _) do
1529 conn
1530 |> put_status(501)
1531 |> json(%{error: "Can't display this activity"})
1532 end
1533
1534 defp present?(nil), do: false
1535 defp present?(false), do: false
1536 defp present?(_), do: true
1537 end