Merge branch 'support/oauth' 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:
1299 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1300 access_token: token,
1301 locale: "en",
1302 domain: Pleroma.Web.Endpoint.host(),
1303 admin: "1",
1304 me: "#{user.id}",
1305 unfollow_modal: false,
1306 boost_modal: false,
1307 delete_modal: true,
1308 auto_play_gif: false,
1309 display_sensitive_media: false,
1310 reduce_motion: false,
1311 max_toot_chars: limit,
1312 mascot: "/images/pleroma-fox-tan-smol.png"
1313 },
1314 rights: %{
1315 delete_others_notice: present?(user.info.is_moderator),
1316 admin: present?(user.info.is_admin)
1317 },
1318 compose: %{
1319 me: "#{user.id}",
1320 default_privacy: user.info.default_scope,
1321 default_sensitive: false,
1322 allow_content_types: Config.get([:instance, :allowed_post_formats])
1323 },
1324 media_attachments: %{
1325 accept_content_types: [
1326 ".jpg",
1327 ".jpeg",
1328 ".png",
1329 ".gif",
1330 ".webm",
1331 ".mp4",
1332 ".m4v",
1333 "image\/jpeg",
1334 "image\/png",
1335 "image\/gif",
1336 "video\/webm",
1337 "video\/mp4"
1338 ]
1339 },
1340 settings:
1341 user.info.settings ||
1342 %{
1343 onboarded: true,
1344 home: %{
1345 shows: %{
1346 reblog: true,
1347 reply: true
1348 }
1349 },
1350 notifications: %{
1351 alerts: %{
1352 follow: true,
1353 favourite: true,
1354 reblog: true,
1355 mention: true
1356 },
1357 shows: %{
1358 follow: true,
1359 favourite: true,
1360 reblog: true,
1361 mention: true
1362 },
1363 sounds: %{
1364 follow: true,
1365 favourite: true,
1366 reblog: true,
1367 mention: true
1368 }
1369 }
1370 },
1371 push_subscription: nil,
1372 accounts: accounts,
1373 custom_emojis: mastodon_emoji,
1374 char_limit: limit
1375 }
1376 |> Jason.encode!()
1377
1378 conn
1379 |> put_layout(false)
1380 |> put_view(MastodonView)
1381 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1382 else
1383 conn
1384 |> put_session(:return_to, conn.request_path)
1385 |> redirect(to: "/web/login")
1386 end
1387 end
1388
1389 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1390 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1391
1392 with changeset <- Ecto.Changeset.change(user),
1393 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1394 {:ok, _user} <- User.update_and_set_cache(changeset) do
1395 json(conn, %{})
1396 else
1397 e ->
1398 conn
1399 |> put_resp_content_type("application/json")
1400 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1401 end
1402 end
1403
1404 @supported_flavours ["glitch", "vanilla"]
1405
1406 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1407 when flavour in @supported_flavours do
1408 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1409
1410 with changeset <- Ecto.Changeset.change(user),
1411 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1412 {:ok, user} <- User.update_and_set_cache(changeset),
1413 flavour <- user.info.flavour do
1414 json(conn, flavour)
1415 else
1416 e ->
1417 conn
1418 |> put_resp_content_type("application/json")
1419 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1420 end
1421 end
1422
1423 def set_flavour(conn, _params) do
1424 conn
1425 |> put_status(400)
1426 |> json(%{error: "Unsupported flavour"})
1427 end
1428
1429 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1430 json(conn, get_user_flavour(user))
1431 end
1432
1433 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1434 flavour
1435 end
1436
1437 defp get_user_flavour(_) do
1438 "glitch"
1439 end
1440
1441 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1442 redirect(conn, to: local_mastodon_root_path(conn))
1443 end
1444
1445 @doc "Local Mastodon FE login init action"
1446 def login(conn, %{"code" => auth_token}) do
1447 with {:ok, app} <- get_or_make_app(),
1448 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1449 {:ok, token} <- Token.exchange_token(app, auth) do
1450 conn
1451 |> put_session(:oauth_token, token.token)
1452 |> redirect(to: local_mastodon_root_path(conn))
1453 end
1454 end
1455
1456 @doc "Local Mastodon FE callback action"
1457 def login(conn, _) do
1458 with {:ok, app} <- get_or_make_app() do
1459 path =
1460 o_auth_path(
1461 conn,
1462 :authorize,
1463 response_type: "code",
1464 client_id: app.client_id,
1465 redirect_uri: ".",
1466 scope: Enum.join(app.scopes, " ")
1467 )
1468
1469 redirect(conn, to: path)
1470 end
1471 end
1472
1473 defp local_mastodon_root_path(conn) do
1474 case get_session(conn, :return_to) do
1475 nil ->
1476 mastodon_api_path(conn, :index, ["getting-started"])
1477
1478 return_to ->
1479 delete_session(conn, :return_to)
1480 return_to
1481 end
1482 end
1483
1484 defp get_or_make_app do
1485 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1486 scopes = ["read", "write", "follow", "push"]
1487
1488 with %App{} = app <- Repo.get_by(App, find_attrs) do
1489 {:ok, app} =
1490 if app.scopes == scopes do
1491 {:ok, app}
1492 else
1493 app
1494 |> Ecto.Changeset.change(%{scopes: scopes})
1495 |> Repo.update()
1496 end
1497
1498 {:ok, app}
1499 else
1500 _e ->
1501 cs =
1502 App.register_changeset(
1503 %App{},
1504 Map.put(find_attrs, :scopes, scopes)
1505 )
1506
1507 Repo.insert(cs)
1508 end
1509 end
1510
1511 def logout(conn, _) do
1512 conn
1513 |> clear_session
1514 |> redirect(to: "/")
1515 end
1516
1517 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1518 Logger.debug("Unimplemented, returning unmodified relationship")
1519
1520 with %User{} = target <- User.get_cached_by_id(id) do
1521 conn
1522 |> put_view(AccountView)
1523 |> render("relationship.json", %{user: user, target: target})
1524 end
1525 end
1526
1527 def empty_array(conn, _) do
1528 Logger.debug("Unimplemented, returning an empty array")
1529 json(conn, [])
1530 end
1531
1532 def empty_object(conn, _) do
1533 Logger.debug("Unimplemented, returning an empty object")
1534 json(conn, %{})
1535 end
1536
1537 def get_filters(%{assigns: %{user: user}} = conn, _) do
1538 filters = Filter.get_filters(user)
1539 res = FilterView.render("filters.json", filters: filters)
1540 json(conn, res)
1541 end
1542
1543 def create_filter(
1544 %{assigns: %{user: user}} = conn,
1545 %{"phrase" => phrase, "context" => context} = params
1546 ) do
1547 query = %Filter{
1548 user_id: user.id,
1549 phrase: phrase,
1550 context: context,
1551 hide: Map.get(params, "irreversible", nil),
1552 whole_word: Map.get(params, "boolean", true)
1553 # expires_at
1554 }
1555
1556 {:ok, response} = Filter.create(query)
1557 res = FilterView.render("filter.json", filter: response)
1558 json(conn, res)
1559 end
1560
1561 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1562 filter = Filter.get(filter_id, user)
1563 res = FilterView.render("filter.json", filter: filter)
1564 json(conn, res)
1565 end
1566
1567 def update_filter(
1568 %{assigns: %{user: user}} = conn,
1569 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1570 ) do
1571 query = %Filter{
1572 user_id: user.id,
1573 filter_id: filter_id,
1574 phrase: phrase,
1575 context: context,
1576 hide: Map.get(params, "irreversible", nil),
1577 whole_word: Map.get(params, "boolean", true)
1578 # expires_at
1579 }
1580
1581 {:ok, response} = Filter.update(query)
1582 res = FilterView.render("filter.json", filter: response)
1583 json(conn, res)
1584 end
1585
1586 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1587 query = %Filter{
1588 user_id: user.id,
1589 filter_id: filter_id
1590 }
1591
1592 {:ok, _} = Filter.delete(query)
1593 json(conn, %{})
1594 end
1595
1596 # fallback action
1597 #
1598 def errors(conn, {:error, %Changeset{} = changeset}) do
1599 error_message =
1600 changeset
1601 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1602 |> Enum.map_join(", ", fn {_k, v} -> v end)
1603
1604 conn
1605 |> put_status(422)
1606 |> json(%{error: error_message})
1607 end
1608
1609 def errors(conn, {:error, :not_found}) do
1610 conn
1611 |> put_status(404)
1612 |> json(%{error: "Record not found"})
1613 end
1614
1615 def errors(conn, _) do
1616 conn
1617 |> put_status(500)
1618 |> json("Something went wrong")
1619 end
1620
1621 def suggestions(%{assigns: %{user: user}} = conn, _) do
1622 suggestions = Config.get(:suggestions)
1623
1624 if Keyword.get(suggestions, :enabled, false) do
1625 api = Keyword.get(suggestions, :third_party_engine, "")
1626 timeout = Keyword.get(suggestions, :timeout, 5000)
1627 limit = Keyword.get(suggestions, :limit, 23)
1628
1629 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1630
1631 user = user.nickname
1632
1633 url =
1634 api
1635 |> String.replace("{{host}}", host)
1636 |> String.replace("{{user}}", user)
1637
1638 with {:ok, %{status: 200, body: body}} <-
1639 @httpoison.get(
1640 url,
1641 [],
1642 adapter: [
1643 recv_timeout: timeout,
1644 pool: :default
1645 ]
1646 ),
1647 {:ok, data} <- Jason.decode(body) do
1648 data =
1649 data
1650 |> Enum.slice(0, limit)
1651 |> Enum.map(fn x ->
1652 Map.put(
1653 x,
1654 "id",
1655 case User.get_or_fetch(x["acct"]) do
1656 {:ok, %User{id: id}} -> id
1657 _ -> 0
1658 end
1659 )
1660 end)
1661 |> Enum.map(fn x ->
1662 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1663 end)
1664 |> Enum.map(fn x ->
1665 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1666 end)
1667
1668 conn
1669 |> json(data)
1670 else
1671 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1672 end
1673 else
1674 json(conn, [])
1675 end
1676 end
1677
1678 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1679 with %Activity{} = activity <- Activity.get_by_id(status_id),
1680 true <- Visibility.visible_for_user?(activity, user) do
1681 data =
1682 StatusView.render(
1683 "card.json",
1684 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1685 )
1686
1687 json(conn, data)
1688 else
1689 _e ->
1690 %{}
1691 end
1692 end
1693
1694 def reports(%{assigns: %{user: user}} = conn, params) do
1695 case CommonAPI.report(user, params) do
1696 {:ok, activity} ->
1697 conn
1698 |> put_view(ReportView)
1699 |> try_render("report.json", %{activity: activity})
1700
1701 {:error, err} ->
1702 conn
1703 |> put_status(:bad_request)
1704 |> json(%{error: err})
1705 end
1706 end
1707
1708 def try_render(conn, target, params)
1709 when is_binary(target) do
1710 res = render(conn, target, params)
1711
1712 if res == nil do
1713 conn
1714 |> put_status(501)
1715 |> json(%{error: "Can't display this activity"})
1716 else
1717 res
1718 end
1719 end
1720
1721 def try_render(conn, _, _) do
1722 conn
1723 |> put_status(501)
1724 |> json(%{error: "Can't display this activity"})
1725 end
1726
1727 defp present?(nil), do: false
1728 defp present?(false), do: false
1729 defp present?(_), do: true
1730 end