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