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