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