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