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