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