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