bookmarks in separate table
[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 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