8a528707982c0bfb5637848b1b2a58ff06039df0
[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.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_count,
151 :hide_follows_count,
152 :hide_followers,
153 :hide_follows,
154 :hide_favorites,
155 :show_role,
156 :skip_thread_containment
157 ]
158 |> Enum.reduce(%{}, fn key, acc ->
159 add_if_present(acc, params, to_string(key), key, fn value ->
160 {:ok, ControllerHelper.truthy_param?(value)}
161 end)
162 end)
163 |> add_if_present(params, "default_scope", :default_scope)
164 |> add_if_present(params, "fields", :fields, fn fields ->
165 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
166
167 {:ok, fields}
168 end)
169 |> add_if_present(params, "fields", :raw_fields)
170 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
171 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
172 end)
173 |> add_if_present(params, "header", :banner, fn value ->
174 with %Plug.Upload{} <- value,
175 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
176 {:ok, object.data}
177 else
178 _ -> :error
179 end
180 end)
181 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
182 with %Plug.Upload{} <- value,
183 {:ok, object} <- ActivityPub.upload(value, type: :background) do
184 {:ok, object.data}
185 else
186 _ -> :error
187 end
188 end)
189 |> Map.put(:emoji, user_info_emojis)
190
191 changeset =
192 user
193 |> User.update_changeset(user_params)
194 |> User.change_info(&User.Info.profile_update(&1, info_params))
195
196 with {:ok, user} <- User.update_and_set_cache(changeset) do
197 if original_user != user, do: CommonAPI.update(user)
198
199 json(
200 conn,
201 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
202 )
203 else
204 _e -> render_error(conn, :forbidden, "Invalid request")
205 end
206 end
207
208 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
209 change = Changeset.change(user, %{avatar: nil})
210 {:ok, user} = User.update_and_set_cache(change)
211 CommonAPI.update(user)
212
213 json(conn, %{url: nil})
214 end
215
216 def update_avatar(%{assigns: %{user: user}} = conn, params) do
217 {:ok, object} = ActivityPub.upload(params, type: :avatar)
218 change = Changeset.change(user, %{avatar: object.data})
219 {:ok, user} = User.update_and_set_cache(change)
220 CommonAPI.update(user)
221 %{"url" => [%{"href" => href} | _]} = object.data
222
223 json(conn, %{url: href})
224 end
225
226 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
227 new_info = %{"banner" => %{}}
228
229 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
230 CommonAPI.update(user)
231 json(conn, %{url: nil})
232 end
233 end
234
235 def update_banner(%{assigns: %{user: user}} = conn, params) do
236 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
237 new_info <- %{"banner" => object.data},
238 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
239 CommonAPI.update(user)
240 %{"url" => [%{"href" => href} | _]} = object.data
241
242 json(conn, %{url: href})
243 end
244 end
245
246 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
247 new_info = %{"background" => %{}}
248
249 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
250 json(conn, %{url: nil})
251 end
252 end
253
254 def update_background(%{assigns: %{user: user}} = conn, params) do
255 with {:ok, object} <- ActivityPub.upload(params, type: :background),
256 new_info <- %{"background" => object.data},
257 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
258 %{"url" => [%{"href" => href} | _]} = object.data
259
260 json(conn, %{url: href})
261 end
262 end
263
264 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
265 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
266
267 account =
268 AccountView.render("account.json", %{
269 user: user,
270 for: user,
271 with_pleroma_settings: true,
272 with_chat_token: chat_token
273 })
274
275 json(conn, account)
276 end
277
278 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
279 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
280 conn
281 |> put_view(AppView)
282 |> render("short.json", %{app: app})
283 end
284 end
285
286 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
287 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
288 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
289 account = AccountView.render("account.json", %{user: user, for: for_user})
290 json(conn, account)
291 else
292 _e -> render_error(conn, :not_found, "Can't find user")
293 end
294 end
295
296 @mastodon_api_level "2.7.2"
297
298 def masto_instance(conn, _params) do
299 instance = Config.get(:instance)
300
301 response = %{
302 uri: Web.base_url(),
303 title: Keyword.get(instance, :name),
304 description: Keyword.get(instance, :description),
305 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
306 email: Keyword.get(instance, :email),
307 urls: %{
308 streaming_api: Pleroma.Web.Endpoint.websocket_url()
309 },
310 stats: Stats.get_stats(),
311 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
312 languages: ["en"],
313 registrations: Pleroma.Config.get([:instance, :registrations_open]),
314 # Extra (not present in Mastodon):
315 max_toot_chars: Keyword.get(instance, :limit),
316 poll_limits: Keyword.get(instance, :poll_limits)
317 }
318
319 json(conn, response)
320 end
321
322 def peers(conn, _params) do
323 json(conn, Stats.get_peers())
324 end
325
326 defp mastodonized_emoji do
327 Pleroma.Emoji.get_all()
328 |> Enum.map(fn {shortcode, relative_url, tags} ->
329 url = to_string(URI.merge(Web.base_url(), relative_url))
330
331 %{
332 "shortcode" => shortcode,
333 "static_url" => url,
334 "visible_in_picker" => true,
335 "url" => url,
336 "tags" => tags,
337 # Assuming that a comma is authorized in the category name
338 "category" => (tags -- ["Custom"]) |> Enum.join(",")
339 }
340 end)
341 end
342
343 def custom_emojis(conn, _params) do
344 mastodon_emoji = mastodonized_emoji()
345 json(conn, mastodon_emoji)
346 end
347
348 def home_timeline(%{assigns: %{user: user}} = conn, params) do
349 params =
350 params
351 |> Map.put("type", ["Create", "Announce"])
352 |> Map.put("blocking_user", user)
353 |> Map.put("muting_user", user)
354 |> Map.put("user", user)
355
356 activities =
357 [user.ap_id | user.following]
358 |> ActivityPub.fetch_activities(params)
359 |> Enum.reverse()
360
361 conn
362 |> add_link_headers(activities)
363 |> put_view(StatusView)
364 |> render("index.json", %{activities: activities, for: user, as: :activity})
365 end
366
367 def public_timeline(%{assigns: %{user: user}} = conn, params) do
368 local_only = params["local"] in [true, "True", "true", "1"]
369
370 activities =
371 params
372 |> Map.put("type", ["Create", "Announce"])
373 |> Map.put("local_only", local_only)
374 |> Map.put("blocking_user", user)
375 |> Map.put("muting_user", user)
376 |> ActivityPub.fetch_public_activities()
377 |> Enum.reverse()
378
379 conn
380 |> add_link_headers(activities, %{"local" => local_only})
381 |> put_view(StatusView)
382 |> render("index.json", %{activities: activities, for: user, as: :activity})
383 end
384
385 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
386 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
387 params =
388 params
389 |> Map.put("tag", params["tagged"])
390
391 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
392
393 conn
394 |> add_link_headers(activities)
395 |> put_view(StatusView)
396 |> render("index.json", %{
397 activities: activities,
398 for: reading_user,
399 as: :activity
400 })
401 end
402 end
403
404 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
405 params =
406 params
407 |> Map.put("type", "Create")
408 |> Map.put("blocking_user", user)
409 |> Map.put("user", user)
410 |> Map.put(:visibility, "direct")
411
412 activities =
413 [user.ap_id]
414 |> ActivityPub.fetch_activities_query(params)
415 |> Pagination.fetch_paginated(params)
416
417 conn
418 |> add_link_headers(activities)
419 |> put_view(StatusView)
420 |> render("index.json", %{activities: activities, for: user, as: :activity})
421 end
422
423 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
424 limit = 100
425
426 activities =
427 ids
428 |> Enum.take(limit)
429 |> Activity.all_by_ids_with_object()
430 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
431
432 conn
433 |> put_view(StatusView)
434 |> render("index.json", activities: activities, for: user, as: :activity)
435 end
436
437 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
438 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
439 true <- Visibility.visible_for_user?(activity, user) do
440 conn
441 |> put_view(StatusView)
442 |> try_render("status.json", %{activity: activity, for: user})
443 end
444 end
445
446 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
447 with %Activity{} = activity <- Activity.get_by_id(id),
448 activities <-
449 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
450 "blocking_user" => user,
451 "user" => user,
452 "exclude_id" => activity.id
453 }),
454 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
455 result = %{
456 ancestors:
457 StatusView.render(
458 "index.json",
459 for: user,
460 activities: grouped_activities[true] || [],
461 as: :activity
462 )
463 |> Enum.reverse(),
464 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
465 descendants:
466 StatusView.render(
467 "index.json",
468 for: user,
469 activities: grouped_activities[false] || [],
470 as: :activity
471 )
472 |> Enum.reverse()
473 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
474 }
475
476 json(conn, result)
477 end
478 end
479
480 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
481 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
482 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
483 true <- Visibility.visible_for_user?(activity, user) do
484 conn
485 |> put_view(StatusView)
486 |> try_render("poll.json", %{object: object, for: user})
487 else
488 error when is_nil(error) or error == false ->
489 render_error(conn, :not_found, "Record not found")
490 end
491 end
492
493 defp get_cached_vote_or_vote(user, object, choices) do
494 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
495
496 {_, res} =
497 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
498 case CommonAPI.vote(user, object, choices) do
499 {:error, _message} = res -> {:ignore, res}
500 res -> {:commit, res}
501 end
502 end)
503
504 res
505 end
506
507 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
508 with %Object{} = object <- Object.get_by_id(id),
509 true <- object.data["type"] == "Question",
510 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
511 true <- Visibility.visible_for_user?(activity, user),
512 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
513 conn
514 |> put_view(StatusView)
515 |> try_render("poll.json", %{object: object, for: user})
516 else
517 nil ->
518 render_error(conn, :not_found, "Record not found")
519
520 false ->
521 render_error(conn, :not_found, "Record not found")
522
523 {:error, message} ->
524 conn
525 |> put_status(:unprocessable_entity)
526 |> json(%{error: message})
527 end
528 end
529
530 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
531 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
532 conn
533 |> add_link_headers(scheduled_activities)
534 |> put_view(ScheduledActivityView)
535 |> render("index.json", %{scheduled_activities: scheduled_activities})
536 end
537 end
538
539 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
540 with %ScheduledActivity{} = scheduled_activity <-
541 ScheduledActivity.get(user, scheduled_activity_id) do
542 conn
543 |> put_view(ScheduledActivityView)
544 |> render("show.json", %{scheduled_activity: scheduled_activity})
545 else
546 _ -> {:error, :not_found}
547 end
548 end
549
550 def update_scheduled_status(
551 %{assigns: %{user: user}} = conn,
552 %{"id" => scheduled_activity_id} = params
553 ) do
554 with %ScheduledActivity{} = scheduled_activity <-
555 ScheduledActivity.get(user, scheduled_activity_id),
556 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
557 conn
558 |> put_view(ScheduledActivityView)
559 |> render("show.json", %{scheduled_activity: scheduled_activity})
560 else
561 nil -> {:error, :not_found}
562 error -> error
563 end
564 end
565
566 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
567 with %ScheduledActivity{} = scheduled_activity <-
568 ScheduledActivity.get(user, scheduled_activity_id),
569 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
570 conn
571 |> put_view(ScheduledActivityView)
572 |> render("show.json", %{scheduled_activity: scheduled_activity})
573 else
574 nil -> {:error, :not_found}
575 error -> error
576 end
577 end
578
579 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
580 params =
581 params
582 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
583
584 scheduled_at = params["scheduled_at"]
585
586 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
587 with {:ok, scheduled_activity} <-
588 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
589 conn
590 |> put_view(ScheduledActivityView)
591 |> render("show.json", %{scheduled_activity: scheduled_activity})
592 end
593 else
594 params = Map.drop(params, ["scheduled_at"])
595
596 case CommonAPI.post(user, params) do
597 {:error, message} ->
598 conn
599 |> put_status(:unprocessable_entity)
600 |> json(%{error: message})
601
602 {:ok, activity} ->
603 conn
604 |> put_view(StatusView)
605 |> try_render("status.json", %{
606 activity: activity,
607 for: user,
608 as: :activity,
609 with_direct_conversation_id: true
610 })
611 end
612 end
613 end
614
615 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
616 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
617 json(conn, %{})
618 else
619 _e -> render_error(conn, :forbidden, "Can't delete this post")
620 end
621 end
622
623 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
624 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
625 %Activity{} = announce <- Activity.normalize(announce.data) do
626 conn
627 |> put_view(StatusView)
628 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
629 end
630 end
631
632 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
633 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
634 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
635 conn
636 |> put_view(StatusView)
637 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
638 end
639 end
640
641 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
642 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
643 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
644 conn
645 |> put_view(StatusView)
646 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
647 end
648 end
649
650 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
651 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
652 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
653 conn
654 |> put_view(StatusView)
655 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
656 end
657 end
658
659 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
660 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
661 conn
662 |> put_view(StatusView)
663 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
664 end
665 end
666
667 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
668 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) 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 bookmark_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.create(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 unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
687 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
688 %User{} = user <- User.get_cached_by_nickname(user.nickname),
689 true <- Visibility.visible_for_user?(activity, user),
690 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
691 conn
692 |> put_view(StatusView)
693 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
694 end
695 end
696
697 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
698 activity = Activity.get_by_id(id)
699
700 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
701 conn
702 |> put_view(StatusView)
703 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
704 end
705 end
706
707 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
708 activity = Activity.get_by_id(id)
709
710 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
711 conn
712 |> put_view(StatusView)
713 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
714 end
715 end
716
717 def notifications(%{assigns: %{user: user}} = conn, params) do
718 notifications = MastodonAPI.get_notifications(user, params)
719
720 conn
721 |> add_link_headers(notifications)
722 |> put_view(NotificationView)
723 |> render("index.json", %{notifications: notifications, for: user})
724 end
725
726 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
727 with {:ok, notification} <- Notification.get(user, id) do
728 conn
729 |> put_view(NotificationView)
730 |> render("show.json", %{notification: notification, for: user})
731 else
732 {:error, reason} ->
733 conn
734 |> put_status(:forbidden)
735 |> json(%{"error" => reason})
736 end
737 end
738
739 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
740 Notification.clear(user)
741 json(conn, %{})
742 end
743
744 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
745 with {:ok, _notif} <- Notification.dismiss(user, id) do
746 json(conn, %{})
747 else
748 {:error, reason} ->
749 conn
750 |> put_status(:forbidden)
751 |> json(%{"error" => reason})
752 end
753 end
754
755 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
756 Notification.destroy_multiple(user, ids)
757 json(conn, %{})
758 end
759
760 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
761 id = List.wrap(id)
762 q = from(u in User, where: u.id in ^id)
763 targets = Repo.all(q)
764
765 conn
766 |> put_view(AccountView)
767 |> render("relationships.json", %{user: user, targets: targets})
768 end
769
770 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
771 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
772
773 def update_media(%{assigns: %{user: user}} = conn, data) do
774 with %Object{} = object <- Repo.get(Object, data["id"]),
775 true <- Object.authorize_mutation(object, user),
776 true <- is_binary(data["description"]),
777 description <- data["description"] do
778 new_data = %{object.data | "name" => description}
779
780 {:ok, _} =
781 object
782 |> Object.change(%{data: new_data})
783 |> Repo.update()
784
785 attachment_data = Map.put(new_data, "id", object.id)
786
787 conn
788 |> put_view(StatusView)
789 |> render("attachment.json", %{attachment: attachment_data})
790 end
791 end
792
793 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
794 with {:ok, object} <-
795 ActivityPub.upload(
796 file,
797 actor: User.ap_id(user),
798 description: Map.get(data, "description")
799 ) do
800 attachment_data = Map.put(object.data, "id", object.id)
801
802 conn
803 |> put_view(StatusView)
804 |> render("attachment.json", %{attachment: attachment_data})
805 end
806 end
807
808 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
809 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
810 %{} = attachment_data <- Map.put(object.data, "id", object.id),
811 # Reject if not an image
812 %{type: "image"} = rendered <-
813 StatusView.render("attachment.json", %{attachment: attachment_data}) do
814 # Sure!
815 # Save to the user's info
816 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
817
818 json(conn, rendered)
819 else
820 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
821 end
822 end
823
824 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
825 mascot = User.get_mascot(user)
826
827 conn
828 |> json(mascot)
829 end
830
831 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
832 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
833 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
834 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
835 q = from(u in User, where: u.ap_id in ^likes)
836
837 users =
838 Repo.all(q)
839 |> Enum.filter(&(not User.blocks?(user, &1)))
840
841 conn
842 |> put_view(AccountView)
843 |> render("accounts.json", %{for: user, users: users, as: :user})
844 else
845 {:visible, false} -> {:error, :not_found}
846 _ -> json(conn, [])
847 end
848 end
849
850 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
851 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
852 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
853 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
854 q = from(u in User, where: u.ap_id in ^announces)
855
856 users =
857 Repo.all(q)
858 |> Enum.filter(&(not User.blocks?(user, &1)))
859
860 conn
861 |> put_view(AccountView)
862 |> render("accounts.json", %{for: user, users: users, as: :user})
863 else
864 {:visible, false} -> {:error, :not_found}
865 _ -> json(conn, [])
866 end
867 end
868
869 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
870 local_only = params["local"] in [true, "True", "true", "1"]
871
872 tags =
873 [params["tag"], params["any"]]
874 |> List.flatten()
875 |> Enum.uniq()
876 |> Enum.filter(& &1)
877 |> Enum.map(&String.downcase(&1))
878
879 tag_all =
880 params["all"] ||
881 []
882 |> Enum.map(&String.downcase(&1))
883
884 tag_reject =
885 params["none"] ||
886 []
887 |> Enum.map(&String.downcase(&1))
888
889 activities =
890 params
891 |> Map.put("type", "Create")
892 |> Map.put("local_only", local_only)
893 |> Map.put("blocking_user", user)
894 |> Map.put("muting_user", user)
895 |> Map.put("user", user)
896 |> Map.put("tag", tags)
897 |> Map.put("tag_all", tag_all)
898 |> Map.put("tag_reject", tag_reject)
899 |> ActivityPub.fetch_public_activities()
900 |> Enum.reverse()
901
902 conn
903 |> add_link_headers(activities, %{"local" => local_only})
904 |> put_view(StatusView)
905 |> render("index.json", %{activities: activities, for: user, as: :activity})
906 end
907
908 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
909 with %User{} = user <- User.get_cached_by_id(id),
910 followers <- MastodonAPI.get_followers(user, params) do
911 followers =
912 cond do
913 for_user && user.id == for_user.id -> followers
914 user.info.hide_followers -> []
915 true -> followers
916 end
917
918 conn
919 |> add_link_headers(followers)
920 |> put_view(AccountView)
921 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
922 end
923 end
924
925 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
926 with %User{} = user <- User.get_cached_by_id(id),
927 followers <- MastodonAPI.get_friends(user, params) do
928 followers =
929 cond do
930 for_user && user.id == for_user.id -> followers
931 user.info.hide_follows -> []
932 true -> followers
933 end
934
935 conn
936 |> add_link_headers(followers)
937 |> put_view(AccountView)
938 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
939 end
940 end
941
942 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
943 follow_requests = User.get_follow_requests(followed)
944
945 conn
946 |> put_view(AccountView)
947 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
948 end
949
950 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
951 with %User{} = follower <- User.get_cached_by_id(id),
952 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
953 conn
954 |> put_view(AccountView)
955 |> render("relationship.json", %{user: followed, target: follower})
956 else
957 {:error, message} ->
958 conn
959 |> put_status(:forbidden)
960 |> json(%{error: message})
961 end
962 end
963
964 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
965 with %User{} = follower <- User.get_cached_by_id(id),
966 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
967 conn
968 |> put_view(AccountView)
969 |> render("relationship.json", %{user: followed, target: follower})
970 else
971 {:error, message} ->
972 conn
973 |> put_status(:forbidden)
974 |> json(%{error: message})
975 end
976 end
977
978 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
979 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
980 {_, true} <- {:followed, follower.id != followed.id},
981 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
982 conn
983 |> put_view(AccountView)
984 |> render("relationship.json", %{user: follower, target: followed})
985 else
986 {:followed, _} ->
987 {:error, :not_found}
988
989 {:error, message} ->
990 conn
991 |> put_status(:forbidden)
992 |> json(%{error: message})
993 end
994 end
995
996 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
997 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
998 {_, true} <- {:followed, follower.id != followed.id},
999 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1000 conn
1001 |> put_view(AccountView)
1002 |> render("account.json", %{user: followed, for: follower})
1003 else
1004 {:followed, _} ->
1005 {:error, :not_found}
1006
1007 {:error, message} ->
1008 conn
1009 |> put_status(:forbidden)
1010 |> json(%{error: message})
1011 end
1012 end
1013
1014 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1015 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1016 {_, true} <- {:followed, follower.id != followed.id},
1017 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1018 conn
1019 |> put_view(AccountView)
1020 |> render("relationship.json", %{user: follower, target: followed})
1021 else
1022 {:followed, _} ->
1023 {:error, :not_found}
1024
1025 error ->
1026 error
1027 end
1028 end
1029
1030 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1031 notifications =
1032 if Map.has_key?(params, "notifications"),
1033 do: params["notifications"] in [true, "True", "true", "1"],
1034 else: true
1035
1036 with %User{} = muted <- User.get_cached_by_id(id),
1037 {:ok, muter} <- User.mute(muter, muted, notifications) do
1038 conn
1039 |> put_view(AccountView)
1040 |> render("relationship.json", %{user: muter, target: muted})
1041 else
1042 {:error, message} ->
1043 conn
1044 |> put_status(:forbidden)
1045 |> json(%{error: message})
1046 end
1047 end
1048
1049 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1050 with %User{} = muted <- User.get_cached_by_id(id),
1051 {:ok, muter} <- User.unmute(muter, muted) do
1052 conn
1053 |> put_view(AccountView)
1054 |> render("relationship.json", %{user: muter, target: muted})
1055 else
1056 {:error, message} ->
1057 conn
1058 |> put_status(:forbidden)
1059 |> json(%{error: message})
1060 end
1061 end
1062
1063 def mutes(%{assigns: %{user: user}} = conn, _) do
1064 with muted_accounts <- User.muted_users(user) do
1065 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1066 json(conn, res)
1067 end
1068 end
1069
1070 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1071 with %User{} = blocked <- User.get_cached_by_id(id),
1072 {:ok, blocker} <- User.block(blocker, blocked),
1073 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1074 conn
1075 |> put_view(AccountView)
1076 |> render("relationship.json", %{user: blocker, target: blocked})
1077 else
1078 {:error, message} ->
1079 conn
1080 |> put_status(:forbidden)
1081 |> json(%{error: message})
1082 end
1083 end
1084
1085 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1086 with %User{} = blocked <- User.get_cached_by_id(id),
1087 {:ok, blocker} <- User.unblock(blocker, blocked),
1088 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1089 conn
1090 |> put_view(AccountView)
1091 |> render("relationship.json", %{user: blocker, target: blocked})
1092 else
1093 {:error, message} ->
1094 conn
1095 |> put_status(:forbidden)
1096 |> json(%{error: message})
1097 end
1098 end
1099
1100 def blocks(%{assigns: %{user: user}} = conn, _) do
1101 with blocked_accounts <- User.blocked_users(user) do
1102 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1103 json(conn, res)
1104 end
1105 end
1106
1107 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1108 json(conn, info.domain_blocks || [])
1109 end
1110
1111 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1112 User.block_domain(blocker, domain)
1113 json(conn, %{})
1114 end
1115
1116 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1117 User.unblock_domain(blocker, domain)
1118 json(conn, %{})
1119 end
1120
1121 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1122 with %User{} = subscription_target <- User.get_cached_by_id(id),
1123 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1124 conn
1125 |> put_view(AccountView)
1126 |> render("relationship.json", %{user: user, target: subscription_target})
1127 else
1128 {:error, message} ->
1129 conn
1130 |> put_status(:forbidden)
1131 |> json(%{error: message})
1132 end
1133 end
1134
1135 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1136 with %User{} = subscription_target <- User.get_cached_by_id(id),
1137 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1138 conn
1139 |> put_view(AccountView)
1140 |> render("relationship.json", %{user: user, target: subscription_target})
1141 else
1142 {:error, message} ->
1143 conn
1144 |> put_status(:forbidden)
1145 |> json(%{error: message})
1146 end
1147 end
1148
1149 def favourites(%{assigns: %{user: user}} = conn, params) do
1150 params =
1151 params
1152 |> Map.put("type", "Create")
1153 |> Map.put("favorited_by", user.ap_id)
1154 |> Map.put("blocking_user", user)
1155
1156 activities =
1157 ActivityPub.fetch_activities([], params)
1158 |> Enum.reverse()
1159
1160 conn
1161 |> add_link_headers(activities)
1162 |> put_view(StatusView)
1163 |> render("index.json", %{activities: activities, for: user, as: :activity})
1164 end
1165
1166 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1167 with %User{} = user <- User.get_by_id(id),
1168 false <- user.info.hide_favorites do
1169 params =
1170 params
1171 |> Map.put("type", "Create")
1172 |> Map.put("favorited_by", user.ap_id)
1173 |> Map.put("blocking_user", for_user)
1174
1175 recipients =
1176 if for_user do
1177 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1178 else
1179 [Pleroma.Constants.as_public()]
1180 end
1181
1182 activities =
1183 recipients
1184 |> ActivityPub.fetch_activities(params)
1185 |> Enum.reverse()
1186
1187 conn
1188 |> add_link_headers(activities)
1189 |> put_view(StatusView)
1190 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1191 else
1192 nil -> {:error, :not_found}
1193 true -> render_error(conn, :forbidden, "Can't get favorites")
1194 end
1195 end
1196
1197 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1198 user = User.get_cached_by_id(user.id)
1199
1200 bookmarks =
1201 Bookmark.for_user_query(user.id)
1202 |> Pagination.fetch_paginated(params)
1203
1204 activities =
1205 bookmarks
1206 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1207
1208 conn
1209 |> add_link_headers(bookmarks)
1210 |> put_view(StatusView)
1211 |> render("index.json", %{activities: activities, for: user, as: :activity})
1212 end
1213
1214 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1215 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1216 res = ListView.render("lists.json", lists: lists)
1217 json(conn, res)
1218 end
1219
1220 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1221 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1222 params =
1223 params
1224 |> Map.put("type", "Create")
1225 |> Map.put("blocking_user", user)
1226 |> Map.put("user", user)
1227 |> Map.put("muting_user", user)
1228
1229 # we must filter the following list for the user to avoid leaking statuses the user
1230 # does not actually have permission to see (for more info, peruse security issue #270).
1231 activities =
1232 following
1233 |> Enum.filter(fn x -> x in user.following end)
1234 |> ActivityPub.fetch_activities_bounded(following, params)
1235 |> Enum.reverse()
1236
1237 conn
1238 |> put_view(StatusView)
1239 |> render("index.json", %{activities: activities, for: user, as: :activity})
1240 else
1241 _e -> render_error(conn, :forbidden, "Error.")
1242 end
1243 end
1244
1245 def index(%{assigns: %{user: user}} = conn, _params) do
1246 token = get_session(conn, :oauth_token)
1247
1248 if user && token do
1249 mastodon_emoji = mastodonized_emoji()
1250
1251 limit = Config.get([:instance, :limit])
1252
1253 accounts =
1254 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1255
1256 initial_state =
1257 %{
1258 meta: %{
1259 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1260 access_token: token,
1261 locale: "en",
1262 domain: Pleroma.Web.Endpoint.host(),
1263 admin: "1",
1264 me: "#{user.id}",
1265 unfollow_modal: false,
1266 boost_modal: false,
1267 delete_modal: true,
1268 auto_play_gif: false,
1269 display_sensitive_media: false,
1270 reduce_motion: false,
1271 max_toot_chars: limit,
1272 mascot: User.get_mascot(user)["url"]
1273 },
1274 poll_limits: Config.get([:instance, :poll_limits]),
1275 rights: %{
1276 delete_others_notice: present?(user.info.is_moderator),
1277 admin: present?(user.info.is_admin)
1278 },
1279 compose: %{
1280 me: "#{user.id}",
1281 default_privacy: user.info.default_scope,
1282 default_sensitive: false,
1283 allow_content_types: Config.get([:instance, :allowed_post_formats])
1284 },
1285 media_attachments: %{
1286 accept_content_types: [
1287 ".jpg",
1288 ".jpeg",
1289 ".png",
1290 ".gif",
1291 ".webm",
1292 ".mp4",
1293 ".m4v",
1294 "image\/jpeg",
1295 "image\/png",
1296 "image\/gif",
1297 "video\/webm",
1298 "video\/mp4"
1299 ]
1300 },
1301 settings:
1302 user.info.settings ||
1303 %{
1304 onboarded: true,
1305 home: %{
1306 shows: %{
1307 reblog: true,
1308 reply: true
1309 }
1310 },
1311 notifications: %{
1312 alerts: %{
1313 follow: true,
1314 favourite: true,
1315 reblog: true,
1316 mention: true
1317 },
1318 shows: %{
1319 follow: true,
1320 favourite: true,
1321 reblog: true,
1322 mention: true
1323 },
1324 sounds: %{
1325 follow: true,
1326 favourite: true,
1327 reblog: true,
1328 mention: true
1329 }
1330 }
1331 },
1332 push_subscription: nil,
1333 accounts: accounts,
1334 custom_emojis: mastodon_emoji,
1335 char_limit: limit
1336 }
1337 |> Jason.encode!()
1338
1339 conn
1340 |> put_layout(false)
1341 |> put_view(MastodonView)
1342 |> render("index.html", %{initial_state: initial_state})
1343 else
1344 conn
1345 |> put_session(:return_to, conn.request_path)
1346 |> redirect(to: "/web/login")
1347 end
1348 end
1349
1350 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1351 with {:ok, _user} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) 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(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