9179a26826840d1c5a33df5a49ebe8ed64b5dd8a
[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, "header", :banner, fn value ->
107 with %Plug.Upload{} <- value,
108 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
109 {:ok, object.data}
110 else
111 _ -> :error
112 end
113 end)
114
115 info_cng = User.Info.profile_update(user.info, info_params)
116
117 with changeset <- User.update_changeset(user, user_params),
118 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
119 {:ok, user} <- User.update_and_set_cache(changeset) do
120 if original_user != user do
121 CommonAPI.update(user)
122 end
123
124 json(conn, AccountView.render("account.json", %{user: user, for: user}))
125 else
126 _e ->
127 conn
128 |> put_status(403)
129 |> json(%{error: "Invalid request"})
130 end
131 end
132
133 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
134 account = AccountView.render("account.json", %{user: user, for: user})
135 json(conn, account)
136 end
137
138 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
139 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
140 conn
141 |> put_view(AppView)
142 |> render("short.json", %{app: app})
143 end
144 end
145
146 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
147 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
148 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
149 account = AccountView.render("account.json", %{user: user, for: for_user})
150 json(conn, account)
151 else
152 _e ->
153 conn
154 |> put_status(404)
155 |> json(%{error: "Can't find user"})
156 end
157 end
158
159 @mastodon_api_level "2.5.0"
160
161 def masto_instance(conn, _params) do
162 instance = Config.get(:instance)
163
164 response = %{
165 uri: Web.base_url(),
166 title: Keyword.get(instance, :name),
167 description: Keyword.get(instance, :description),
168 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
169 email: Keyword.get(instance, :email),
170 urls: %{
171 streaming_api: Pleroma.Web.Endpoint.websocket_url()
172 },
173 stats: Stats.get_stats(),
174 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
175 languages: ["en"],
176 registrations: Pleroma.Config.get([:instance, :registrations_open]),
177 # Extra (not present in Mastodon):
178 max_toot_chars: Keyword.get(instance, :limit)
179 }
180
181 json(conn, response)
182 end
183
184 def peers(conn, _params) do
185 json(conn, Stats.get_peers())
186 end
187
188 defp mastodonized_emoji do
189 Pleroma.Emoji.get_all()
190 |> Enum.map(fn {shortcode, relative_url, tags} ->
191 url = to_string(URI.merge(Web.base_url(), relative_url))
192
193 %{
194 "shortcode" => shortcode,
195 "static_url" => url,
196 "visible_in_picker" => true,
197 "url" => url,
198 "tags" => tags
199 }
200 end)
201 end
202
203 def custom_emojis(conn, _params) do
204 mastodon_emoji = mastodonized_emoji()
205 json(conn, mastodon_emoji)
206 end
207
208 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
209 params =
210 conn.params
211 |> Map.drop(["since_id", "max_id", "min_id"])
212 |> Map.merge(params)
213
214 last = List.last(activities)
215
216 if last do
217 max_id = last.id
218
219 limit =
220 params
221 |> Map.get("limit", "20")
222 |> String.to_integer()
223
224 min_id =
225 if length(activities) <= limit do
226 activities
227 |> List.first()
228 |> Map.get(:id)
229 else
230 activities
231 |> Enum.at(limit * -1)
232 |> Map.get(:id)
233 end
234
235 {next_url, prev_url} =
236 if param do
237 {
238 mastodon_api_url(
239 Pleroma.Web.Endpoint,
240 method,
241 param,
242 Map.merge(params, %{max_id: max_id})
243 ),
244 mastodon_api_url(
245 Pleroma.Web.Endpoint,
246 method,
247 param,
248 Map.merge(params, %{min_id: min_id})
249 )
250 }
251 else
252 {
253 mastodon_api_url(
254 Pleroma.Web.Endpoint,
255 method,
256 Map.merge(params, %{max_id: max_id})
257 ),
258 mastodon_api_url(
259 Pleroma.Web.Endpoint,
260 method,
261 Map.merge(params, %{min_id: min_id})
262 )
263 }
264 end
265
266 conn
267 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
268 else
269 conn
270 end
271 end
272
273 def home_timeline(%{assigns: %{user: user}} = conn, params) do
274 params =
275 params
276 |> Map.put("type", ["Create", "Announce"])
277 |> Map.put("blocking_user", user)
278 |> Map.put("muting_user", user)
279 |> Map.put("user", user)
280
281 activities =
282 [user.ap_id | user.following]
283 |> ActivityPub.fetch_activities(params)
284 |> ActivityPub.contain_timeline(user)
285 |> Enum.reverse()
286
287 user = Repo.preload(user, bookmarks: :activity)
288
289 conn
290 |> add_link_headers(:home_timeline, activities)
291 |> put_view(StatusView)
292 |> render("index.json", %{activities: activities, for: user, as: :activity})
293 end
294
295 def public_timeline(%{assigns: %{user: user}} = conn, params) do
296 local_only = params["local"] in [true, "True", "true", "1"]
297
298 activities =
299 params
300 |> Map.put("type", ["Create", "Announce"])
301 |> Map.put("local_only", local_only)
302 |> Map.put("blocking_user", user)
303 |> Map.put("muting_user", user)
304 |> ActivityPub.fetch_public_activities()
305 |> Enum.reverse()
306
307 user = Repo.preload(user, bookmarks: :activity)
308
309 conn
310 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
311 |> put_view(StatusView)
312 |> render("index.json", %{activities: activities, for: user, as: :activity})
313 end
314
315 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
316 with %User{} = user <- User.get_cached_by_id(params["id"]),
317 reading_user <- Repo.preload(reading_user, :bookmarks) do
318 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
319
320 conn
321 |> add_link_headers(:user_statuses, activities, params["id"])
322 |> put_view(StatusView)
323 |> render("index.json", %{
324 activities: activities,
325 for: reading_user,
326 as: :activity
327 })
328 end
329 end
330
331 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
332 params =
333 params
334 |> Map.put("type", "Create")
335 |> Map.put("blocking_user", user)
336 |> Map.put("user", user)
337 |> Map.put(:visibility, "direct")
338
339 activities =
340 [user.ap_id]
341 |> ActivityPub.fetch_activities_query(params)
342 |> Pagination.fetch_paginated(params)
343
344 user = Repo.preload(user, bookmarks: :activity)
345
346 conn
347 |> add_link_headers(:dm_timeline, activities)
348 |> put_view(StatusView)
349 |> render("index.json", %{activities: activities, for: user, as: :activity})
350 end
351
352 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
353 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
354 true <- Visibility.visible_for_user?(activity, user) do
355 user = Repo.preload(user, bookmarks: :activity)
356
357 conn
358 |> put_view(StatusView)
359 |> try_render("status.json", %{activity: activity, for: user})
360 end
361 end
362
363 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
364 with %Activity{} = activity <- Activity.get_by_id(id),
365 activities <-
366 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
367 "blocking_user" => user,
368 "user" => user
369 }),
370 activities <-
371 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
372 activities <-
373 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
374 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
375 result = %{
376 ancestors:
377 StatusView.render(
378 "index.json",
379 for: user,
380 activities: grouped_activities[true] || [],
381 as: :activity
382 )
383 |> Enum.reverse(),
384 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
385 descendants:
386 StatusView.render(
387 "index.json",
388 for: user,
389 activities: grouped_activities[false] || [],
390 as: :activity
391 )
392 |> Enum.reverse()
393 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
394 }
395
396 json(conn, result)
397 end
398 end
399
400 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
401 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
402 conn
403 |> add_link_headers(:scheduled_statuses, scheduled_activities)
404 |> put_view(ScheduledActivityView)
405 |> render("index.json", %{scheduled_activities: scheduled_activities})
406 end
407 end
408
409 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
410 with %ScheduledActivity{} = scheduled_activity <-
411 ScheduledActivity.get(user, scheduled_activity_id) do
412 conn
413 |> put_view(ScheduledActivityView)
414 |> render("show.json", %{scheduled_activity: scheduled_activity})
415 else
416 _ -> {:error, :not_found}
417 end
418 end
419
420 def update_scheduled_status(
421 %{assigns: %{user: user}} = conn,
422 %{"id" => scheduled_activity_id} = params
423 ) do
424 with %ScheduledActivity{} = scheduled_activity <-
425 ScheduledActivity.get(user, scheduled_activity_id),
426 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
427 conn
428 |> put_view(ScheduledActivityView)
429 |> render("show.json", %{scheduled_activity: scheduled_activity})
430 else
431 nil -> {:error, :not_found}
432 error -> error
433 end
434 end
435
436 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
437 with %ScheduledActivity{} = scheduled_activity <-
438 ScheduledActivity.get(user, scheduled_activity_id),
439 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
440 conn
441 |> put_view(ScheduledActivityView)
442 |> render("show.json", %{scheduled_activity: scheduled_activity})
443 else
444 nil -> {:error, :not_found}
445 error -> error
446 end
447 end
448
449 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
450 when length(media_ids) > 0 do
451 params =
452 params
453 |> Map.put("status", ".")
454
455 post_status(conn, params)
456 end
457
458 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
459 params =
460 params
461 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
462
463 idempotency_key =
464 case get_req_header(conn, "idempotency-key") do
465 [key] -> key
466 _ -> Ecto.UUID.generate()
467 end
468
469 scheduled_at = params["scheduled_at"]
470
471 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
472 with {:ok, scheduled_activity} <-
473 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
474 conn
475 |> put_view(ScheduledActivityView)
476 |> render("show.json", %{scheduled_activity: scheduled_activity})
477 end
478 else
479 params = Map.drop(params, ["scheduled_at"])
480
481 {:ok, activity} =
482 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
483 CommonAPI.post(user, params)
484 end)
485
486 conn
487 |> put_view(StatusView)
488 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
489 end
490 end
491
492 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
493 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
494 json(conn, %{})
495 else
496 _e ->
497 conn
498 |> put_status(403)
499 |> json(%{error: "Can't delete this post"})
500 end
501 end
502
503 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
504 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
505 %Activity{} = announce <- Activity.normalize(announce.data) do
506 user = Repo.preload(user, bookmarks: :activity)
507
508 conn
509 |> put_view(StatusView)
510 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
511 end
512 end
513
514 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
515 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
516 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
517 user = Repo.preload(user, bookmarks: :activity)
518
519 conn
520 |> put_view(StatusView)
521 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
522 end
523 end
524
525 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
526 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
527 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
528 conn
529 |> put_view(StatusView)
530 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
531 end
532 end
533
534 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
535 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
536 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
537 conn
538 |> put_view(StatusView)
539 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
540 end
541 end
542
543 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
544 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
545 conn
546 |> put_view(StatusView)
547 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
548 else
549 {:error, reason} ->
550 conn
551 |> put_resp_content_type("application/json")
552 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
553 end
554 end
555
556 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
557 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
558 conn
559 |> put_view(StatusView)
560 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
561 end
562 end
563
564 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
565 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
566 %User{} = user <- User.get_cached_by_nickname(user.nickname),
567 true <- Visibility.visible_for_user?(activity, user),
568 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
569 user = Repo.preload(user, bookmarks: :activity)
570
571 conn
572 |> put_view(StatusView)
573 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
574 end
575 end
576
577 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
578 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
579 %User{} = user <- User.get_cached_by_nickname(user.nickname),
580 true <- Visibility.visible_for_user?(activity, user),
581 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
582 user = Repo.preload(user, bookmarks: :activity)
583
584 conn
585 |> put_view(StatusView)
586 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
587 end
588 end
589
590 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
591 activity = Activity.get_by_id(id)
592
593 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
594 conn
595 |> put_view(StatusView)
596 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
597 else
598 {:error, reason} ->
599 conn
600 |> put_resp_content_type("application/json")
601 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
602 end
603 end
604
605 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
606 activity = Activity.get_by_id(id)
607
608 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
609 conn
610 |> put_view(StatusView)
611 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
612 end
613 end
614
615 def notifications(%{assigns: %{user: user}} = conn, params) do
616 notifications = MastodonAPI.get_notifications(user, params)
617
618 conn
619 |> add_link_headers(:notifications, notifications)
620 |> put_view(NotificationView)
621 |> render("index.json", %{notifications: notifications, for: user})
622 end
623
624 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
625 with {:ok, notification} <- Notification.get(user, id) do
626 conn
627 |> put_view(NotificationView)
628 |> render("show.json", %{notification: notification, for: user})
629 else
630 {:error, reason} ->
631 conn
632 |> put_resp_content_type("application/json")
633 |> send_resp(403, Jason.encode!(%{"error" => reason}))
634 end
635 end
636
637 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
638 Notification.clear(user)
639 json(conn, %{})
640 end
641
642 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
643 with {:ok, _notif} <- Notification.dismiss(user, id) do
644 json(conn, %{})
645 else
646 {:error, reason} ->
647 conn
648 |> put_resp_content_type("application/json")
649 |> send_resp(403, Jason.encode!(%{"error" => reason}))
650 end
651 end
652
653 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
654 Notification.destroy_multiple(user, ids)
655 json(conn, %{})
656 end
657
658 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
659 id = List.wrap(id)
660 q = from(u in User, where: u.id in ^id)
661 targets = Repo.all(q)
662
663 conn
664 |> put_view(AccountView)
665 |> render("relationships.json", %{user: user, targets: targets})
666 end
667
668 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
669 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
670
671 def update_media(%{assigns: %{user: user}} = conn, data) do
672 with %Object{} = object <- Repo.get(Object, data["id"]),
673 true <- Object.authorize_mutation(object, user),
674 true <- is_binary(data["description"]),
675 description <- data["description"] do
676 new_data = %{object.data | "name" => description}
677
678 {:ok, _} =
679 object
680 |> Object.change(%{data: new_data})
681 |> Repo.update()
682
683 attachment_data = Map.put(new_data, "id", object.id)
684
685 conn
686 |> put_view(StatusView)
687 |> render("attachment.json", %{attachment: attachment_data})
688 end
689 end
690
691 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
692 with {:ok, object} <-
693 ActivityPub.upload(
694 file,
695 actor: User.ap_id(user),
696 description: Map.get(data, "description")
697 ) do
698 attachment_data = Map.put(object.data, "id", object.id)
699
700 conn
701 |> put_view(StatusView)
702 |> render("attachment.json", %{attachment: attachment_data})
703 end
704 end
705
706 def favourited_by(conn, %{"id" => id}) do
707 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
708 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
709 q = from(u in User, where: u.ap_id in ^likes)
710 users = Repo.all(q)
711
712 conn
713 |> put_view(AccountView)
714 |> render(AccountView, "accounts.json", %{users: users, as: :user})
715 else
716 _ -> json(conn, [])
717 end
718 end
719
720 def reblogged_by(conn, %{"id" => id}) do
721 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
722 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
723 q = from(u in User, where: u.ap_id in ^announces)
724 users = Repo.all(q)
725
726 conn
727 |> put_view(AccountView)
728 |> render("accounts.json", %{users: users, as: :user})
729 else
730 _ -> json(conn, [])
731 end
732 end
733
734 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
735 local_only = params["local"] in [true, "True", "true", "1"]
736
737 tags =
738 [params["tag"], params["any"]]
739 |> List.flatten()
740 |> Enum.uniq()
741 |> Enum.filter(& &1)
742 |> Enum.map(&String.downcase(&1))
743
744 tag_all =
745 params["all"] ||
746 []
747 |> Enum.map(&String.downcase(&1))
748
749 tag_reject =
750 params["none"] ||
751 []
752 |> Enum.map(&String.downcase(&1))
753
754 activities =
755 params
756 |> Map.put("type", "Create")
757 |> Map.put("local_only", local_only)
758 |> Map.put("blocking_user", user)
759 |> Map.put("muting_user", user)
760 |> Map.put("tag", tags)
761 |> Map.put("tag_all", tag_all)
762 |> Map.put("tag_reject", tag_reject)
763 |> ActivityPub.fetch_public_activities()
764 |> Enum.reverse()
765
766 conn
767 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
768 |> put_view(StatusView)
769 |> render("index.json", %{activities: activities, for: user, as: :activity})
770 end
771
772 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
773 with %User{} = user <- User.get_cached_by_id(id),
774 followers <- MastodonAPI.get_followers(user, params) do
775 followers =
776 cond do
777 for_user && user.id == for_user.id -> followers
778 user.info.hide_followers -> []
779 true -> followers
780 end
781
782 conn
783 |> add_link_headers(:followers, followers, user)
784 |> put_view(AccountView)
785 |> render("accounts.json", %{users: followers, as: :user})
786 end
787 end
788
789 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
790 with %User{} = user <- User.get_cached_by_id(id),
791 followers <- MastodonAPI.get_friends(user, params) do
792 followers =
793 cond do
794 for_user && user.id == for_user.id -> followers
795 user.info.hide_follows -> []
796 true -> followers
797 end
798
799 conn
800 |> add_link_headers(:following, followers, user)
801 |> put_view(AccountView)
802 |> render("accounts.json", %{users: followers, as: :user})
803 end
804 end
805
806 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
807 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
808 conn
809 |> put_view(AccountView)
810 |> render("accounts.json", %{users: follow_requests, as: :user})
811 end
812 end
813
814 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
815 with %User{} = follower <- User.get_cached_by_id(id),
816 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
817 conn
818 |> put_view(AccountView)
819 |> render("relationship.json", %{user: followed, target: follower})
820 else
821 {:error, message} ->
822 conn
823 |> put_resp_content_type("application/json")
824 |> send_resp(403, Jason.encode!(%{"error" => message}))
825 end
826 end
827
828 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
829 with %User{} = follower <- User.get_cached_by_id(id),
830 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
831 conn
832 |> put_view(AccountView)
833 |> render("relationship.json", %{user: followed, target: follower})
834 else
835 {:error, message} ->
836 conn
837 |> put_resp_content_type("application/json")
838 |> send_resp(403, Jason.encode!(%{"error" => message}))
839 end
840 end
841
842 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
843 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
844 {_, true} <- {:followed, follower.id != followed.id},
845 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
846 conn
847 |> put_view(AccountView)
848 |> render("relationship.json", %{user: follower, target: followed})
849 else
850 {:followed, _} ->
851 {:error, :not_found}
852
853 {:error, message} ->
854 conn
855 |> put_resp_content_type("application/json")
856 |> send_resp(403, Jason.encode!(%{"error" => message}))
857 end
858 end
859
860 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
861 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
862 {_, true} <- {:followed, follower.id != followed.id},
863 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
864 conn
865 |> put_view(AccountView)
866 |> render("account.json", %{user: followed, for: follower})
867 else
868 {:followed, _} ->
869 {:error, :not_found}
870
871 {:error, message} ->
872 conn
873 |> put_resp_content_type("application/json")
874 |> send_resp(403, Jason.encode!(%{"error" => message}))
875 end
876 end
877
878 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
879 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
880 {_, true} <- {:followed, follower.id != followed.id},
881 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
882 conn
883 |> put_view(AccountView)
884 |> render("relationship.json", %{user: follower, target: followed})
885 else
886 {:followed, _} ->
887 {:error, :not_found}
888
889 error ->
890 error
891 end
892 end
893
894 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
895 with %User{} = muted <- User.get_cached_by_id(id),
896 {:ok, muter} <- User.mute(muter, muted) do
897 conn
898 |> put_view(AccountView)
899 |> render("relationship.json", %{user: muter, target: muted})
900 else
901 {:error, message} ->
902 conn
903 |> put_resp_content_type("application/json")
904 |> send_resp(403, Jason.encode!(%{"error" => message}))
905 end
906 end
907
908 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
909 with %User{} = muted <- User.get_cached_by_id(id),
910 {:ok, muter} <- User.unmute(muter, muted) do
911 conn
912 |> put_view(AccountView)
913 |> render("relationship.json", %{user: muter, target: muted})
914 else
915 {:error, message} ->
916 conn
917 |> put_resp_content_type("application/json")
918 |> send_resp(403, Jason.encode!(%{"error" => message}))
919 end
920 end
921
922 def mutes(%{assigns: %{user: user}} = conn, _) do
923 with muted_accounts <- User.muted_users(user) do
924 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
925 json(conn, res)
926 end
927 end
928
929 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
930 with %User{} = blocked <- User.get_cached_by_id(id),
931 {:ok, blocker} <- User.block(blocker, blocked),
932 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
933 conn
934 |> put_view(AccountView)
935 |> render("relationship.json", %{user: blocker, target: blocked})
936 else
937 {:error, message} ->
938 conn
939 |> put_resp_content_type("application/json")
940 |> send_resp(403, Jason.encode!(%{"error" => message}))
941 end
942 end
943
944 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
945 with %User{} = blocked <- User.get_cached_by_id(id),
946 {:ok, blocker} <- User.unblock(blocker, blocked),
947 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
948 conn
949 |> put_view(AccountView)
950 |> render("relationship.json", %{user: blocker, target: blocked})
951 else
952 {:error, message} ->
953 conn
954 |> put_resp_content_type("application/json")
955 |> send_resp(403, Jason.encode!(%{"error" => message}))
956 end
957 end
958
959 def blocks(%{assigns: %{user: user}} = conn, _) do
960 with blocked_accounts <- User.blocked_users(user) do
961 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
962 json(conn, res)
963 end
964 end
965
966 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
967 json(conn, info.domain_blocks || [])
968 end
969
970 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
971 User.block_domain(blocker, domain)
972 json(conn, %{})
973 end
974
975 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
976 User.unblock_domain(blocker, domain)
977 json(conn, %{})
978 end
979
980 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
981 with %User{} = subscription_target <- User.get_cached_by_id(id),
982 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
983 conn
984 |> put_view(AccountView)
985 |> render("relationship.json", %{user: user, target: subscription_target})
986 else
987 {:error, message} ->
988 conn
989 |> put_resp_content_type("application/json")
990 |> send_resp(403, Jason.encode!(%{"error" => message}))
991 end
992 end
993
994 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
995 with %User{} = subscription_target <- User.get_cached_by_id(id),
996 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
997 conn
998 |> put_view(AccountView)
999 |> render("relationship.json", %{user: user, target: subscription_target})
1000 else
1001 {:error, message} ->
1002 conn
1003 |> put_resp_content_type("application/json")
1004 |> send_resp(403, Jason.encode!(%{"error" => message}))
1005 end
1006 end
1007
1008 def status_search(user, query) do
1009 fetched =
1010 if Regex.match?(~r/https?:/, query) do
1011 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1012 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1013 true <- Visibility.visible_for_user?(activity, user) do
1014 [activity]
1015 else
1016 _e -> []
1017 end
1018 end || []
1019
1020 q =
1021 from(
1022 [a, o] in Activity.with_preloaded_object(Activity),
1023 where: fragment("?->>'type' = 'Create'", a.data),
1024 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1025 where:
1026 fragment(
1027 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1028 o.data,
1029 ^query
1030 ),
1031 limit: 20,
1032 order_by: [desc: :id]
1033 )
1034
1035 Repo.all(q) ++ fetched
1036 end
1037
1038 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1039 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1040
1041 statuses = status_search(user, query)
1042
1043 tags_path = Web.base_url() <> "/tag/"
1044
1045 tags =
1046 query
1047 |> String.split()
1048 |> Enum.uniq()
1049 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1050 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1051 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1052
1053 res = %{
1054 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1055 "statuses" =>
1056 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1057 "hashtags" => tags
1058 }
1059
1060 json(conn, res)
1061 end
1062
1063 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1064 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1065
1066 statuses = status_search(user, query)
1067
1068 tags =
1069 query
1070 |> String.split()
1071 |> Enum.uniq()
1072 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1073 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1074
1075 res = %{
1076 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1077 "statuses" =>
1078 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1079 "hashtags" => tags
1080 }
1081
1082 json(conn, res)
1083 end
1084
1085 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1086 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1087
1088 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1089
1090 json(conn, res)
1091 end
1092
1093 def favourites(%{assigns: %{user: user}} = conn, params) do
1094 params =
1095 params
1096 |> Map.put("type", "Create")
1097 |> Map.put("favorited_by", user.ap_id)
1098 |> Map.put("blocking_user", user)
1099
1100 activities =
1101 ActivityPub.fetch_activities([], params)
1102 |> Enum.reverse()
1103
1104 user = Repo.preload(user, bookmarks: :activity)
1105
1106 conn
1107 |> add_link_headers(:favourites, activities)
1108 |> put_view(StatusView)
1109 |> render("index.json", %{activities: activities, for: user, as: :activity})
1110 end
1111
1112 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1113 with %User{} = user <- User.get_by_id(id),
1114 false <- user.info.hide_favorites do
1115 params =
1116 params
1117 |> Map.put("type", "Create")
1118 |> Map.put("favorited_by", user.ap_id)
1119 |> Map.put("blocking_user", for_user)
1120
1121 recipients =
1122 if for_user do
1123 ["https://www.w3.org/ns/activitystreams#Public"] ++
1124 [for_user.ap_id | for_user.following]
1125 else
1126 ["https://www.w3.org/ns/activitystreams#Public"]
1127 end
1128
1129 activities =
1130 recipients
1131 |> ActivityPub.fetch_activities(params)
1132 |> Enum.reverse()
1133
1134 conn
1135 |> add_link_headers(:favourites, activities)
1136 |> put_view(StatusView)
1137 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1138 else
1139 nil ->
1140 {:error, :not_found}
1141
1142 true ->
1143 conn
1144 |> put_status(403)
1145 |> json(%{error: "Can't get favorites"})
1146 end
1147 end
1148
1149 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1150 user = User.get_cached_by_id(user.id)
1151 user = Repo.preload(user, bookmarks: :activity)
1152
1153 bookmarks =
1154 Bookmark.for_user_query(user.id)
1155 |> Pagination.fetch_paginated(params)
1156
1157 activities =
1158 bookmarks
1159 |> Enum.map(fn b -> b.activity end)
1160
1161 conn
1162 |> add_link_headers(:bookmarks, bookmarks)
1163 |> put_view(StatusView)
1164 |> render("index.json", %{activities: activities, for: user, as: :activity})
1165 end
1166
1167 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1168 lists = Pleroma.List.for_user(user, opts)
1169 res = ListView.render("lists.json", lists: lists)
1170 json(conn, res)
1171 end
1172
1173 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1174 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1175 res = ListView.render("list.json", list: list)
1176 json(conn, res)
1177 else
1178 _e ->
1179 conn
1180 |> put_status(404)
1181 |> json(%{error: "Record not found"})
1182 end
1183 end
1184
1185 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1186 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1187 res = ListView.render("lists.json", lists: lists)
1188 json(conn, res)
1189 end
1190
1191 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1192 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1193 {:ok, _list} <- Pleroma.List.delete(list) do
1194 json(conn, %{})
1195 else
1196 _e ->
1197 json(conn, "error")
1198 end
1199 end
1200
1201 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1202 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1203 res = ListView.render("list.json", list: list)
1204 json(conn, res)
1205 end
1206 end
1207
1208 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1209 accounts
1210 |> Enum.each(fn account_id ->
1211 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1212 %User{} = followed <- User.get_cached_by_id(account_id) do
1213 Pleroma.List.follow(list, followed)
1214 end
1215 end)
1216
1217 json(conn, %{})
1218 end
1219
1220 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1221 accounts
1222 |> Enum.each(fn account_id ->
1223 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1224 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1225 Pleroma.List.unfollow(list, followed)
1226 end
1227 end)
1228
1229 json(conn, %{})
1230 end
1231
1232 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1233 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1234 {:ok, users} = Pleroma.List.get_following(list) do
1235 conn
1236 |> put_view(AccountView)
1237 |> render("accounts.json", %{users: users, as: :user})
1238 end
1239 end
1240
1241 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1242 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1243 {:ok, list} <- Pleroma.List.rename(list, title) do
1244 res = ListView.render("list.json", list: list)
1245 json(conn, res)
1246 else
1247 _e ->
1248 json(conn, "error")
1249 end
1250 end
1251
1252 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1253 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1254 params =
1255 params
1256 |> Map.put("type", "Create")
1257 |> Map.put("blocking_user", user)
1258 |> Map.put("muting_user", user)
1259
1260 # we must filter the following list for the user to avoid leaking statuses the user
1261 # does not actually have permission to see (for more info, peruse security issue #270).
1262 activities =
1263 following
1264 |> Enum.filter(fn x -> x in user.following end)
1265 |> ActivityPub.fetch_activities_bounded(following, params)
1266 |> Enum.reverse()
1267
1268 user = Repo.preload(user, bookmarks: :activity)
1269
1270 conn
1271 |> put_view(StatusView)
1272 |> render("index.json", %{activities: activities, for: user, as: :activity})
1273 else
1274 _e ->
1275 conn
1276 |> put_status(403)
1277 |> json(%{error: "Error."})
1278 end
1279 end
1280
1281 def index(%{assigns: %{user: user}} = conn, _params) do
1282 token = get_session(conn, :oauth_token)
1283
1284 if user && token do
1285 mastodon_emoji = mastodonized_emoji()
1286
1287 limit = Config.get([:instance, :limit])
1288
1289 accounts =
1290 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1291
1292 flavour = get_user_flavour(user)
1293
1294 initial_state =
1295 %{
1296 meta: %{
1297 streaming_api_base_url:
1298 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
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 %{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