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