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