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