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