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