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