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