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