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