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