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