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