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