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