4f8af32c3a0d69ce789a62b452fbf63f6e9bfcab
[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 %Object{} = object <- Object.normalize(activity),
557 %User{} = user <- User.get_cached_by_nickname(user.nickname),
558 true <- Visibility.visible_for_user?(activity, user),
559 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
560 user = Repo.preload(user, :bookmarks)
561
562 conn
563 |> put_view(StatusView)
564 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
565 end
566 end
567
568 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
569 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
570 %Object{} = object <- Object.normalize(activity),
571 %User{} = user <- User.get_cached_by_nickname(user.nickname),
572 true <- Visibility.visible_for_user?(activity, user),
573 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
574 user = Repo.preload(user, :bookmarks)
575
576 conn
577 |> put_view(StatusView)
578 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
579 end
580 end
581
582 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
583 activity = Activity.get_by_id(id)
584
585 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
586 conn
587 |> put_view(StatusView)
588 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
589 else
590 {:error, reason} ->
591 conn
592 |> put_resp_content_type("application/json")
593 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
594 end
595 end
596
597 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
598 activity = Activity.get_by_id(id)
599
600 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
601 conn
602 |> put_view(StatusView)
603 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
604 end
605 end
606
607 def notifications(%{assigns: %{user: user}} = conn, params) do
608 notifications = MastodonAPI.get_notifications(user, params)
609
610 conn
611 |> add_link_headers(:notifications, notifications)
612 |> put_view(NotificationView)
613 |> render("index.json", %{notifications: notifications, for: user})
614 end
615
616 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
617 with {:ok, notification} <- Notification.get(user, id) do
618 conn
619 |> put_view(NotificationView)
620 |> render("show.json", %{notification: notification, for: user})
621 else
622 {:error, reason} ->
623 conn
624 |> put_resp_content_type("application/json")
625 |> send_resp(403, Jason.encode!(%{"error" => reason}))
626 end
627 end
628
629 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
630 Notification.clear(user)
631 json(conn, %{})
632 end
633
634 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
635 with {:ok, _notif} <- Notification.dismiss(user, id) do
636 json(conn, %{})
637 else
638 {:error, reason} ->
639 conn
640 |> put_resp_content_type("application/json")
641 |> send_resp(403, Jason.encode!(%{"error" => reason}))
642 end
643 end
644
645 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
646 Notification.destroy_multiple(user, ids)
647 json(conn, %{})
648 end
649
650 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
651 id = List.wrap(id)
652 q = from(u in User, where: u.id in ^id)
653 targets = Repo.all(q)
654
655 conn
656 |> put_view(AccountView)
657 |> render("relationships.json", %{user: user, targets: targets})
658 end
659
660 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
661 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
662
663 def update_media(%{assigns: %{user: user}} = conn, data) do
664 with %Object{} = object <- Repo.get(Object, data["id"]),
665 true <- Object.authorize_mutation(object, user),
666 true <- is_binary(data["description"]),
667 description <- data["description"] do
668 new_data = %{object.data | "name" => description}
669
670 {:ok, _} =
671 object
672 |> Object.change(%{data: new_data})
673 |> Repo.update()
674
675 attachment_data = Map.put(new_data, "id", object.id)
676
677 conn
678 |> put_view(StatusView)
679 |> render("attachment.json", %{attachment: attachment_data})
680 end
681 end
682
683 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
684 with {:ok, object} <-
685 ActivityPub.upload(
686 file,
687 actor: User.ap_id(user),
688 description: Map.get(data, "description")
689 ) do
690 attachment_data = Map.put(object.data, "id", object.id)
691
692 conn
693 |> put_view(StatusView)
694 |> render("attachment.json", %{attachment: attachment_data})
695 end
696 end
697
698 def favourited_by(conn, %{"id" => id}) do
699 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
700 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
701 q = from(u in User, where: u.ap_id in ^likes)
702 users = Repo.all(q)
703
704 conn
705 |> put_view(AccountView)
706 |> render(AccountView, "accounts.json", %{users: users, as: :user})
707 else
708 _ -> json(conn, [])
709 end
710 end
711
712 def reblogged_by(conn, %{"id" => id}) do
713 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
714 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
715 q = from(u in User, where: u.ap_id in ^announces)
716 users = Repo.all(q)
717
718 conn
719 |> put_view(AccountView)
720 |> render("accounts.json", %{users: users, as: :user})
721 else
722 _ -> json(conn, [])
723 end
724 end
725
726 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
727 local_only = params["local"] in [true, "True", "true", "1"]
728
729 tags =
730 [params["tag"], params["any"]]
731 |> List.flatten()
732 |> Enum.uniq()
733 |> Enum.filter(& &1)
734 |> Enum.map(&String.downcase(&1))
735
736 tag_all =
737 params["all"] ||
738 []
739 |> Enum.map(&String.downcase(&1))
740
741 tag_reject =
742 params["none"] ||
743 []
744 |> Enum.map(&String.downcase(&1))
745
746 activities =
747 params
748 |> Map.put("type", "Create")
749 |> Map.put("local_only", local_only)
750 |> Map.put("blocking_user", user)
751 |> Map.put("muting_user", user)
752 |> Map.put("tag", tags)
753 |> Map.put("tag_all", tag_all)
754 |> Map.put("tag_reject", tag_reject)
755 |> ActivityPub.fetch_public_activities()
756 |> Enum.reverse()
757
758 conn
759 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
760 |> put_view(StatusView)
761 |> render("index.json", %{activities: activities, for: user, as: :activity})
762 end
763
764 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
765 with %User{} = user <- User.get_cached_by_id(id),
766 followers <- MastodonAPI.get_followers(user, params) do
767 followers =
768 cond do
769 for_user && user.id == for_user.id -> followers
770 user.info.hide_followers -> []
771 true -> followers
772 end
773
774 conn
775 |> add_link_headers(:followers, followers, user)
776 |> put_view(AccountView)
777 |> render("accounts.json", %{users: followers, as: :user})
778 end
779 end
780
781 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
782 with %User{} = user <- User.get_cached_by_id(id),
783 followers <- MastodonAPI.get_friends(user, params) do
784 followers =
785 cond do
786 for_user && user.id == for_user.id -> followers
787 user.info.hide_follows -> []
788 true -> followers
789 end
790
791 conn
792 |> add_link_headers(:following, followers, user)
793 |> put_view(AccountView)
794 |> render("accounts.json", %{users: followers, as: :user})
795 end
796 end
797
798 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
799 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
800 conn
801 |> put_view(AccountView)
802 |> render("accounts.json", %{users: follow_requests, as: :user})
803 end
804 end
805
806 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
807 with %User{} = follower <- User.get_cached_by_id(id),
808 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
809 conn
810 |> put_view(AccountView)
811 |> render("relationship.json", %{user: followed, target: follower})
812 else
813 {:error, message} ->
814 conn
815 |> put_resp_content_type("application/json")
816 |> send_resp(403, Jason.encode!(%{"error" => message}))
817 end
818 end
819
820 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
821 with %User{} = follower <- User.get_cached_by_id(id),
822 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
823 conn
824 |> put_view(AccountView)
825 |> render("relationship.json", %{user: followed, target: follower})
826 else
827 {:error, message} ->
828 conn
829 |> put_resp_content_type("application/json")
830 |> send_resp(403, Jason.encode!(%{"error" => message}))
831 end
832 end
833
834 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
835 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
836 {_, true} <- {:followed, follower.id != followed.id},
837 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
838 conn
839 |> put_view(AccountView)
840 |> render("relationship.json", %{user: follower, target: followed})
841 else
842 {:followed, _} ->
843 {:error, :not_found}
844
845 {:error, message} ->
846 conn
847 |> put_resp_content_type("application/json")
848 |> send_resp(403, Jason.encode!(%{"error" => message}))
849 end
850 end
851
852 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
853 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
854 {_, true} <- {:followed, follower.id != followed.id},
855 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
856 conn
857 |> put_view(AccountView)
858 |> render("account.json", %{user: followed, for: follower})
859 else
860 {:followed, _} ->
861 {:error, :not_found}
862
863 {:error, message} ->
864 conn
865 |> put_resp_content_type("application/json")
866 |> send_resp(403, Jason.encode!(%{"error" => message}))
867 end
868 end
869
870 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
871 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
872 {_, true} <- {:followed, follower.id != followed.id},
873 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
874 conn
875 |> put_view(AccountView)
876 |> render("relationship.json", %{user: follower, target: followed})
877 else
878 {:followed, _} ->
879 {:error, :not_found}
880
881 error ->
882 error
883 end
884 end
885
886 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
887 with %User{} = muted <- User.get_cached_by_id(id),
888 {:ok, muter} <- User.mute(muter, muted) do
889 conn
890 |> put_view(AccountView)
891 |> render("relationship.json", %{user: muter, target: muted})
892 else
893 {:error, message} ->
894 conn
895 |> put_resp_content_type("application/json")
896 |> send_resp(403, Jason.encode!(%{"error" => message}))
897 end
898 end
899
900 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
901 with %User{} = muted <- User.get_cached_by_id(id),
902 {:ok, muter} <- User.unmute(muter, muted) do
903 conn
904 |> put_view(AccountView)
905 |> render("relationship.json", %{user: muter, target: muted})
906 else
907 {:error, message} ->
908 conn
909 |> put_resp_content_type("application/json")
910 |> send_resp(403, Jason.encode!(%{"error" => message}))
911 end
912 end
913
914 def mutes(%{assigns: %{user: user}} = conn, _) do
915 with muted_accounts <- User.muted_users(user) do
916 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
917 json(conn, res)
918 end
919 end
920
921 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
922 with %User{} = blocked <- User.get_cached_by_id(id),
923 {:ok, blocker} <- User.block(blocker, blocked),
924 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
925 conn
926 |> put_view(AccountView)
927 |> render("relationship.json", %{user: blocker, target: blocked})
928 else
929 {:error, message} ->
930 conn
931 |> put_resp_content_type("application/json")
932 |> send_resp(403, Jason.encode!(%{"error" => message}))
933 end
934 end
935
936 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
937 with %User{} = blocked <- User.get_cached_by_id(id),
938 {:ok, blocker} <- User.unblock(blocker, blocked),
939 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
940 conn
941 |> put_view(AccountView)
942 |> render("relationship.json", %{user: blocker, target: blocked})
943 else
944 {:error, message} ->
945 conn
946 |> put_resp_content_type("application/json")
947 |> send_resp(403, Jason.encode!(%{"error" => message}))
948 end
949 end
950
951 def blocks(%{assigns: %{user: user}} = conn, _) do
952 with blocked_accounts <- User.blocked_users(user) do
953 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
954 json(conn, res)
955 end
956 end
957
958 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
959 json(conn, info.domain_blocks || [])
960 end
961
962 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
963 User.block_domain(blocker, domain)
964 json(conn, %{})
965 end
966
967 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
968 User.unblock_domain(blocker, domain)
969 json(conn, %{})
970 end
971
972 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
973 with %User{} = subscription_target <- User.get_cached_by_id(id),
974 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
975 conn
976 |> put_view(AccountView)
977 |> render("relationship.json", %{user: user, target: subscription_target})
978 else
979 {:error, message} ->
980 conn
981 |> put_resp_content_type("application/json")
982 |> send_resp(403, Jason.encode!(%{"error" => message}))
983 end
984 end
985
986 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
987 with %User{} = subscription_target <- User.get_cached_by_id(id),
988 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
989 conn
990 |> put_view(AccountView)
991 |> render("relationship.json", %{user: user, target: subscription_target})
992 else
993 {:error, message} ->
994 conn
995 |> put_resp_content_type("application/json")
996 |> send_resp(403, Jason.encode!(%{"error" => message}))
997 end
998 end
999
1000 def status_search(user, query) do
1001 fetched =
1002 if Regex.match?(~r/https?:/, query) do
1003 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1004 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1005 true <- Visibility.visible_for_user?(activity, user) do
1006 [activity]
1007 else
1008 _e -> []
1009 end
1010 end || []
1011
1012 q =
1013 from(
1014 [a, o] in Activity.with_preloaded_object(Activity),
1015 where: fragment("?->>'type' = 'Create'", a.data),
1016 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1017 where:
1018 fragment(
1019 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1020 o.data,
1021 ^query
1022 ),
1023 limit: 20,
1024 order_by: [desc: :id]
1025 )
1026
1027 Repo.all(q) ++ fetched
1028 end
1029
1030 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1031 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1032
1033 statuses = status_search(user, query)
1034
1035 tags_path = Web.base_url() <> "/tag/"
1036
1037 tags =
1038 query
1039 |> String.split()
1040 |> Enum.uniq()
1041 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1042 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1043 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1044
1045 res = %{
1046 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1047 "statuses" =>
1048 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1049 "hashtags" => tags
1050 }
1051
1052 json(conn, res)
1053 end
1054
1055 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1056 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1057
1058 statuses = status_search(user, query)
1059
1060 tags =
1061 query
1062 |> String.split()
1063 |> Enum.uniq()
1064 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1065 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1066
1067 res = %{
1068 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1069 "statuses" =>
1070 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1071 "hashtags" => tags
1072 }
1073
1074 json(conn, res)
1075 end
1076
1077 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1078 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1079
1080 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1081
1082 json(conn, res)
1083 end
1084
1085 def favourites(%{assigns: %{user: user}} = conn, params) do
1086 params =
1087 params
1088 |> Map.put("type", "Create")
1089 |> Map.put("favorited_by", user.ap_id)
1090 |> Map.put("blocking_user", user)
1091
1092 activities =
1093 ActivityPub.fetch_activities([], params)
1094 |> Enum.reverse()
1095
1096 user = Repo.preload(user, :bookmarks)
1097
1098 conn
1099 |> add_link_headers(:favourites, activities)
1100 |> put_view(StatusView)
1101 |> render("index.json", %{activities: activities, for: user, as: :activity})
1102 end
1103
1104 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1105 with %User{} = user <- User.get_by_id(id),
1106 false <- user.info.hide_favorites do
1107 params =
1108 params
1109 |> Map.put("type", "Create")
1110 |> Map.put("favorited_by", user.ap_id)
1111 |> Map.put("blocking_user", for_user)
1112
1113 recipients =
1114 if for_user do
1115 ["https://www.w3.org/ns/activitystreams#Public"] ++
1116 [for_user.ap_id | for_user.following]
1117 else
1118 ["https://www.w3.org/ns/activitystreams#Public"]
1119 end
1120
1121 activities =
1122 recipients
1123 |> ActivityPub.fetch_activities(params)
1124 |> Enum.reverse()
1125
1126 conn
1127 |> add_link_headers(:favourites, activities)
1128 |> put_view(StatusView)
1129 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1130 else
1131 nil ->
1132 {:error, :not_found}
1133
1134 true ->
1135 conn
1136 |> put_status(403)
1137 |> json(%{error: "Can't get favorites"})
1138 end
1139 end
1140
1141 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1142 user = User.get_cached_by_id(user.id)
1143 user = Repo.preload(user, :bookmarks)
1144
1145 bookmarks =
1146 Bookmark.for_user_query(user.id)
1147 |> Pagination.fetch_paginated(params)
1148
1149 activities =
1150 bookmarks
1151 |> Enum.map(fn b -> b.activity end)
1152
1153 conn
1154 |> add_link_headers(:bookmarks, bookmarks)
1155 |> put_view(StatusView)
1156 |> render("index.json", %{activities: activities, for: user, as: :activity})
1157 end
1158
1159 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1160 lists = Pleroma.List.for_user(user, opts)
1161 res = ListView.render("lists.json", lists: lists)
1162 json(conn, res)
1163 end
1164
1165 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1166 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1167 res = ListView.render("list.json", list: list)
1168 json(conn, res)
1169 else
1170 _e ->
1171 conn
1172 |> put_status(404)
1173 |> json(%{error: "Record not found"})
1174 end
1175 end
1176
1177 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1178 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1179 res = ListView.render("lists.json", lists: lists)
1180 json(conn, res)
1181 end
1182
1183 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1184 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1185 {:ok, _list} <- Pleroma.List.delete(list) do
1186 json(conn, %{})
1187 else
1188 _e ->
1189 json(conn, "error")
1190 end
1191 end
1192
1193 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1194 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1195 res = ListView.render("list.json", list: list)
1196 json(conn, res)
1197 end
1198 end
1199
1200 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1201 accounts
1202 |> Enum.each(fn account_id ->
1203 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1204 %User{} = followed <- User.get_cached_by_id(account_id) do
1205 Pleroma.List.follow(list, followed)
1206 end
1207 end)
1208
1209 json(conn, %{})
1210 end
1211
1212 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1213 accounts
1214 |> Enum.each(fn account_id ->
1215 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1216 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1217 Pleroma.List.unfollow(list, followed)
1218 end
1219 end)
1220
1221 json(conn, %{})
1222 end
1223
1224 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1225 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1226 {:ok, users} = Pleroma.List.get_following(list) do
1227 conn
1228 |> put_view(AccountView)
1229 |> render("accounts.json", %{users: users, as: :user})
1230 end
1231 end
1232
1233 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1234 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1235 {:ok, list} <- Pleroma.List.rename(list, title) do
1236 res = ListView.render("list.json", list: list)
1237 json(conn, res)
1238 else
1239 _e ->
1240 json(conn, "error")
1241 end
1242 end
1243
1244 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1245 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1246 params =
1247 params
1248 |> Map.put("type", "Create")
1249 |> Map.put("blocking_user", user)
1250 |> Map.put("muting_user", user)
1251
1252 # we must filter the following list for the user to avoid leaking statuses the user
1253 # does not actually have permission to see (for more info, peruse security issue #270).
1254 activities =
1255 following
1256 |> Enum.filter(fn x -> x in user.following end)
1257 |> ActivityPub.fetch_activities_bounded(following, params)
1258 |> Enum.reverse()
1259
1260 user = Repo.preload(user, :bookmarks)
1261
1262 conn
1263 |> put_view(StatusView)
1264 |> render("index.json", %{activities: activities, for: user, as: :activity})
1265 else
1266 _e ->
1267 conn
1268 |> put_status(403)
1269 |> json(%{error: "Error."})
1270 end
1271 end
1272
1273 def index(%{assigns: %{user: user}} = conn, _params) do
1274 token = get_session(conn, :oauth_token)
1275
1276 if user && token do
1277 mastodon_emoji = mastodonized_emoji()
1278
1279 limit = Config.get([:instance, :limit])
1280
1281 accounts =
1282 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1283
1284 flavour = get_user_flavour(user)
1285
1286 initial_state =
1287 %{
1288 meta: %{
1289 streaming_api_base_url:
1290 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1291 access_token: token,
1292 locale: "en",
1293 domain: Pleroma.Web.Endpoint.host(),
1294 admin: "1",
1295 me: "#{user.id}",
1296 unfollow_modal: false,
1297 boost_modal: false,
1298 delete_modal: true,
1299 auto_play_gif: false,
1300 display_sensitive_media: false,
1301 reduce_motion: false,
1302 max_toot_chars: limit,
1303 mascot: "/images/pleroma-fox-tan-smol.png"
1304 },
1305 rights: %{
1306 delete_others_notice: present?(user.info.is_moderator),
1307 admin: present?(user.info.is_admin)
1308 },
1309 compose: %{
1310 me: "#{user.id}",
1311 default_privacy: user.info.default_scope,
1312 default_sensitive: false,
1313 allow_content_types: Config.get([:instance, :allowed_post_formats])
1314 },
1315 media_attachments: %{
1316 accept_content_types: [
1317 ".jpg",
1318 ".jpeg",
1319 ".png",
1320 ".gif",
1321 ".webm",
1322 ".mp4",
1323 ".m4v",
1324 "image\/jpeg",
1325 "image\/png",
1326 "image\/gif",
1327 "video\/webm",
1328 "video\/mp4"
1329 ]
1330 },
1331 settings:
1332 user.info.settings ||
1333 %{
1334 onboarded: true,
1335 home: %{
1336 shows: %{
1337 reblog: true,
1338 reply: true
1339 }
1340 },
1341 notifications: %{
1342 alerts: %{
1343 follow: true,
1344 favourite: true,
1345 reblog: true,
1346 mention: true
1347 },
1348 shows: %{
1349 follow: true,
1350 favourite: true,
1351 reblog: true,
1352 mention: true
1353 },
1354 sounds: %{
1355 follow: true,
1356 favourite: true,
1357 reblog: true,
1358 mention: true
1359 }
1360 }
1361 },
1362 push_subscription: nil,
1363 accounts: accounts,
1364 custom_emojis: mastodon_emoji,
1365 char_limit: limit
1366 }
1367 |> Jason.encode!()
1368
1369 conn
1370 |> put_layout(false)
1371 |> put_view(MastodonView)
1372 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1373 else
1374 conn
1375 |> put_session(:return_to, conn.request_path)
1376 |> redirect(to: "/web/login")
1377 end
1378 end
1379
1380 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1381 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1382
1383 with changeset <- Ecto.Changeset.change(user),
1384 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1385 {:ok, _user} <- User.update_and_set_cache(changeset) do
1386 json(conn, %{})
1387 else
1388 e ->
1389 conn
1390 |> put_resp_content_type("application/json")
1391 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1392 end
1393 end
1394
1395 @supported_flavours ["glitch", "vanilla"]
1396
1397 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1398 when flavour in @supported_flavours do
1399 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1400
1401 with changeset <- Ecto.Changeset.change(user),
1402 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1403 {:ok, user} <- User.update_and_set_cache(changeset),
1404 flavour <- user.info.flavour do
1405 json(conn, flavour)
1406 else
1407 e ->
1408 conn
1409 |> put_resp_content_type("application/json")
1410 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1411 end
1412 end
1413
1414 def set_flavour(conn, _params) do
1415 conn
1416 |> put_status(400)
1417 |> json(%{error: "Unsupported flavour"})
1418 end
1419
1420 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1421 json(conn, get_user_flavour(user))
1422 end
1423
1424 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1425 flavour
1426 end
1427
1428 defp get_user_flavour(_) do
1429 "glitch"
1430 end
1431
1432 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1433 redirect(conn, to: local_mastodon_root_path(conn))
1434 end
1435
1436 @doc "Local Mastodon FE login init action"
1437 def login(conn, %{"code" => auth_token}) do
1438 with {:ok, app} <- get_or_make_app(),
1439 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1440 {:ok, token} <- Token.exchange_token(app, auth) do
1441 conn
1442 |> put_session(:oauth_token, token.token)
1443 |> redirect(to: local_mastodon_root_path(conn))
1444 end
1445 end
1446
1447 @doc "Local Mastodon FE callback action"
1448 def login(conn, _) do
1449 with {:ok, app} <- get_or_make_app() do
1450 path =
1451 o_auth_path(
1452 conn,
1453 :authorize,
1454 response_type: "code",
1455 client_id: app.client_id,
1456 redirect_uri: ".",
1457 scope: Enum.join(app.scopes, " ")
1458 )
1459
1460 redirect(conn, to: path)
1461 end
1462 end
1463
1464 defp local_mastodon_root_path(conn) do
1465 case get_session(conn, :return_to) do
1466 nil ->
1467 mastodon_api_path(conn, :index, ["getting-started"])
1468
1469 return_to ->
1470 delete_session(conn, :return_to)
1471 return_to
1472 end
1473 end
1474
1475 defp get_or_make_app do
1476 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1477 scopes = ["read", "write", "follow", "push"]
1478
1479 with %App{} = app <- Repo.get_by(App, find_attrs) do
1480 {:ok, app} =
1481 if app.scopes == scopes do
1482 {:ok, app}
1483 else
1484 app
1485 |> Ecto.Changeset.change(%{scopes: scopes})
1486 |> Repo.update()
1487 end
1488
1489 {:ok, app}
1490 else
1491 _e ->
1492 cs =
1493 App.register_changeset(
1494 %App{},
1495 Map.put(find_attrs, :scopes, scopes)
1496 )
1497
1498 Repo.insert(cs)
1499 end
1500 end
1501
1502 def logout(conn, _) do
1503 conn
1504 |> clear_session
1505 |> redirect(to: "/")
1506 end
1507
1508 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1509 Logger.debug("Unimplemented, returning unmodified relationship")
1510
1511 with %User{} = target <- User.get_cached_by_id(id) do
1512 conn
1513 |> put_view(AccountView)
1514 |> render("relationship.json", %{user: user, target: target})
1515 end
1516 end
1517
1518 def empty_array(conn, _) do
1519 Logger.debug("Unimplemented, returning an empty array")
1520 json(conn, [])
1521 end
1522
1523 def empty_object(conn, _) do
1524 Logger.debug("Unimplemented, returning an empty object")
1525 json(conn, %{})
1526 end
1527
1528 def get_filters(%{assigns: %{user: user}} = conn, _) do
1529 filters = Filter.get_filters(user)
1530 res = FilterView.render("filters.json", filters: filters)
1531 json(conn, res)
1532 end
1533
1534 def create_filter(
1535 %{assigns: %{user: user}} = conn,
1536 %{"phrase" => phrase, "context" => context} = params
1537 ) do
1538 query = %Filter{
1539 user_id: user.id,
1540 phrase: phrase,
1541 context: context,
1542 hide: Map.get(params, "irreversible", nil),
1543 whole_word: Map.get(params, "boolean", true)
1544 # expires_at
1545 }
1546
1547 {:ok, response} = Filter.create(query)
1548 res = FilterView.render("filter.json", filter: response)
1549 json(conn, res)
1550 end
1551
1552 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1553 filter = Filter.get(filter_id, user)
1554 res = FilterView.render("filter.json", filter: filter)
1555 json(conn, res)
1556 end
1557
1558 def update_filter(
1559 %{assigns: %{user: user}} = conn,
1560 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1561 ) do
1562 query = %Filter{
1563 user_id: user.id,
1564 filter_id: filter_id,
1565 phrase: phrase,
1566 context: context,
1567 hide: Map.get(params, "irreversible", nil),
1568 whole_word: Map.get(params, "boolean", true)
1569 # expires_at
1570 }
1571
1572 {:ok, response} = Filter.update(query)
1573 res = FilterView.render("filter.json", filter: response)
1574 json(conn, res)
1575 end
1576
1577 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1578 query = %Filter{
1579 user_id: user.id,
1580 filter_id: filter_id
1581 }
1582
1583 {:ok, _} = Filter.delete(query)
1584 json(conn, %{})
1585 end
1586
1587 # fallback action
1588 #
1589 def errors(conn, {:error, %Changeset{} = changeset}) do
1590 error_message =
1591 changeset
1592 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1593 |> Enum.map_join(", ", fn {_k, v} -> v end)
1594
1595 conn
1596 |> put_status(422)
1597 |> json(%{error: error_message})
1598 end
1599
1600 def errors(conn, {:error, :not_found}) do
1601 conn
1602 |> put_status(404)
1603 |> json(%{error: "Record not found"})
1604 end
1605
1606 def errors(conn, _) do
1607 conn
1608 |> put_status(500)
1609 |> json("Something went wrong")
1610 end
1611
1612 def suggestions(%{assigns: %{user: user}} = conn, _) do
1613 suggestions = Config.get(:suggestions)
1614
1615 if Keyword.get(suggestions, :enabled, false) do
1616 api = Keyword.get(suggestions, :third_party_engine, "")
1617 timeout = Keyword.get(suggestions, :timeout, 5000)
1618 limit = Keyword.get(suggestions, :limit, 23)
1619
1620 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1621
1622 user = user.nickname
1623
1624 url =
1625 api
1626 |> String.replace("{{host}}", host)
1627 |> String.replace("{{user}}", user)
1628
1629 with {:ok, %{status: 200, body: body}} <-
1630 @httpoison.get(
1631 url,
1632 [],
1633 adapter: [
1634 recv_timeout: timeout,
1635 pool: :default
1636 ]
1637 ),
1638 {:ok, data} <- Jason.decode(body) do
1639 data =
1640 data
1641 |> Enum.slice(0, limit)
1642 |> Enum.map(fn x ->
1643 Map.put(
1644 x,
1645 "id",
1646 case User.get_or_fetch(x["acct"]) do
1647 %{id: id} -> id
1648 _ -> 0
1649 end
1650 )
1651 end)
1652 |> Enum.map(fn x ->
1653 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1654 end)
1655 |> Enum.map(fn x ->
1656 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1657 end)
1658
1659 conn
1660 |> json(data)
1661 else
1662 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1663 end
1664 else
1665 json(conn, [])
1666 end
1667 end
1668
1669 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1670 with %Activity{} = activity <- Activity.get_by_id(status_id),
1671 true <- Visibility.visible_for_user?(activity, user) do
1672 data =
1673 StatusView.render(
1674 "card.json",
1675 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1676 )
1677
1678 json(conn, data)
1679 else
1680 _e ->
1681 %{}
1682 end
1683 end
1684
1685 def reports(%{assigns: %{user: user}} = conn, params) do
1686 case CommonAPI.report(user, params) do
1687 {:ok, activity} ->
1688 conn
1689 |> put_view(ReportView)
1690 |> try_render("report.json", %{activity: activity})
1691
1692 {:error, err} ->
1693 conn
1694 |> put_status(:bad_request)
1695 |> json(%{error: err})
1696 end
1697 end
1698
1699 def try_render(conn, target, params)
1700 when is_binary(target) do
1701 res = render(conn, target, params)
1702
1703 if res == nil do
1704 conn
1705 |> put_status(501)
1706 |> json(%{error: "Can't display this activity"})
1707 else
1708 res
1709 end
1710 end
1711
1712 def try_render(conn, _, _) do
1713 conn
1714 |> put_status(501)
1715 |> json(%{error: "Can't display this activity"})
1716 end
1717
1718 defp present?(nil), do: false
1719 defp present?(false), do: false
1720 defp present?(_), do: true
1721 end