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