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